diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 815e0059..2488dd4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,16 @@ -name: basePower 1.17 CI +name: basePower 1.18 CI on: push: - branches: [master-1.17.1, 'feature/**'] + branches: [master-1.18.2, 'feature/**'] pull_request: - branches: [master-1.17.1] + branches: [master-1.18.2] permissions: contents: read concurrency: - group: poweradvantage-1.17-${{ github.workflow }}-${{ github.ref }} + group: basepower-1.18-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/OreSpawn - ref: b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf + ref: d31f52f296739f34a0c13f554abf3e1ac071ae25 path: .ci-dependencies/OreSpawn - name: Use canonical LF dependency inputs # Use the same source bytes on hosted and local dependency builds. @@ -34,24 +34,19 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/BaseMetals - ref: 0deeeeecdbbc1fbcdecf910c59477de8efe164d7 + ref: 3488ed58403722ac3ff203cff7680dfc16e0fd3f path: .ci-dependencies/BaseMetals - name: Install pinned Java 25 Mavenizer runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '25.0.3+9.0.LTS' - - name: Install Java 16 production toolchain - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 - with: - distribution: temurin - java-version: '16.0.2+7' - name: Install Java 8 renamer runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '8.0.502+7' - - name: Install Java 17 for Gradle + - name: Install Java 17 for Gradle and production uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin @@ -60,7 +55,7 @@ jobs: # These are Gradle project properties, not JVM system properties. shell: bash run: >- - echo "MMD_GRADLE_TOOLCHAINS=-Porg.gradle.java.installations.paths=$JAVA_HOME,$JAVA_HOME_16_X64,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64 + echo "MMD_GRADLE_TOOLCHAINS=-Porg.gradle.java.installations.paths=$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64 -Porg.gradle.java.installations.auto-detect=false -Porg.gradle.java.installations.auto-download=false" >> "$GITHUB_ENV" - name: Set up Gradle @@ -75,8 +70,8 @@ jobs: chmod +x "$orespawn/gradlew" "$basemetals/gradlew" (cd "$orespawn" && ./gradlew $MMD_GRADLE_TOOLCHAINS jar --no-daemon --stacktrace) (cd "$basemetals" && ./gradlew $MMD_GRADLE_TOOLCHAINS jar --no-daemon --stacktrace) - osjar="$orespawn/build/libs-dev/OreSpawn-4.1.0.117011-deobf.jar" - bmjar="$basemetals/build/libs-dev/BaseMetals-3.1.1.117011-deobf.jar" + osjar="$orespawn/build/libs-dev/OreSpawn-4.1.0.118021-deobf.jar" + bmjar="$basemetals/build/libs-dev/BaseMetals-3.1.2.118021-deobf.jar" test -f "$osjar" && test -f "$bmjar" echo "orespawn=$osjar" >> "$GITHUB_OUTPUT" echo "basemetals=$bmjar" >> "$GITHUB_OUTPUT" @@ -90,20 +85,20 @@ jobs: - name: Upload audited release candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: PowerAdvantage-1.17.1-${{ github.sha }} + name: basePower-1.18.2-${{ github.sha }} if-no-files-found: error retention-days: 30 path: | - build/libs/basePower-2.4.9.117011.jar - build/libs/basePower-2.4.9.117011-sources.jar - build/libs/basePower-2.4.9.117011-javadoc.jar + build/libs/basePower-2.4.10.118021.jar + build/libs/basePower-2.4.10.118021-sources.jar + build/libs/basePower-2.4.10.118021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt - name: Upload diagnostics on failure if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: PowerAdvantage-1.17.1-diagnostics-${{ github.sha }} + name: basePower-1.18.2-diagnostics-${{ github.sha }} if-no-files-found: ignore retention-days: 14 path: | diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b8afd74d..a9cca5eb 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,9 +2,9 @@ name: CodeQL on: push: - branches: [master-1.17.1, 'feature/**'] + branches: [master-1.18.2, 'feature/**'] pull_request: - branches: [master-1.17.1] + branches: [master-1.18.2] schedule: - cron: '47 7 * * 4' @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/OreSpawn - ref: b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf + ref: d31f52f296739f34a0c13f554abf3e1ac071ae25 path: .ci-dependencies/OreSpawn - name: Use canonical LF dependency inputs # Use the same source bytes on hosted and local dependency builds. @@ -34,24 +34,19 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/BaseMetals - ref: 0deeeeecdbbc1fbcdecf910c59477de8efe164d7 + ref: 3488ed58403722ac3ff203cff7680dfc16e0fd3f path: .ci-dependencies/BaseMetals - name: Install pinned Java 25 Mavenizer runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '25.0.3+9.0.LTS' - - name: Install Java 16 production toolchain - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 - with: - distribution: temurin - java-version: '16.0.2+7' - name: Install Java 8 renamer runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '8.0.502+7' - - name: Install Java 17 for Gradle + - name: Install Java 17 for Gradle and production uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin @@ -60,7 +55,7 @@ jobs: # These are Gradle project properties, not JVM system properties. shell: bash run: >- - echo "MMD_GRADLE_TOOLCHAINS=-Porg.gradle.java.installations.paths=$JAVA_HOME,$JAVA_HOME_16_X64,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64 + echo "MMD_GRADLE_TOOLCHAINS=-Porg.gradle.java.installations.paths=$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64 -Porg.gradle.java.installations.auto-detect=false -Porg.gradle.java.installations.auto-download=false" >> "$GITHUB_ENV" - name: Set up Gradle @@ -75,8 +70,8 @@ jobs: chmod +x "$orespawn/gradlew" "$basemetals/gradlew" (cd "$orespawn" && ./gradlew $MMD_GRADLE_TOOLCHAINS jar --no-daemon --stacktrace) (cd "$basemetals" && ./gradlew $MMD_GRADLE_TOOLCHAINS jar --no-daemon --stacktrace) - echo "orespawn=$orespawn/build/libs-dev/OreSpawn-4.1.0.117011-deobf.jar" >> "$GITHUB_OUTPUT" - echo "basemetals=$basemetals/build/libs-dev/BaseMetals-3.1.1.117011-deobf.jar" >> "$GITHUB_OUTPUT" + echo "orespawn=$orespawn/build/libs-dev/OreSpawn-4.1.0.118021-deobf.jar" >> "$GITHUB_OUTPUT" + echo "basemetals=$basemetals/build/libs-dev/BaseMetals-3.1.2.118021-deobf.jar" >> "$GITHUB_OUTPUT" - name: Initialize CodeQL uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 with: diff --git a/.github/workflows/validate-gradle-build.yml b/.github/workflows/validate-gradle-build.yml index 0bf04b84..afe21adc 100644 --- a/.github/workflows/validate-gradle-build.yml +++ b/.github/workflows/validate-gradle-build.yml @@ -3,11 +3,11 @@ name: Validate Gradle Wrapper on: push: branches: - - master-1.17.1 + - master-1.18.2 - 'feature/**' pull_request: branches: - - master-1.17.1 + - master-1.18.2 permissions: contents: read diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 463bdc95..0f89f797 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +basePower 2.4.10.118021 (Minecraft 1.18.2) + +- Bring basePower to Forge 40 and Java 17, keeping the same blocks, items and machine data. +- Use Forge's registry lifecycle for blocks, items and oil, and native save and update packets for machines. +- Carry old machine and entity data into 1.18's chunk format without blocking new terrain below the old world floor. +- Retire old Forge biome entries only when Minecraft's own data fixer handles the rename or merge. +- When upgrading with Base Metals, use 3.1.2. It lets basePower preserve old item wear, entities and machine facings before Minecraft converts the chunks. + basePower 2.4.9.117011 (Minecraft 1.17.1) - Bring basePower to Minecraft 1.17.1 with one shared tab for the three mods. diff --git a/README.md b/README.md index 6f030dd5..ad901af2 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ # basePower -**Minecraft 1.17.1, version 2.4.9.117011. Local test candidate.** +**Minecraft 1.18.2, version 2.4.10.118021.** -See the [1.17 port record](docs/1.17/PORT-STATUS.md) for this candidate's -validation status. The retained 1.16 and earlier reports are historical evidence, -not passes for 1.17. Manual testing follows the current automated qualification. +See the [1.18 port record](docs/1.18/PORT-STATUS.md) for the current checks. +Results from earlier versions do not qualify this build. basePower continues DrCyano's Power Advantage. It provides shared power, fluids and item transport for baseSteam and baseElectric. **Back up the entire world before upgrading. Test only disposable copies.** -The port retains existing registry names and legacy migration code. Upgrade tests -use disposable original and maintained saves, including connected machines. -Check every other mod's target-version support too. +The port keeps existing registry names and machine data. I tested disposable +copies of original Advantage and released base* saves, including connected +machines, and checked them again after closing and restarting Minecraft. +Check that the other mods in your world support 1.18.2 before you upgrade. The accepted [1.14 results](docs/1.14/BROADER-QUALIFICATION.md) are historical evidence, not qualification of this port or a whole modpack. @@ -20,18 +20,22 @@ Recipe-book discovery now follows relevant ingredients. See the [all-Advantage m The [manufacturing progression audit](docs/1.13/MANUFACTURING-PROGRESSION.md) traces survival parts from raw resources, including configured material aliases, processing, loot-only parts and automated restart checks. -## Building Minecraft 1.17.1 +## Building Minecraft 1.18.2 -The `master-1.17.1` build uses Forge 37.1.1, official `1.17.1` mappings, -MCP `20210706.113038`, ForgeGradle 7.0.34 and Gradle 9.6.1. -Run Gradle with Java 17 and compile and run production code with Java 16. Mavenizer also +The build uses Forge 40.3.0, official `1.18.2` mappings, +MCP `20220404.173914`, ForgeGradle 7.0.34 and Gradle 9.6.1. +Use Java 17 for Gradle, compilation and Minecraft. Mavenizer also needs Java 25. CI pins these JDKs and Java 8 for the legacy renamer. Pass the installed homes using -`-Porg.gradle.java.installations.paths=,,,` and disable +`-Porg.gradle.java.installations.paths=,,` and disable automatic discovery and downloads for a reproducibility check. -Build the pinned Base Metals `3.1.1.117011` and OreSpawn `4.1.0.117011` deobf jars first. -Use Base Metals 3.1.1 for its 1.17.1 startup fix. The build pins the exact -dependency source and checksums in `gradle.properties`. +Build the pinned Base Metals `3.1.2.118021` and OreSpawn `4.1.0.118021` deobf jars first. +The source pins and checksums are in `gradle.properties`. +Check out the pinned dependency sources with `core.autocrlf=false` so the +Base Metals development jar matches CI. +Use Base Metals 3.1.2 for upgrades with Base Metals installed. Its shared +save hook lets both mods preserve old items and machines before Minecraft +converts the chunk. Older Base Metals builds do not have that hook. Then run from the repository root: @@ -42,8 +46,8 @@ gradlew.bat clean check build javadoc verifyReleaseDependencies verifyReleaseArt The default dependency jars are: ```text -D:\BaseMetals\BaseMetals 117\BaseMetals\build\libs-dev\BaseMetals-3.1.1.117011-deobf.jar -D:\MinecraftMineralogy\OS117\OreSpawn\build\libs-dev\OreSpawn-4.1.0.117011-deobf.jar +D:\BaseMetals\BaseMetals 118\BaseMetals\build\libs-dev\BaseMetals-3.1.2.118021-deobf.jar +D:\MinecraftMineralogy\OS118\OreSpawn\build\libs-dev\OreSpawn-4.1.0.118021-deobf.jar ``` Other layouts can pass `-PbaseMetalsDeobfJar=` and `-PoreSpawnDeobfJar=`. @@ -65,10 +69,10 @@ Refresh the project in Eclipse, allow its Java build to finish, and launch the g Eclipse copies production resources from `build/resources/main`, not directly from `src/main/resources`. This prevents its builder from overwriting the expanded `mods.toml` with raw `${version}` placeholders. After updating this setup, use **Gradle > Refresh Gradle Project**, then **Project > Clean** before relaunching. After editing resources or version properties, run `processResources` or refresh the Gradle project before the Eclipse build. See the [metadata regression notes](docs/1.13/ECLIPSE-METADATA.md). -To prepare a combined client, build the baseSteam and baseElectric `2.2.10.117011` deobf jars, +To prepare a combined client, build the baseSteam and baseElectric `2.2.11.118021` deobf jars, run `gradlew.bat prepareCombinedEclipseLaunch`, refresh Eclipse, then launch `runClientAllAdvantage.launch`. Inputs must match -`test-rig/profiles/native117/dev-core.json`; alternative layouts can pass +`test-rig/profiles/native118/dev-core.json`; alternative layouts can pass `-PsteamAdvantageDeobfJar=` and `-PelectricAdvantageDeobfJar=`. The ordinary `runClient.launch` remains Power-only. Both use the `run` directory; preparing a launch does not copy, open or qualify a save. @@ -77,15 +81,9 @@ preparing a launch does not copy, open or qualify a save. which reads every development runtime jar entry and checks its size and CRC. This catches damaged cached dependencies before the next test launch. -Client-resource and recipe-book checks must use the exact 1.17 candidate. -See the [client test instructions](test-rig/client-resources/README.md). -Manual visual and interaction acceptance remains separate. The retained Java 8 -Eclipse compiler script is historical tooling, not a 1.17 verification command. -Keep Eclipse's access-restriction checks enabled and use Java 16 for its project -JRE and Minecraft launches. -The [native Eclipse compiler check](tools/verification/native117/README.md) -compiles fresh production and test outputs, then runs the complete unit-test -inventory on Java 16 without replacing the built mod jars. +Keep Eclipse's access-restriction checks enabled and use Java 17 for its project +JRE and Minecraft launches. Check visuals, menus and recipe discovery against the +same jars used for the automated machine tests. ## Compatibility @@ -109,7 +107,6 @@ data-pack tags. The shared pre-flattening migration hook remains necessary; its presence is not proof that an upgrade is safe. The [legacy migration guide](docs/1.13/MIGRATION.md) describes the earlier protocols. -Use the [native Advantage Works guide](test-rig/NATIVE117.md) for this checkout. -The [1.17 status](docs/1.17/PORT-STATUS.md) tracks this port's qualification. +The [1.18 status](docs/1.18/PORT-STATUS.md) tracks this port's qualification. The historical 1.10 handbook remains on the [1.10.2 branch](https://github.com/MinecraftModDevelopmentMods/PowerAdvantage/blob/master-1.10.2/docs/1.10/README.md). diff --git a/build.gradle b/build.gradle index 7633a2d9..b421ed82 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,7 @@ ext.release_tag = project.mod_version java { toolchain { - languageVersion = JavaLanguageVersion.of(16) + languageVersion = JavaLanguageVersion.of(17) vendor = JvmVendorSpec.ADOPTIUM } withSourcesJar() @@ -54,11 +54,11 @@ java { tasks.withType(JavaCompile).configureEach { javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(16) + languageVersion = JavaLanguageVersion.of(17) vendor = JvmVendorSpec.ADOPTIUM } - sourceCompatibility = JavaVersion.VERSION_16 - targetCompatibility = JavaVersion.VERSION_16 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 options.encoding = 'UTF-8' options.compilerArgs.addAll(['-Xmaxerrs', '1000']) } @@ -132,7 +132,7 @@ sourceSets.main.resources { def forgeRunModClassesDirectory = layout.buildDirectory.dir('forge-run-mod-classes/main') def prepareForgeRunModClasses = tasks.register('prepareForgeRunModClasses', Sync) { group = 'ide' - description = 'Merges classes and processed resources for Forge 1.17 launches.' + description = 'Merges classes and processed resources for Forge 1.18 launches.' dependsOn tasks.named('classes') from sourceSets.main.output into forgeRunModClassesDirectory @@ -167,11 +167,11 @@ repositories { // The default paths mirror the local MMD checkout layout without tying the // build to a drive letter. CI and other layouts can override either path. def baseMetalsDeobfJar = file(providers.gradleProperty('baseMetalsDeobfJar') - .getOrElse('../../../BaseMetals/BaseMetals 117/BaseMetals/build/libs-dev/' + .getOrElse('../../../BaseMetals/BaseMetals 118/BaseMetals/build/libs-dev/' + "BaseMetals-${project.basemetals_version}-deobf.jar")) def oreSpawnDeobfJar = file(providers.gradleProperty('oreSpawnDeobfJar') - .getOrElse('../../../MinecraftMineralogy/OS117/OreSpawn/build/libs-dev/' - + 'OreSpawn-4.1.0.117011-deobf.jar')) + .getOrElse('../../../MinecraftMineralogy/OS118/OreSpawn/build/libs-dev/' + + "OreSpawn-${project.orespawn_version}-deobf.jar")) dependencies { implementation minecraft.dependency( @@ -333,22 +333,23 @@ tasks.register('verifyReleaseConfiguration') { group = 'verification' description = 'Validates release identity, target metadata, and optional integration policy.' doLast { - if (new JsonSlurper().parse(file('src/main/resources/pack.mcmeta')).pack.pack_format != 7) { - throw new GradleException('Minecraft 1.17.1 resources must use pack format 7') + def pack = new JsonSlurper().parse(file('src/main/resources/pack.mcmeta')).pack + if (pack.pack_format != 9 || pack['forge:resource_pack_format'] != 8 || pack['forge:data_pack_format'] != 9) { + throw new GradleException('Minecraft 1.18.2 requires resource pack format 8 and data pack format 9') } - if (project.mod_version != '2.4.9.117011') { + if (project.mod_version != '2.4.10.118021') { throw new GradleException("Unexpected PowerAdvantage release version: ${project.mod_version}") } - if (project.minecraft_version != '1.17.1' - || project.forge_version != '37.1.1' + if (project.minecraft_version != '1.18.2' + || project.forge_version != '40.3.0' || project.mapping_channel != 'official' - || project.mapping_version != '1.17.1' - || project.mcp_version != '20210706.113038') { + || project.mapping_version != '1.18.2' + || project.mcp_version != '20220404.173914') { throw new GradleException('Unexpected Minecraft, Forge, or mappings target') } if (project.loader_name != 'forge' || project.loader_code != '1' - || project.java_version != '16' - || project.java_toolchain_version != '16.0.2+7' + || project.java_version != '17' + || project.java_toolchain_version != '17.0.1+12' || project.gradle_java_version != '17') { throw new GradleException('Unexpected release dispatcher target metadata') } @@ -360,19 +361,19 @@ tasks.register('verifyReleaseConfiguration') { } if (project.cf_requirements != 'base-metals,mmd-orespawn' || project.cf_optionals != 'additional-loot-tables' - || project.cf_versions != '1.17.1') { + || project.cf_versions != '1.18.2') { throw new GradleException('Unexpected CurseForge dependency or game-version metadata') } - if (project.basemetals_version != '3.1.1.117011' - || project.orespawn_version != '4.1.0.117011' - || project.basemetals_source_commit != '0deeeeecdbbc1fbcdecf910c59477de8efe164d7' - || project.orespawn_source_commit != 'b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf') { + if (project.basemetals_version != '3.1.2.118021' + || project.orespawn_version != '4.1.0.118021' + || project.basemetals_source_commit != '3488ed58403722ac3ff203cff7680dfc16e0fd3f' + || project.orespawn_source_commit != 'd31f52f296739f34a0c13f554abf3e1ac071ae25') { throw new GradleException('Unexpected Base Metals or OreSpawn dependency identity') } String mainSource = file( 'src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java').getText('UTF-8') if (!mainSource.contains('MODID = "basepower"') - || !mainSource.contains('VERSION = "2.4.9.117011"') + || !mainSource.contains('VERSION = "2.4.10.118021"') || !mainSource.contains('@Mod(PowerAdvantage.MODID)')) { throw new GradleException('PowerAdvantage source identity changed') } @@ -560,7 +561,7 @@ tasks.register('verifyReleaseArtifacts') { new ZipFile(mainJar).withCloseable { zip -> List names = zip.entries().collect { it.name } ['META-INF/mods.toml', 'META-INF/accesstransformer.cfg', 'pack.mcmeta', - 'coremods/poweradvantage_116_migration.js', + 'coremods/poweradvantage_118_migration.js', 'zone/moddev/mc/basepower/PowerAdvantage.class', 'cyano/poweradvantage/api/PoweredEntity.class', 'cyano/poweradvantage/api/fluid/FluidNetworkApi.class', @@ -609,8 +610,8 @@ tasks.register('verifyReleaseArtifacts') { } } int major = ((header[6] & 0xff) << 8) | (header[7] & 0xff) - if (major != 60) { - throw new GradleException("${entry.name} uses Java class major ${major}, expected 60") + if (major != 61) { + throw new GradleException("${entry.name} uses Java class major ${major}, expected 61") } } } @@ -1201,9 +1202,9 @@ tasks.register('clientResourceProbeJar', Jar) { // these deobf jars are runtime inputs, never release contents or compile dependencies. def advantageExtensionJars = [ basesteam: file(providers.gradleProperty('steamAdvantageDeobfJar').getOrElse( - '../SteamAdvantage/SteamAdvantage/build/libs-dev/baseSteam-2.2.10.117011-deobf.jar')), + '../SteamAdvantage/SteamAdvantage/build/libs-dev/baseSteam-2.2.11.118021-deobf.jar')), baseelectric: file(providers.gradleProperty('electricAdvantageDeobfJar').getOrElse( - '../ElectricAdvantage/ElectricAdvantage/build/libs-dev/baseElectric-2.2.10.117011-deobf.jar')) + '../ElectricAdvantage/ElectricAdvantage/build/libs-dev/baseElectric-2.2.11.118021-deobf.jar')) ] tasks.register('prepareCombinedEclipseLaunch') { @@ -1213,7 +1214,7 @@ tasks.register('prepareCombinedEclipseLaunch') { dependsOn tasks.named('deobfJar') doLast { File inputProfile = file(providers.gradleProperty('advantageDevelopmentProfile').getOrElse( - 'test-rig/profiles/native117/dev-core.json')) + 'test-rig/profiles/native118/dev-core.json')) def profile = new JsonSlurper().parse(inputProfile) if (profile.powerDeobfSha256) { File powerJar = tasks.named('jar', Jar).get().archiveFile.get().asFile diff --git a/docs/1.18/PORT-STATUS.md b/docs/1.18/PORT-STATUS.md new file mode 100644 index 00000000..6a24ec2d --- /dev/null +++ b/docs/1.18/PORT-STATUS.md @@ -0,0 +1,73 @@ +# Minecraft 1.18.2 Port + +This is a local candidate, not a released build. Keep original saves and test +upgrades on copies. + +Forge 40 registers fluids, blocks and items during their registry events. +Machines use native block-entity saves and update packets. Existing machine +IDs and NBT keys are unchanged. + +Legacy chunk migration restores entities into 1.18's terrain layout while +leaving separate entity files in their native format. The terrain guard +protects the old block heights only, so 1.18 can generate the new underground. +The original numeric registry stays available for chunks that have not yet +been visited after an upgrade. + +Local build, unit tests, dependency and archive audits, and the Eclipse +production classpath check pass. Power, Base Metals and OreSpawn also booted +and reopened a disposable fresh world on a packaged Forge server. +The combined client probe checked the machine screen labels and layouts, +oil rendering and recipe-book grouping. The packaged Works tests exercised +all 21 stations with and without Base Metals and checked them again after +restarting. The mining tests exercised the actual survival drop path with +correct and incorrect tools, Silk Touch, and every wire and scaffold +connection mask. + +## Upgrading Older Worlds + +Use the matching Base Metals 3.1.2 build if Base Metals is installed. +The earlier build rewrote old chunks before basePower could translate +Advantage data. In a saved-world test, a musket kept its ID and enchantment +but lost its wear. The shared migration hook now lets basePower translate +legacy item data and decode machine facings before vanilla flattens the +chunk. Neither mod replaces the other's registry identities. + +The legacy fixtures use the original 1.10.2 Advantage releases and the +released 1.10.2 and 1.12.2 base mods. The upgrade checks compare both live +and saved state, then start the same world in a new server process and run +its existing conveyor, steam crusher and electric furnace without replacing +any blocks. Always keep a backup of a real world before upgrading. + +The player and dimension fixtures also check ownership and inventory. +The strict Nether and End reports record one native difference: a tamed +wolf loaded at 8 health heals to 20. A fresh Forge server with no mods +reproduced that change; the upgrade checks retain the original verdicts. + +The final 30-minute test exercised both a fresh and a migrated world. +Both processed the expected items and fluids in 30 batches, reopened their +saves, and processed fresh inputs again. The read-only replay checks all +33 checkpoints per world and measures tick rate between production batches, +without counting setup time. + +## Running the local checks + +Use Java 17, an installed Forge 40.3.0 dedicated server, and a separate +inventory sealed by `native118.seal`. The Python runners under +`test-rig/migration` take a pinned profile, the seed directory, the +inventory and its SHA-256, the Java executable, a new output path below +`test-rig/build`, and a free port. Run each script with `--help` for its +options: + +- `native118_broad.py` exercises both packaged Works profiles. +- `native118_recipes.py` checks unlocks, the real recipe book and survival progression. +- `native118_machines.py` runs moving drills, mining and oil transport. +- `native118_networks.py` checks partial networks, unloaded pipe bridges and Forge capabilities. +- `native118_legacy_world.py` checks live and serialized state against a protected historical fixture, then runs its existing machines. +- `native118_modern_upgrade.py` checks the same witnesses when stepping from 1.17.1 to 1.18.2. +- `native118_actors.py` checks player inventories, nested items, ownership and entity saves using protected fixtures in the Overworld, Nether and End. +- `native118_respawn.py` checks inventory retention and drops, forced spawns, beds and missing beds through the native server handler, including after restart. +- `native118_wolf_control.py` measures native wolf loading in a fresh Forge world with no mods, separate from the strict upgrade checks. +- `native118_endurance.py` measures repeated inputs and outputs over 30 minutes and checks resumed operation after a full restart. A shorter smoke run is not endurance evidence. +- `audit_native118_endurance.py` replays the saved station checkpoints without reopening the worlds. + +No manual acceptance or publication is claimed. diff --git a/gradle.properties b/gradle.properties index 98fc9cc1..5dc17a52 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,42 +5,42 @@ org.gradle.caching=true org.gradle.parallel=false net.minecraftforge.gradle.merge-source-sets=false -minecraft_version=1.17.1 -minecraft_version_range=[1.17.1] -forge_version=37.1.1 -forge_installer_sha256=31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B -forge_version_range=[37.1.1,38) -loader_version_range=[37,) +minecraft_version=1.18.2 +minecraft_version_range=[1.18.2] +forge_version=40.3.0 +forge_installer_sha256=9434C29790504DCD11CE97CB30DD8891B4B18848982357BC1B5ABFA79FB95103 +forge_version_range=[40.3.0,41) +loader_version_range=[40,) mapping_channel=official -mapping_version=1.17.1 -mcp_version=20210706.113038 +mapping_version=1.18.2 +mcp_version=20220404.173914 loader_name=forge loader_code=1 -java_version=16 -java_toolchain_version=16.0.2+7 -java_setup_version=16.0.2+7 +java_version=17 +java_toolchain_version=17.0.1+12 +java_setup_version=17.0.1+12 gradle_java_version=17 curseforge_project_id=1780140 mod_id=basepower mod_name=basePower mod_license=LGPL-3.0-only -mod_version=2.4.9.117011 +mod_version=2.4.10.118021 mod_group=zone.moddev.mc.basepower mod_authors=DrCyano (aka Cyanobacterium) mod_description=API and shared machines for pressure, electricity, quantum power, fluids, and item transport. cf_requirements=base-metals,mmd-orespawn cf_optionals=additional-loot-tables -cf_versions=1.17.1 +cf_versions=1.18.2 -basemetals_version=3.1.1.117011 -basemetals_source_commit=0deeeeecdbbc1fbcdecf910c59477de8efe164d7 +basemetals_version=3.1.2.118021 +basemetals_source_commit=3488ed58403722ac3ff203cff7680dfc16e0fd3f basemetals_curse_project_id=240967 -basemetals_deobf_sha256=83A8C32C633B56BB7E4405E8B3B24A4DEA32B9119216E5097B884056D679CAD1 +basemetals_deobf_sha256=E4F9E2CDE355275218C79954D3792D000615790C7CBAE5FCEC59636A8C4CA57D -orespawn_version=4.1.0.117011 -orespawn_source_commit=b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf +orespawn_version=4.1.0.118021 +orespawn_source_commit=d31f52f296739f34a0c13f554abf3e1ac071ae25 orespawn_curse_project_id=245586 -orespawn_deobf_sha256=CB7A736314842B110B2E4788FBD5E21D4AE8803C5ED34A49199B034BE572DA39 +orespawn_deobf_sha256=34B25B2116FD91B0050C80393F8356A5CA256E707BF443E4882BE5CFAD1335B6 diff --git a/src/main/java/cyano/poweradvantage/api/PoweredEntity.java b/src/main/java/cyano/poweradvantage/api/PoweredEntity.java index d886d570..1bb5a22d 100644 --- a/src/main/java/cyano/poweradvantage/api/PoweredEntity.java +++ b/src/main/java/cyano/poweradvantage/api/PoweredEntity.java @@ -296,14 +296,18 @@ public void load(final CompoundTag tagRoot) { * the network. */ @Override - public CompoundTag save(final CompoundTag tagRoot) { - super.save(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot) { + super.saveAdditional(tagRoot); int[] data = new int[this.getTypes().length]; for (int i = 0; i < data.length; i++) { data[i] = Float.floatToIntBits(this.getEnergy(this.getTypes()[i])); } tagRoot.putIntArray("Energy", data); - return tagRoot; + } + + /** @deprecated Minecraft now serializes through {@link #saveWithFullMetadata()}. */ + @Deprecated public CompoundTag save(CompoundTag tag) { + return tag.merge(saveWithFullMetadata()); } /** @deprecated Minecraft now calls {@link #load(CompoundTag)}. */ diff --git a/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java b/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java index 600c0574..8e5c81a5 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java @@ -81,12 +81,11 @@ protected List getRequestsForFluid(ConduitType powerType) { * @param tagRoot An NBT tag */ @Override - public CompoundTag save(final CompoundTag tagRoot) { - super.save(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot) { + super.saveAdditional(tagRoot); CompoundTag tankTag = new CompoundTag(); this.getTank().writeToNBT(tankTag); tagRoot.put("Tank", tankTag); - return tagRoot; } /** diff --git a/src/main/java/cyano/poweradvantage/api/fluid/InteractiveFluidBlock.java b/src/main/java/cyano/poweradvantage/api/fluid/InteractiveFluidBlock.java index 0d160ac8..10225245 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/InteractiveFluidBlock.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/InteractiveFluidBlock.java @@ -1,6 +1,7 @@ package cyano.poweradvantage.api.fluid; import java.util.function.BiConsumer; +import java.util.function.Supplier; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.LiquidBlock; @@ -20,6 +21,11 @@ public class InteractiveFluidBlock extends LiquidBlock { public InteractiveFluidBlock(FlowingFluid fluid, Block.Properties properties, boolean flammable, BiConsumer immersionEffect) { + this(() -> fluid, properties, flammable, immersionEffect); + } + + public InteractiveFluidBlock(Supplier fluid, Block.Properties properties, + boolean flammable, BiConsumer immersionEffect) { super(fluid, properties); this.flammable = flammable; this.immersionEffect = immersionEffect; diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java index 0841ea89..076da655 100644 --- a/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java +++ b/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java @@ -182,10 +182,9 @@ public void load(final CompoundTag tagRoot) { } @Override - public CompoundTag save(final CompoundTag tagRoot) { - super .save(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot) { + super.saveAdditional(tagRoot); tagRoot.putDouble("Other", this.getOtherEnergy()); - return tagRoot; } diff --git a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java index 51d7ed96..fbefe2eb 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java +++ b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java @@ -294,8 +294,8 @@ public void load(final CompoundTag tagRoot) { * @param tagRoot The root of the NBT */ @Override - public CompoundTag save(final CompoundTag tagRoot) { - super .save(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot) { + super.saveAdditional(tagRoot); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ final ListTag nbttaglist = new ListTag(); @@ -312,7 +312,6 @@ public CompoundTag save(final CompoundTag tagRoot) { if (this.hasCustomName()) { tagRoot.putString("CustomName", this.customName); } - return tagRoot; } private final ConduitType[] types = {Fluids.fluidConduit_general}; @@ -433,13 +432,12 @@ public void readDataFieldUpdateTag(CompoundTag tag){ */ @Override public ClientboundBlockEntityDataPacket getUpdatePacket(){ - CompoundTag nbtTag = createDataFieldUpdateTag(); - return new ClientboundBlockEntityDataPacket(this.worldPosition, 0, nbtTag); + return ClientboundBlockEntityDataPacket.create(this, tile -> createDataFieldUpdateTag()); } @Override public CompoundTag getUpdateTag() { - return save(new CompoundTag()); + return saveWithFullMetadata(); } @Override diff --git a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java index e66357b8..56e148ae 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java +++ b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java @@ -319,8 +319,8 @@ public void load(final CompoundTag tagRoot) { * @param tagRoot The root of the NBT */ @Override - public CompoundTag save(final CompoundTag tagRoot) { - super .save(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot) { + super.saveAdditional(tagRoot); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ final ListTag nbttaglist = new ListTag(); @@ -337,7 +337,6 @@ public CompoundTag save(final CompoundTag tagRoot) { if (this.hasCustomName()) { tagRoot.putString("CustomName", this.customName); } - return tagRoot; } @@ -431,8 +430,7 @@ public void readDataFieldUpdateTag(CompoundTag tag){ */ @Override public ClientboundBlockEntityDataPacket getUpdatePacket(){ - CompoundTag nbtTag = createDataFieldUpdateTag(); - return new ClientboundBlockEntityDataPacket(this.worldPosition, 0, nbtTag); + return ClientboundBlockEntityDataPacket.create(this, tile -> createDataFieldUpdateTag()); } /** diff --git a/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java b/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java index 167d10a9..329d78fd 100644 --- a/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java +++ b/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java @@ -16,6 +16,7 @@ import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.DistExecutor; +import net.minecraftforge.fml.ModList; import net.minecraftforge.fml.ModLoadingContext; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.config.ModConfig; @@ -24,25 +25,24 @@ import net.minecraftforge.fml.loading.FMLPaths; import zone.moddev.mc.basepower.config.PowerAdvantageConfig; import zone.moddev.mc.basepower.conduitnetwork.ConduitRegistry; -import zone.moddev.mc.basepower.init.Blocks; import zone.moddev.mc.basepower.init.Fluids; import zone.moddev.mc.basepower.init.GUI; import zone.moddev.mc.basepower.init.Fuels; -import zone.moddev.mc.basepower.init.Items; +import zone.moddev.mc.basepower.init.ItemGroups; import zone.moddev.mc.basepower.init.ModSupport; import zone.moddev.mc.basepower.init.Recipes; import zone.moddev.mc.basepower.registry.MachineGUIRegistry; import zone.moddev.mc.basepower.recipe.RecipeConditions; import zone.moddev.mc.basepower.recipe.RecipeDiscovery; import zone.moddev.mc.basepower.migration.AdvantageLegacyMigrations; -import zone.moddev.mc.basepower.migration.LegacyWorldDataHook; +import zone.moddev.mc.basepower.migration.BaseMetalsLegacyMigration; -/** Forge 1.16 entry point for Power Advantage. */ +/** Forge entry point for basePower. */ @Mod(PowerAdvantage.MODID) public final class PowerAdvantage { public static final String MODID = "basepower"; public static final String NAME = "basePower"; - public static final String VERSION = "2.4.9.117011"; + public static final String VERSION = "2.4.10.118021"; public static final Logger LOGGER = LogManager.getLogger(MODID); private static PowerAdvantage instance; @@ -84,11 +84,8 @@ public PowerAdvantage() { FMLJavaModLoadingContext.get().getModEventBus().addListener( zone.moddev.mc.basepower.recipe.MaterialTags::registerPack); + ItemGroups.init(); Fluids.init(); - Blocks.init(); - Items.init(); - Fuels.init(); - GUI.init(); migrateLegacyOreSpawnConfiguration(); migrateLegacyConfiguration(); @@ -141,10 +138,14 @@ private static void migrateLegacyConfiguration() { } private void commonSetup(FMLCommonSetupEvent event) { + Fuels.init(); + GUI.init(); // CraftingHelper's shared condition map is not safe during parallel loading. event.enqueueWork(RecipeConditions::register); + if (ModList.get().isLoaded("basemetals")) { + event.enqueueWork(() -> BaseMetalsLegacyMigration.register()); + } // Forge's shared hook list is not safe to mutate during parallel mod construction. - event.enqueueWork(LegacyWorldDataHook::register); PowerAdvantageConfig.apply(); ModSupport.configureForgeEnergyConversions(); Recipes.initDistillationRecipes(PowerAdvantageConfig.distillationRecipes()); diff --git a/src/main/java/zone/moddev/mc/basepower/client/ClientOilRenderer.java b/src/main/java/zone/moddev/mc/basepower/client/ClientOilRenderer.java index 14785fca..80e2a095 100644 --- a/src/main/java/zone/moddev/mc/basepower/client/ClientOilRenderer.java +++ b/src/main/java/zone/moddev/mc/basepower/client/ClientOilRenderer.java @@ -36,7 +36,7 @@ public static void configureRenderLayers() { } private static void stitch(TextureStitchEvent.Pre event) { - if (!TextureAtlas.LOCATION_BLOCKS.equals(event.getMap().location())) return; + if (!TextureAtlas.LOCATION_BLOCKS.equals(event.getAtlas().location())) return; for (String name : new String[] {"crude_oil_still", "crude_oil_flow", "refined_oil_still", "refined_oil_flow"}) { event.addSprite(new ResourceLocation(PowerAdvantage.MODID, "blocks/" + name)); } diff --git a/src/main/java/zone/moddev/mc/basepower/init/Blocks.java b/src/main/java/zone/moddev/mc/basepower/init/Blocks.java index 42a73f00..8a6ea007 100644 --- a/src/main/java/zone/moddev/mc/basepower/init/Blocks.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Blocks.java @@ -105,10 +105,10 @@ public static synchronized void init() { Block.Properties oilProperties = Block.Properties.of(Material.WATER).noCollission() .strength(100.0F).dynamicShape(); - refined_oil_block = (LiquidBlock) add(new InteractiveFluidBlock(Fluids.refined_oil, + refined_oil_block = (LiquidBlock) add(new InteractiveFluidBlock(() -> Fluids.refined_oil, oilProperties, true, (world, entity) -> entity .addEffect(new MobEffectInstance(MobEffects.CONFUSION, 200))), "refined_oil"); - crude_oil_block = (LiquidBlock) add(new InteractiveFluidBlock(Fluids.crude_oil, + crude_oil_block = (LiquidBlock) add(new InteractiveFluidBlock(() -> Fluids.crude_oil, oilProperties, true, (world, entity) -> entity .addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 200, 2))), "crude_oil"); diff --git a/src/main/java/zone/moddev/mc/basepower/init/Fluids.java b/src/main/java/zone/moddev/mc/basepower/init/Fluids.java index 76f03974..eb6b1d7d 100644 --- a/src/main/java/zone/moddev/mc/basepower/init/Fluids.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Fluids.java @@ -47,7 +47,11 @@ public static synchronized void init() { if (initialized) return; fluidConduit_water = registerNewFluid(net.minecraft.world.level.material.Fluids.WATER); fluidConduit_lava = registerNewFluid(net.minecraft.world.level.material.Fluids.LAVA); + initialized = true; + } + @SubscribeEvent + public static void registerFluids(RegistryEvent.Register event) { flowing_crude_oil = new PowerFluid.Flowing(Kind.CRUDE); crude_oil = new PowerFluid.Source(Kind.CRUDE); flowing_refined_oil = new PowerFluid.Flowing(Kind.REFINED); @@ -61,18 +65,13 @@ public static synchronized void init() { registerNewFluid(flowing_crude_oil); registerNewFluid(refined_oil); registerNewFluid(flowing_refined_oil); - initialized = true; + event.getRegistry().registerAll(crude_oil, flowing_crude_oil, refined_oil, flowing_refined_oil); } private static void register(String name, Fluid fluid) { fluid.setRegistryName(PowerAdvantage.MODID, name); } - @SubscribeEvent - public static void registerFluids(RegistryEvent.Register event) { - event.getRegistry().registerAll(crude_oil, flowing_crude_oil, refined_oil, flowing_refined_oil); - } - public static ConduitType registerNewFluid(Fluid fluid) { ResourceLocation id = fluid.getRegistryName(); ConduitType type = new ConduitType(id == null ? "unknown_fluid" : id.toString()); diff --git a/src/main/java/zone/moddev/mc/basepower/init/Items.java b/src/main/java/zone/moddev/mc/basepower/init/Items.java index 1594f683..29a242f1 100644 --- a/src/main/java/zone/moddev/mc/basepower/init/Items.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Items.java @@ -29,14 +29,14 @@ private Items() {} public static synchronized void init() { if (initialized) return; - Blocks.init(); + ItemGroups.init(); starch = add("starch", new Item(properties())); bioplastic_ingot = add("bioplastic_ingot", new Item(properties())); sprocket = add("sprocket", new Item(properties())); rotator_tool = add("rotator_tool", new RotationTool(properties().stacksTo(1))); - crude_oil_bucket = add("crude_oil_bucket", new BucketItem(Fluids.crude_oil, + crude_oil_bucket = add("crude_oil_bucket", new BucketItem(() -> Fluids.crude_oil, new Item.Properties().tab(ItemGroups.tab_powerAdvantage).stacksTo(1).craftRemainder(net.minecraft.world.item.Items.BUCKET))); - refined_oil_bucket = add("refined_oil_bucket", new BucketItem(Fluids.refined_oil, + refined_oil_bucket = add("refined_oil_bucket", new BucketItem(() -> Fluids.refined_oil, new Item.Properties().tab(ItemGroups.tab_powerAdvantage).stacksTo(1).craftRemainder(net.minecraft.world.item.Items.BUCKET))); initialized = true; } diff --git a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyor.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyor.java index a78a65f7..f74ae2aa 100644 --- a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyor.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyor.java @@ -270,8 +270,8 @@ public void load(final CompoundTag tagRoot) { * @param tagRoot The root of the NBT */ @Override - public CompoundTag save(final CompoundTag tagRoot) { - super .save(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot) { + super.saveAdditional(tagRoot); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ final ListTag nbttaglist = new ListTag(); @@ -288,7 +288,6 @@ public CompoundTag save(final CompoundTag tagRoot) { if (this.hasCustomName()) { tagRoot.putString("CustomName", this.customName); } - return tagRoot; } /** diff --git a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityOreFilter.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityOreFilter.java index 9260e67c..6e0290a0 100644 --- a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityOreFilter.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityOreFilter.java @@ -10,10 +10,8 @@ public class TileEntityOreFilter extends TileEntityConveyorFilter { @Override public boolean matchesFilter(ItemStack item) { if (item == null || item.isEmpty()) return false; - return Tags.Items.INGOTS.contains(item.getItem()) - || Tags.Items.ORES.contains(item.getItem()) - || Tags.Items.DUSTS.contains(item.getItem()) - || Tags.Items.NUGGETS.contains(item.getItem()); + return item.is(Tags.Items.INGOTS) || item.is(Tags.Items.ORES) + || item.is(Tags.Items.DUSTS) || item.is(Tags.Items.NUGGETS); } } diff --git a/src/main/java/zone/moddev/mc/basepower/machines/creative/InfiniteEnergyTileEntity.java b/src/main/java/zone/moddev/mc/basepower/machines/creative/InfiniteEnergyTileEntity.java index 0f24b004..34ad7b2c 100644 --- a/src/main/java/zone/moddev/mc/basepower/machines/creative/InfiniteEnergyTileEntity.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/creative/InfiniteEnergyTileEntity.java @@ -89,10 +89,9 @@ public void powerUpdate() { } @Override - public CompoundTag save(final CompoundTag tagRoot) { - super .save(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot) { + super.saveAdditional(tagRoot); tagRoot.putString("InfType", this.type[0].toString()); - return tagRoot; } @Override diff --git a/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDischargeTileEntity.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDischargeTileEntity.java index c79f6fa4..df99311c 100644 --- a/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDischargeTileEntity.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDischargeTileEntity.java @@ -88,7 +88,7 @@ private static BlockPos findPlacement(Level world, BlockPos initial, Fluid fluid } @Override public void handleUpdateTag(CompoundTag tag) { getTank().readFromNBT(tag); } @Override public ClientboundBlockEntityDataPacket getUpdatePacket() { - return new ClientboundBlockEntityDataPacket(getBlockPos(), 0, getUpdateTag()); + return ClientboundBlockEntityDataPacket.create(this); } @Override public void onDataPacket(Connection net, ClientboundBlockEntityDataPacket packet) { handleUpdateTag(packet.getTag()); diff --git a/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StillTileEntity.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StillTileEntity.java index 11cabd96..3bf67576 100644 --- a/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StillTileEntity.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StillTileEntity.java @@ -247,14 +247,13 @@ public FluidTankInfo[] getTankInfo(Direction face) { * @param tagRoot An NBT tag */ @Override - public CompoundTag save(final CompoundTag tagRoot) { - super .save(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot) { + super.saveAdditional(tagRoot); CompoundTag tankTag2 = new CompoundTag(); inputTank.writeToNBT(tankTag2); tagRoot.put("InputTank", tankTag2); tagRoot.putShort("burnTime", this.burnTime); tagRoot.putShort("totalBurnTime", this.totalBurnTime); - return tagRoot; } /** diff --git a/src/main/java/zone/moddev/mc/basepower/migration/BaseMetalsLegacyMigration.java b/src/main/java/zone/moddev/mc/basepower/migration/BaseMetalsLegacyMigration.java new file mode 100644 index 00000000..8f949a47 --- /dev/null +++ b/src/main/java/zone/moddev/mc/basepower/migration/BaseMetalsLegacyMigration.java @@ -0,0 +1,44 @@ +package zone.moddev.mc.basepower.migration; + +import cyano.poweradvantage.api.migration.LegacyMigrationApi; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.registries.ForgeRegistries; +import zone.moddev.mc.basemetals.api.migration.Legacy112MigrationExtension; + +/** Loaded only when Base Metals is present. */ +public final class BaseMetalsLegacyMigration implements Legacy112MigrationExtension { + private BaseMetalsLegacyMigration() {} + + public static void register() { + Legacy112MigrationExtension.register(new ResourceLocation("basepower", "legacy_world"), + new BaseMetalsLegacyMigration()); + } + + @Override + public boolean prepareData(CompoundTag payload, int dataVersion, Payload kind) { + return LegacyWorldDataHook.prepareExternalPayload(payload, kind == Payload.PLAYER); + } + + @Override + public void beforeChunkFlattening(CompoundTag chunk) { + LegacyWorldDataHook.rememberLegacyChunk(chunk); + LegacyWorldDataHook.prepareLegacyEntities(chunk); + } + + @Override + public void afterPlayerFlattening(CompoundTag preparedSource, CompoundTag result) { + LegacyPlayerDataFixes.preserveNames(preparedSource, result); + } + + @Override + public BlockState decodeBlockState(ResourceLocation legacyId, int metadata) { + if (!LegacyMigrationApi.handlesNamespace(legacyId.getNamespace())) return null; + ResourceLocation target = LegacyMigrationApi.blockTarget(legacyId); + if (!ForgeRegistries.BLOCKS.containsKey(target)) { + throw new IllegalStateException("No replacement for saved Advantage block " + legacyId + " (expected " + target + ")"); + } + return LegacyMigrationApi.decodeBlockState(legacyId, ForgeRegistries.BLOCKS.getValue(target), metadata); + } +} diff --git a/src/main/java/zone/moddev/mc/basepower/migration/LegacyEntityDataFixes.java b/src/main/java/zone/moddev/mc/basepower/migration/LegacyEntityDataFixes.java index 4185d6a6..8b55f6aa 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/LegacyEntityDataFixes.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyEntityDataFixes.java @@ -33,9 +33,10 @@ static void prepare(CompoundTag root) { if (version >= current) return; CompoundTag level = entityOwner(root); if (level == null) return; - if (!level.contains("Entities")) return; - requireCompoundList(level, "Entities"); - ListTag entities = level.getList("Entities", 10); + String key = entityKey(root, level); + if (!level.contains(key)) return; + requireCompoundList(level, key); + ListTag entities = level.getList(key, 10); if (entities.isEmpty()) return; DataFixer fixer = DataFixers.getDataFixer(); Set vanillaIds = fixer.getSchema(DataFixUtils.makeKey(version)) @@ -46,7 +47,7 @@ static void prepare(CompoundTag root) { } // Keep these out of vanilla's mixed-choice traversal and avoid fixing them twice. root.put(STAGED, fixed); - level.put("Entities", new ListTag()); + level.put(key, new ListTag()); } private static CompoundTag upgrade(CompoundTag source, int version, int current, @@ -132,23 +133,31 @@ static void finish(CompoundTag root) { } requireCompoundList(root, STAGED); CompoundTag level = entityOwner(root); - if (level.contains("Entities")) requireCompoundList(level, "Entities"); - if (!level.getList("Entities", 10).isEmpty()) { + String key = entityKey(root, level); + if (level.contains(key)) requireCompoundList(level, key); + if (!level.getList(key, 10).isEmpty()) { throw new IllegalStateException("Entity data appeared during isolated Advantage migration"); } - level.put("Entities", root.getList(STAGED, 10)); + level.put(key, root.getList(STAGED, 10)); root.remove(STAGED); } private static CompoundTag entityOwner(CompoundTag root) { if (root.contains("Level", 10)) return root.getCompound("Level"); - if (!root.contains("Position", 11)) return null; + if (!root.contains("Position")) { + return root.contains("xPos", 99) && root.contains("zPos", 99) ? root : null; + } if (root.getIntArray("Position").length != 2) { throw new IllegalStateException("Saved entity chunk has no containing chunk coordinates"); } return root; } + private static String entityKey(CompoundTag root, CompoundTag owner) { + // Terrain NBT was flattened in 1.18; separate entity region files were not. + return owner == root && !root.contains("Position") ? "entities" : "Entities"; + } + private static void requireCompoundList(CompoundTag owner, String key) { if (!owner.contains(key, 9)) throw new IllegalStateException("Malformed entity list: " + key); ListTag list = (ListTag) owner.get(key); diff --git a/src/main/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes.java b/src/main/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes.java index 0ef7b81f..c3cf1ce3 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes.java @@ -28,7 +28,7 @@ public static CompoundTag update(DataFixer fixer, DataFixTypes type, CompoundTag return result; } - private static void preserveNames(Tag original, Tag result) { + static void preserveNames(Tag original, Tag result) { if (original instanceof CompoundTag && result instanceof CompoundTag) { CompoundTag before = (CompoundTag) original, after = (CompoundTag) result; if (isStack(before)) { diff --git a/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldDataHook.java b/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldDataHook.java index 0b23f1fa..2d729f38 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldDataHook.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldDataHook.java @@ -34,9 +34,6 @@ import net.minecraft.nbt.NbtUtils; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.datafix.fixes.BlockStateData; -import net.minecraft.world.level.storage.LevelStorageSource.LevelStorageAccess; -import net.minecraft.world.level.storage.WorldData; -import net.minecraftforge.fmllegacy.WorldPersistenceHooks; import net.minecraftforge.fml.util.ObfuscationReflectionHelper; import net.minecraftforge.registries.ForgeRegistries; import zone.moddev.mc.basepower.PowerAdvantage; @@ -45,28 +42,20 @@ * Shared Advantage pre-flattening migration hook for Minecraft 1.10 and 1.12 * worlds. Add-ons contribute their identities through {@link LegacyMigrationApi}. */ -public final class LegacyWorldDataHook implements WorldPersistenceHooks.WorldPersistenceHook { - private static final LegacyWorldDataHook INSTANCE = new LegacyWorldDataHook(); +public final class LegacyWorldDataHook { private static final ResourceLocation BLOCK_REGISTRY = new ResourceLocation("minecraft", "blocks"); - private static final Map LEGACY_WORLD_DATA = new ConcurrentHashMap<>(); + private static final Set PREPARED_WORLDS = ConcurrentHashMap.newKeySet(); + private static final String REGISTRY_SIDECAR = "basepower-legacy-registry.dat"; private static final BitSet LEGACY_BLOCK_IDS = new BitSet(); private static final Map LEGACY_ENCHANTMENTS = new LinkedHashMap<>(); - private static final Set LEGACY_CHUNKS = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private static final Map> LEGACY_CHUNKS = new ConcurrentHashMap<>(); private static final Map LEGACY_BLOCK_COUNTS = new ConcurrentHashMap<>(); private static final Map VANILLA_BLOCK_ENTITY_IDS = vanillaBlockEntityIds(); private static final String PRESERVE_CHUNK_MARKER = "AdvantageLegacyPreserveChunk"; private static boolean legacyWorldActive; - private static boolean registered; private LegacyWorldDataHook() {} - public static synchronized void register() { - if (!registered) { - WorldPersistenceHooks.addHook(INSTANCE); - registered = true; - } - } - /** Called by the coremod before Forge reads level.dat. */ public static synchronized void prepareLegacyWorld(File levelDat) { AdvantageLegacyMigrations.registerPowerAdvantage(); @@ -74,12 +63,16 @@ public static synchronized void prepareLegacyWorld(File levelDat) { LEGACY_ENCHANTMENTS.clear(); LEGACY_CHUNKS.clear(); LEGACY_BLOCK_COUNTS.clear(); - removeRetiredProfessionSnapshot(levelDat); + prepareForgeRegistryMetadata(levelDat); File source = legacyRegistrySource(levelDat); try { if (source != null) { try (FileInputStream input = new FileInputStream(source)) { CompoundTag registryArchive = NbtIo.readCompressed(input); + if (!containsLegacyRegistryEntry(registryArchive)) { + throw new IOException("Invalid retained Advantage registry snapshot " + source); + } + preserveLegacyRegistry(levelDat, registryArchive); prepareLegacyData(levelDat.getParentFile(), registryArchive.getCompound("FML")); } } @@ -106,7 +99,7 @@ private static boolean hasAdvantageNamespaceAliases(CompoundTag root) { return false; } - private static void removeRetiredProfessionSnapshot(File levelDat) { + static void prepareForgeRegistryMetadata(File levelDat) { if (!levelDat.isFile()) return; try { CompoundTag root; @@ -165,6 +158,17 @@ static boolean removeRetiredVanillaProfessionRegistry(CompoundTag root) { public static synchronized void prepareLegacyChunk(CompoundTag root) { if (root == null) return; migrateLegacyNbt(root); + rememberLegacyChunk(root); + } + + static synchronized boolean prepareExternalPayload(CompoundTag payload, boolean player) { + CompoundTag before = player ? payload.copy() : null; + int changed = migrateLegacyNbt(payload); + if (player) LegacyEntityDataFixes.preparePlayerAttributes(payload); + return changed > 0 || (player && !before.equals(payload)); + } + + static synchronized void rememberLegacyChunk(CompoundTag root) { if (!root.contains("Level", Tag.TAG_COMPOUND)) return; CompoundTag level = root.getCompound("Level"); remapFlattenedPalette(level); @@ -224,32 +228,28 @@ public static void prepareLegacyEntities(CompoundTag root) { public static CompoundTag finalizeLegacyChunk(CompoundTag root) { LegacyEntityDataFixes.finish(root); LegacyMachineItemDataFixes.finish(root); - if (root != null && root.contains("Level", 10)) { - CompoundTag level = root.getCompound("Level"); + if (root != null) { + CompoundTag level = root.contains("Level", 10) ? root.getCompound("Level") : root; if (level.getBoolean(PRESERVE_CHUNK_MARKER)) { - level.putString("Status", "full"); + // Let vanilla fill newly added underground space without rewriting the old surface. + if (!level.contains("below_zero_retrogen", 10)) level.putString("Status", "full"); level.remove(PRESERVE_CHUNK_MARKER); } } return root; } - public static boolean shouldBlockWorldgenWrite(net.minecraft.core.BlockPos position) { + public static boolean shouldBlockWorldgenWrite(net.minecraft.server.level.WorldGenRegion region, + net.minecraft.core.BlockPos position) { return legacyWorldActive && position != null - && LEGACY_CHUNKS.contains(Long.valueOf(chunkKey(position.getX() >> 4, position.getZ() >> 4))); + && isProtectedChunk(region.getLevel().dimension().location(), position); } - @Override public String getModId() { return "FML"; } - - @Override - public CompoundTag getDataForWriting(LevelStorageAccess handler, WorldData info) { - CompoundTag legacy = LEGACY_WORLD_DATA.get(worldKey(handler.getWorldDir().toFile())); - return legacy == null ? new CompoundTag() : legacy.copy(); - } - - @Override - public void readData(LevelStorageAccess handler, WorldData info, CompoundTag tag) { - prepareLegacyData(handler.getWorldDir().toFile(), tag); + static boolean isProtectedChunk(ResourceLocation dimension, net.minecraft.core.BlockPos position) { + Set chunks = LEGACY_CHUNKS.get(dimension); + // Do not block the native 1.18 underground extension or newly added height. + return position.getY() >= 0 && position.getY() < 256 && chunks != null + && chunks.contains(chunkKey(position.getX() >> 4, position.getZ() >> 4)); } private static synchronized void prepareLegacyData(File worldDirectory, CompoundTag tag) { @@ -258,9 +258,8 @@ private static synchronized void prepareLegacyData(File worldDirectory, Compound if (!registries.contains(BLOCK_REGISTRY.toString(), 10)) return; installLegacyEnchantments(registries); String key = worldKey(worldDirectory); - boolean first = !LEGACY_WORLD_DATA.containsKey(key); + boolean first = PREPARED_WORLDS.add(key); int states = installLegacyBlockStates(registries.getCompound(BLOCK_REGISTRY.toString())); - if (first) LEGACY_WORLD_DATA.put(key, tag.copy()); legacyWorldActive = states > 0; int chunks = indexLegacyChunks(worldDirectory); if (first && states > 0) { @@ -455,7 +454,9 @@ private static int migrateFluidName(CompoundTag compound, String key) { return 1; } - private static File legacyRegistrySource(File levelDat) { + static File legacyRegistrySource(File levelDat) { + File sidecar = new File(levelDat.getParentFile(), REGISTRY_SIDECAR); + if (sidecar.isFile()) return sidecar; for (File candidate : new File[] { levelDat, new File(levelDat.getParentFile(), "level.dat_old") }) { if (!candidate.isFile()) continue; try (FileInputStream input = new FileInputStream(candidate)) { @@ -466,6 +467,21 @@ private static File legacyRegistrySource(File levelDat) { return null; } + static void preserveLegacyRegistry(File levelDat, CompoundTag source) throws IOException { + java.nio.file.Path target = levelDat.toPath().resolveSibling(REGISTRY_SIDECAR); + if (Files.exists(target)) return; + // Forge no longer writes the uppercase FML archive. Unvisited legacy chunks still need it. + CompoundTag archive = new CompoundTag(); + archive.put("FML", source.getCompound("FML").copy()); + java.nio.file.Path temporary = Files.createTempFile(target.getParent(), "basepower-legacy-", ".tmp"); + try { + NbtIo.writeCompressed(archive, temporary.toFile()); + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE); + } finally { + Files.deleteIfExists(temporary); + } + } + private static boolean containsLegacyRegistryEntry(CompoundTag root) { ListTag ids = root.getCompound("FML").getCompound("Registries") .getCompound(BLOCK_REGISTRY.toString()).getList("ids", 10); @@ -497,11 +513,17 @@ public static Map legacyPreparedBlockCounts() { return new LinkedHashMap<>(LEGACY_BLOCK_COUNTS); } - private static int indexLegacyChunks(File worldDirectory) { + static int indexLegacyChunks(File worldDirectory) { LEGACY_CHUNKS.clear(); - File regionDirectory = new File(worldDirectory, "region"); + return indexLegacyChunks(new ResourceLocation("overworld"), new File(worldDirectory, "region")) + + indexLegacyChunks(new ResourceLocation("the_nether"), new File(worldDirectory, "DIM-1/region")) + + indexLegacyChunks(new ResourceLocation("the_end"), new File(worldDirectory, "DIM1/region")); + } + + private static int indexLegacyChunks(ResourceLocation dimension, File regionDirectory) { File[] regions = regionDirectory.listFiles((directory, name) -> name.matches("r\\.-?\\d+\\.-?\\d+\\.mc[ar]")); if (regions == null) return 0; + Set chunks = ConcurrentHashMap.newKeySet(); byte[] locations = new byte[4096]; for (File region : regions) { String[] parts = region.getName().split("\\."); @@ -518,7 +540,7 @@ private static int indexLegacyChunks(File worldDirectory) { for (int index = 0; index < read / 4; index++) { int offset = index * 4; if ((locations[offset] | locations[offset + 1] | locations[offset + 2] | locations[offset + 3]) != 0) { - LEGACY_CHUNKS.add(Long.valueOf(chunkKey( + chunks.add(Long.valueOf(chunkKey( regionX * 32 + (index & 31), regionZ * 32 + (index >> 5)))); } } @@ -526,7 +548,8 @@ private static int indexLegacyChunks(File worldDirectory) { PowerAdvantage.LOGGER.warn("Could not inspect legacy chunk locations in '{}'", region, exception); } } - return LEGACY_CHUNKS.size(); + LEGACY_CHUNKS.put(dimension, chunks); + return chunks.size(); } @SuppressWarnings("unchecked") diff --git a/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldMetadata.java b/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldMetadata.java index ecca8515..7d174957 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldMetadata.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldMetadata.java @@ -20,11 +20,17 @@ public final class LegacyWorldMetadata { private LegacyWorldMetadata() {} public static CompoundTag read(File levelDat) throws IOException { + LegacyWorldDataHook.prepareForgeRegistryMetadata(levelDat); CompoundTag root = NbtIo.readCompressed(levelDat); remap(root, ModList.get()::isLoaded); return root; } + public static Tag remapLightweight(Tag root) { + if (root instanceof CompoundTag compound) remap(compound, ModList.get()::isLoaded); + return root; + } + static void remap(CompoundTag root, Predicate loaded) { ListTag mods = root.getCompound("fml").getList("LoadingModList", Tag.TAG_COMPOUND); Set recordedMods = new HashSet<>(); diff --git a/src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java b/src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java index eafecda3..099256e7 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java @@ -13,6 +13,7 @@ import net.minecraft.world.item.crafting.RecipeSerializer; import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.datafix.fixes.CavesAndCliffsRenames; import net.minecraft.world.level.biome.Biome; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.RegistryEvent; @@ -23,6 +24,8 @@ /** Vanilla's data fixers rename saved data, but not Forge's registry snapshot. */ public final class VanillaRegistryRemaps { + private static final ResourceLocation BIOME_REGISTRY = new ResourceLocation("minecraft:worldgen/biome"); + private VanillaRegistryRemaps() {} public static void register() { @@ -96,6 +99,15 @@ static > void remap(RegistryEvent.MissingMappin PowerAdvantage.LOGGER.info("Retired non-serialized vanilla AI registry index {}", mapping.key); continue; } + ResourceLocation updatedBiome = vanillaBiomeFixTarget(event.getName(), mapping.key); + if (updatedBiome != null && active.containsKey(updatedBiome)) { + // Vanilla fixes biome names in chunks and world settings. Several + // old entries share one target; do not assign it multiple numeric IDs. + mapping.ignore(); + PowerAdvantage.LOGGER.info("Retired vanilla biome registry index {} (data fixer: {})", + mapping.key, updatedBiome); + continue; + } ResourceLocation replacement = replacement(event.getName(), mapping.key); if (replacement == null) continue; // The event's registry is STAGING: new entries need not be present there yet. @@ -114,6 +126,14 @@ static boolean retiredTransientMemory(ResourceLocation registry, ResourceLocatio && (old.getPath().equals("golem_last_seen_time") || old.getPath().equals("opened_doors")); } + static ResourceLocation vanillaBiomeFixTarget(ResourceLocation registry, ResourceLocation old) { + if (!BIOME_REGISTRY.equals(registry) || !"minecraft".equals(old.getNamespace())) return null; + String name = old.toString(); + String updated = name.equals("minecraft:deep_warm_ocean") ? "minecraft:warm_ocean" + : CavesAndCliffsRenames.RENAMES.get(name); + return updated == null ? null : new ResourceLocation(updated); + } + static ResourceLocation replacement(ResourceLocation registry, ResourceLocation old) { if (!"minecraft".equals(old.getNamespace())) return null; String target = null; diff --git a/src/main/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPack.java b/src/main/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPack.java index 87bf29bb..1f61425e 100644 --- a/src/main/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPack.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPack.java @@ -87,7 +87,7 @@ private void put(String path, JsonArray values) { @Override @SuppressWarnings("unchecked") public T getMetadataSection(MetadataSectionSerializer serializer) { return serializer == PackMetadataSection.SERIALIZER - ? (T)new PackMetadataSection(new TextComponent(NAME), 7) : null; + ? (T)new PackMetadataSection(new TextComponent(NAME), 9) : null; } @Override public String getName() { return NAME; } @Override public void close() {} diff --git a/src/main/java/zone/moddev/mc/basepower/recipe/MaterialTags.java b/src/main/java/zone/moddev/mc/basepower/recipe/MaterialTags.java index 721cfbc7..2ee5736e 100644 --- a/src/main/java/zone/moddev/mc/basepower/recipe/MaterialTags.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/MaterialTags.java @@ -1,19 +1,5 @@ package zone.moddev.mc.basepower.recipe; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; -import net.minecraft.world.item.Item; -import net.minecraft.tags.Tag; -import net.minecraft.tags.TagCollection; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.core.Registry; -import net.minecraftforge.fml.ModList; -import zone.moddev.mc.basepower.PowerAdvantage; -import zone.moddev.mc.basepower.RecipeMode; -import zone.moddev.mc.basepower.init.Items; - /** Config-dependent aliases resolved by the server's native tag reload. */ public final class MaterialTags { private MaterialTags() {} @@ -29,30 +15,4 @@ public static void registerPack(net.minecraftforge.event.AddPackFindersEvent eve }); } - static TagCollection applyPolicy(TagCollection tags, Item plastic, - boolean rubber, boolean solder) { - if (!rubber && !solder) return tags; - Map> result = new LinkedHashMap<>(tags.getAllTags()); - if (rubber) { - append(result, "forge:rubber", java.util.Collections.singleton(plastic)); - append(result, "forge:ingots/rubber", java.util.Collections.singleton(plastic)); - } - if (solder) { - for (String metal : new String[] {"lead", "tin", "silver"}) { - append(result, "forge:solder", tags.getTagOrEmpty( - new ResourceLocation("forge", "ingots/" + metal)).getValues()); - } - } - return TagCollection.of(result); - } - - private static void append(Map> tags, String name, - java.util.Collection additions) { - ResourceLocation id = new ResourceLocation(name); - Set values = new LinkedHashSet<>(); - Tag existing = tags.get(id); - if (existing != null) values.addAll(existing.getValues()); - for (Item item : additions) if (item != null) values.add(item); - tags.put(id, Tag.fromSet(values)); - } } diff --git a/src/main/java/zone/moddev/mc/basepower/recipe/RecipeConditions.java b/src/main/java/zone/moddev/mc/basepower/recipe/RecipeConditions.java index 515af461..3f2e654e 100644 --- a/src/main/java/zone/moddev/mc/basepower/recipe/RecipeConditions.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/RecipeConditions.java @@ -36,12 +36,14 @@ private Condition(ResourceLocation id, String value) { @Override public ResourceLocation getID() { return id; } - @Override public boolean test() { + @Override public boolean test() { return test(IContext.EMPTY); } + + @Override public boolean test(IContext context) { switch (id.getPath()) { case "recipe_mode": return PowerAdvantage.recipeMode == RecipeMode.valueOf(value.toUpperCase(Locale.ROOT)); case "not_recipe_mode": return PowerAdvantage.recipeMode != RecipeMode.valueOf(value.toUpperCase(Locale.ROOT)); - case "tag_empty": return net.minecraft.tags.SerializationTags.getInstance().getOrEmpty(net.minecraft.core.Registry.ITEM_REGISTRY).getTagOrEmpty(new ResourceLocation(value)).getValues().isEmpty(); - case "tag_not_empty": return !net.minecraft.tags.SerializationTags.getInstance().getOrEmpty(net.minecraft.core.Registry.ITEM_REGISTRY).getTagOrEmpty(new ResourceLocation(value)).getValues().isEmpty(); + case "tag_empty": return context.getTag(ItemTags.create(new ResourceLocation(value))).getValues().isEmpty(); + case "tag_not_empty": return !context.getTag(ItemTags.create(new ResourceLocation(value))).getValues().isEmpty(); default: throw new IllegalStateException("Unknown Advantage condition " + id); } } diff --git a/src/main/java/zone/moddev/mc/basepower/registry/MachineGUIRegistry.java b/src/main/java/zone/moddev/mc/basepower/registry/MachineGUIRegistry.java index 7843c737..139745dd 100644 --- a/src/main/java/zone/moddev/mc/basepower/registry/MachineGUIRegistry.java +++ b/src/main/java/zone/moddev/mc/basepower/registry/MachineGUIRegistry.java @@ -19,11 +19,11 @@ import net.minecraft.world.level.Level; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; -import net.minecraftforge.common.extensions.IForgeContainerType; +import net.minecraftforge.common.extensions.IForgeMenuType; import net.minecraftforge.event.RegistryEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fmllegacy.network.NetworkHooks; +import net.minecraftforge.network.NetworkHooks; import zone.moddev.mc.basepower.PowerAdvantage; import java.util.Map; @@ -52,7 +52,7 @@ public static int addGUI(ITileEntityGUI gui) { @SubscribeEvent public static void registerTypes(RegistryEvent.Register> event) { - TYPE = IForgeContainerType.create(MachineGUIRegistry::createClientContainer); + TYPE = IForgeMenuType.create(MachineGUIRegistry::createClientContainer); TYPE.setRegistryName(PowerAdvantage.MODID, "machine"); event.getRegistry().register(TYPE); } diff --git a/src/main/resources/META-INF/coremods.json b/src/main/resources/META-INF/coremods.json index a432056b..92fa3573 100644 --- a/src/main/resources/META-INF/coremods.json +++ b/src/main/resources/META-INF/coremods.json @@ -1,3 +1,3 @@ { - "poweradvantage_116_migration": "coremods/poweradvantage_116_migration.js" + "poweradvantage_118_migration": "coremods/poweradvantage_118_migration.js" } diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index d8b89c00..7e15753b 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -14,14 +14,14 @@ description='''API and shared machines for pressure, electricity, quantum power, [[dependencies.basepower]] modId="basemetals" mandatory=false -versionRange="[3.1.1.117011,4.0.0)" +versionRange="[3.1.2.118021,4.0.0)" ordering="AFTER" side="BOTH" [[dependencies.basepower]] modId="orespawn" mandatory=true -versionRange="[4.1.0.117011,5.0.0)" +versionRange="[4.1.0.118021,5.0.0)" ordering="AFTER" side="BOTH" diff --git a/src/main/resources/coremods/poweradvantage_116_migration.js b/src/main/resources/coremods/poweradvantage_118_migration.js similarity index 80% rename from src/main/resources/coremods/poweradvantage_116_migration.js rename to src/main/resources/coremods/poweradvantage_118_migration.js index a0c0b935..c0ed94b0 100644 --- a/src/main/resources/coremods/poweradvantage_116_migration.js +++ b/src/main/resources/coremods/poweradvantage_118_migration.js @@ -5,9 +5,11 @@ var JumpInsnNode = Java.type('org.objectweb.asm.tree.JumpInsnNode'); var LabelNode = Java.type('org.objectweb.asm.tree.LabelNode'); var MethodInsnNode = Java.type('org.objectweb.asm.tree.MethodInsnNode'); var VarInsnNode = Java.type('org.objectweb.asm.tree.VarInsnNode'); +var ASMAPI = Java.type('net.minecraftforge.coremod.api.ASMAPI'); -// Only pre-datafix migration needs bytecode integration on Forge 1.17. +// Only pre-datafix migration needs bytecode integration. var hook = 'zone/moddev/mc/basepower/migration/LegacyWorldDataHook'; +var metadata = 'zone/moddev/mc/basepower/migration/LegacyWorldMetadata'; function patchPlayerDataFix(classNode) { var descriptor = '(Lcom/mojang/datafixers/DataFixer;' + @@ -55,7 +57,7 @@ function initializeCoreMod() { var method = classNode.methods.get(i); var worldReader = method.desc === '(Lcom/mojang/serialization/DynamicOps;' + 'Lnet/minecraft/world/level/DataPackConfig;' + - 'Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;' + + 'Lcom/mojang/serialization/Lifecycle;' + 'Ljava/io/File;Lcom/mojang/datafixers/DataFixer;)' + 'Lnet/minecraft/world/level/storage/PrimaryLevelData;'; var packReader = method.desc === '(Ljava/io/File;Lcom/mojang/datafixers/DataFixer;)' + @@ -67,21 +69,20 @@ function initializeCoreMod() { prefix.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, 'prepareLegacyWorld', '(Ljava/io/File;)V', false)); method.instructions.insert(prefix); + ++worldReaders; + continue; } var reads = 0; for (var read = method.instructions.getFirst(); read !== null; read = read.getNext()) { - if (read.getOpcode() === Opcodes.INVOKESTATIC && read.owner === 'net/minecraft/nbt/NbtIo' && - read.desc === '(Ljava/io/File;)Lnet/minecraft/nbt/CompoundTag;') { - var replacement = new MethodInsnNode(Opcodes.INVOKESTATIC, - 'zone/moddev/mc/basepower/migration/LegacyWorldMetadata', 'read', read.desc, false); - method.instructions.set(read, replacement); - read = replacement; + if (read.getOpcode() === Opcodes.INVOKESTATIC && read.owner === classNode.name && + read.desc === '(Ljava/io/File;)Lnet/minecraft/nbt/Tag;') { + method.instructions.insert(read, new MethodInsnNode(Opcodes.INVOKESTATIC, + metadata, 'remapLightweight', '(Lnet/minecraft/nbt/Tag;)Lnet/minecraft/nbt/Tag;', false)); ++reads; } } - if (reads !== 1) throw new Error('Power Advantage expected one level metadata read'); - if (worldReader) ++worldReaders; - if (packReader) ++packReaders; + if (reads !== 1) throw new Error('Power Advantage expected one lightweight metadata read'); + ++packReaders; } if (worldReaders !== 1 || packReaders !== 1) { throw new Error('Power Advantage could not attach both legacy level readers'); @@ -89,11 +90,36 @@ function initializeCoreMod() { return classNode; } }, + 'poweradvantage_forge_save_metadata': { + 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.level.storage.LevelStorageSource$LevelStorageAccess' }, + 'transformer': function(classNode) { + var matches = 0; + for (var i = 0; i < classNode.methods.size(); ++i) { + var method = classNode.methods.get(i); + var read = null; + for (var call = method.instructions.getFirst(); call !== null; call = call.getNext()) { + if (call.getOpcode() !== Opcodes.INVOKESTATIC) continue; + if (call.owner === 'net/minecraft/nbt/NbtIo' && + call.desc === '(Ljava/io/File;)Lnet/minecraft/nbt/CompoundTag;') read = call; + if (call.owner === 'net/minecraftforge/common/ForgeHooks' && + call.name === 'readAdditionalLevelSaveData' && + call.desc === '(Lnet/minecraft/nbt/CompoundTag;Ljava/nio/file/Path;)V') { + if (read === null) throw new Error('Power Advantage expected Forge to read level.dat'); + method.instructions.set(read, new MethodInsnNode(Opcodes.INVOKESTATIC, + metadata, 'read', read.desc, false)); + ++matches; + } + } + } + if (matches !== 1) throw new Error('Power Advantage could not attach the Forge save metadata reader'); + return classNode; + } + }, 'poweradvantage_legacy_chunk_status': { 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.level.chunk.storage.ChunkStorage' }, 'transformer': function(classNode) { var descriptor = '(Lnet/minecraft/resources/ResourceKey;' + - 'Ljava/util/function/Supplier;Lnet/minecraft/nbt/CompoundTag;)' + + 'Ljava/util/function/Supplier;Lnet/minecraft/nbt/CompoundTag;Ljava/util/Optional;)' + 'Lnet/minecraft/nbt/CompoundTag;'; var patchedRead = false; var patchedReturn = false; @@ -178,17 +204,17 @@ function initializeCoreMod() { 'poweradvantage_legacy_worldgen_guard': { 'target': { 'type': 'CLASS', 'name': 'net.minecraft.server.level.WorldGenRegion' }, 'transformer': function(classNode) { - var descriptor = '(Lnet/minecraft/core/BlockPos;' + - 'Lnet/minecraft/world/level/block/state/BlockState;II)Z'; + var descriptor = '(Lnet/minecraft/core/BlockPos;)Z'; var patched = false; for (var i = 0; i < classNode.methods.size(); ++i) { var method = classNode.methods.get(i); - if (method.desc !== descriptor) continue; + if (method.desc !== descriptor || method.name !== ASMAPI.mapMethod('m_180807_')) continue; var allowed = new LabelNode(); var prefix = new InsnList(); + prefix.add(new VarInsnNode(Opcodes.ALOAD, 0)); prefix.add(new VarInsnNode(Opcodes.ALOAD, 1)); prefix.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, - 'shouldBlockWorldgenWrite', '(Lnet/minecraft/core/BlockPos;)Z', false)); + 'shouldBlockWorldgenWrite', '(Lnet/minecraft/server/level/WorldGenRegion;Lnet/minecraft/core/BlockPos;)Z', false)); prefix.add(new JumpInsnNode(Opcodes.IFEQ, allowed)); prefix.add(new InsnNode(Opcodes.ICONST_0)); prefix.add(new InsnNode(Opcodes.IRETURN)); diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index 99861c4b..91c42b11 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,6 +1,8 @@ { "pack": { "description": "Power Advantage resources", - "pack_format": 7 + "pack_format": 9, + "forge:resource_pack_format": 8, + "forge:data_pack_format": 9 } } diff --git a/src/test/java/cyano/poweradvantage/api/fluid/FluidNetworkApiTest.java b/src/test/java/cyano/poweradvantage/api/fluid/FluidNetworkApiTest.java index 038420cb..dee4a425 100644 --- a/src/test/java/cyano/poweradvantage/api/fluid/FluidNetworkApiTest.java +++ b/src/test/java/cyano/poweradvantage/api/fluid/FluidNetworkApiTest.java @@ -10,7 +10,6 @@ import cyano.poweradvantage.api.IPowerMachine; import cyano.poweradvantage.api.PowerConnectorContext; import cyano.poweradvantage.api.PowerRequest; -import net.minecraft.world.level.material.EmptyFluid; import net.minecraft.world.level.material.Fluid; public class FluidNetworkApiTest { @@ -19,7 +18,7 @@ public class FluidNetworkApiTest { private static Fluid testFluid() { net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); - return new EmptyFluid(); + return net.minecraft.world.level.material.Fluids.WATER; } @Test diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntitiesTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntitiesTest.java index d7344fc5..01b07117 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntitiesTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntitiesTest.java @@ -13,6 +13,10 @@ import static org.junit.Assert.*; public class LegacyEntitiesTest { + @org.junit.BeforeClass public static void detectVersion() { + SharedConstants.tryDetectVersion(); + } + @Test public void retainsWolfInDirect110Chunk() { assertWolf(false); } @@ -36,7 +40,7 @@ public class LegacyEntitiesTest { vehicle.put("Passengers", passengers); CompoundTag root = chunk(vehicle, 512); root = convert(root, 512); - CompoundTag actual = root.getCompound("Level").getList("Entities", 10).getCompound(0); + CompoundTag actual = root.getList("entities", 10).getCompound(0); assertEquals("example:vehicle", actual.getString("id")); assertEquals("keep-verbatim", actual.getString("CustomState")); ListTag fixed = actual.getList("Passengers", 10); @@ -52,7 +56,7 @@ public class LegacyEntitiesTest { skeleton.putByte("SkeletonType", (byte) 1); CompoundTag root = chunk(skeleton, 512); root = convert(root, 512); - assertEquals("minecraft:wither_skeleton", root.getCompound("Level").getList("Entities", 10).getCompound(0).getString("id")); + assertEquals("minecraft:wither_skeleton", root.getList("entities", 10).getCompound(0).getString("id")); } @Test public void leavesModernEntityAndUnrelatedCustomNbtUnchangedOnRepeatedPreparation() { @@ -85,6 +89,8 @@ private static CompoundTag chunk(CompoundTag entity, int version) { CompoundTag root = new CompoundTag(); root.putInt("DataVersion", version); CompoundTag level = new CompoundTag(); + level.putInt("xPos", 0); + level.putInt("zPos", 0); ListTag entities = new ListTag(); entities.add(entity); level.put("Entities", entities); @@ -96,6 +102,8 @@ private static void assertWolf(boolean customEntity) { CompoundTag root = new CompoundTag(); root.putInt("DataVersion", 512); CompoundTag level = new CompoundTag(); + level.putInt("xPos", 0); + level.putInt("zPos", 0); ListTag entities = new ListTag(); CompoundTag wolf = new CompoundTag(); wolf.putString("id", "Wolf"); @@ -113,12 +121,12 @@ private static void assertWolf(boolean customEntity) { level.put("Entities", entities); root.put("Level", level); CompoundTag fixed = convert(root, 512); - CompoundTag actual = fixed.getCompound("Level").getList("Entities", 10).getCompound(0); + CompoundTag actual = fixed.getList("entities", 10).getCompound(0); assertEquals("minecraft:wolf", actual.getString("id")); assertEquals(UUID.fromString(wolf.getString("OwnerUUID")), actual.getUUID("Owner")); assertTrue(actual.getBoolean("Sitting")); assertEquals(new UUID(123L, 456L), actual.getUUID("UUID")); - assertEquals(customEntity ? 2 : 1, fixed.getCompound("Level").getList("Entities", 10).size()); + assertEquals(customEntity ? 2 : 1, fixed.getList("entities", 10).size()); } @Test public void preservesCustomEntityUuidAndPrivateNbtThroughWholeChunkFix() { @@ -131,7 +139,7 @@ private static void assertWolf(boolean customEntity) { custom.put("PrivateData", privateData); for (int version : new int[] {512, 1343, 1631, 1976, 2230}) { CompoundTag converted = convert(chunk(custom.copy(), version), version); - CompoundTag actual = converted.getCompound("Level").getList("Entities", 10).getCompound(0); + CompoundTag actual = converted.getList("entities", 10).getCompound(0); assertEquals(new UUID(-9014291490481488382L, -5032249493117728499L), actual.getUUID("UUID")); assertEquals(privateData, actual.getCompound("PrivateData")); assertFalse(converted.contains("AdvantageLegacyEntityDataFix")); @@ -151,7 +159,7 @@ private static void assertWolf(boolean customEntity) { CompoundTag root = chunk(wolf, version); root.getCompound("Level").getList("Entities", 10).add(entity("example:turbine")); CompoundTag converted = convert(root, version); - CompoundTag actual = converted.getCompound("Level").getList("Entities", 10).getCompound(0); + CompoundTag actual = converted.getList("entities", 10).getCompound(0); assertEquals(uuid, actual.getUUID("UUID")); assertEquals(owner, actual.getUUID("Owner")); assertEquals(8F, actual.getFloat("Health"), 0F); @@ -174,15 +182,15 @@ private static CompoundTag convert(CompoundTag root, int version) { CompoundTag custom = entity("example:vehicle"); custom.putLong("UUIDMost", uuid.getMostSignificantBits()); custom.putLong("UUIDLeast", uuid.getLeastSignificantBits()); - assertEquals(uuid, convert(chunk(custom, 512), 512).getCompound("Level") - .getList("Entities", 10).getCompound(0).getUUID("UUID")); + assertEquals(uuid, convert(chunk(custom, 512), 512) + .getList("entities", 10).getCompound(0).getUUID("UUID")); } CompoundTag custom = entity("example:vehicle"); custom.putUUID("UUID", new UUID(17L, 19L)); custom.putLong("UUIDMost", 88L); custom.putLong("UUIDLeast", 99L); - assertEquals(new UUID(17L, 19L), convert(chunk(custom, 2230), 2230).getCompound("Level") - .getList("Entities", 10).getCompound(0).getUUID("UUID")); + assertEquals(new UUID(17L, 19L), convert(chunk(custom, 2230), 2230) + .getList("entities", 10).getCompound(0).getUUID("UUID")); } @Test public void baseMetalsDurabilitySurvivesBothEntryHookOrders() throws Exception { @@ -205,7 +213,7 @@ private static CompoundTag convert(CompoundTag root, int version) { root = NbtUtils.update(DataFixers.getDataFixer(), DataFixTypes.CHUNK, root, 1343, SharedConstants.getCurrentVersion().getWorldVersion()); root = LegacyWorldDataHook.finalizeLegacyChunk(root); - CompoundTag actual = root.getCompound("Level").getList("Entities", 10).getCompound(0) + CompoundTag actual = root.getList("entities", 10).getCompound(0) .getList("Items", 10).getCompound(0); assertEquals("basemetals:steel_pickaxe", actual.getString("id")); assertEquals(1, actual.getInt("Count")); diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java index 8fe79ba9..dfbe67c0 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java @@ -78,7 +78,7 @@ public class LegacyMachineItemsTest { tile.put("Recipe", nested); tile.putFloat("Energy", 19.5F); tile.putString("CustomName", "keep"); - CompoundTag actual = convert(chunk(tile), 512).getCompound("Level").getList("TileEntities", 10).getCompound(0); + CompoundTag actual = convert(chunk(tile), 512).getList("block_entities", 10).getCompound(0); assertEquals("minecraft:lapis_lazuli", actual.getCompound("Recipe").getCompound("Reference").getString("id")); assertEquals(19.5F, actual.getFloat("Energy"), 0F); assertEquals("keep", actual.getString("CustomName")); @@ -165,7 +165,7 @@ public class LegacyMachineItemsTest { book.put("tag", tag); CompoundTag root = chunk(tile(carrier(book))); root.putInt("DataVersion", 1343); - CompoundTag actual = convert(root, 1343).getCompound("Level").getList("TileEntities", 10) + CompoundTag actual = convert(root, 1343).getList("block_entities", 10) .getCompound(0).getList("Items", 10).getCompound(0).getCompound("tag") .getCompound("BlockEntityTag").getList("Items", 10).getCompound(0); assertEquals(enchantments, actual.getCompound("tag").getList("StoredEnchantments", 10)); @@ -313,7 +313,7 @@ private static CompoundTag carrier(CompoundTag item) { } private static CompoundTag convertedTile(CompoundTag item) { - return convert(chunk(tile(item)), 512).getCompound("Level").getList("TileEntities", 10).getCompound(0); + return convert(chunk(tile(item)), 512).getList("block_entities", 10).getCompound(0); } private static CompoundTag convert(CompoundTag root, int version) { diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java index 05e3e645..5a9ba745 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java @@ -171,8 +171,7 @@ private static ClassNode nativeClass(String name) throws Exception { } private static Object embeddedInfo(Class type, CompoundTag source, int version) throws Exception { - // The real private constructor only assigns fields. The getter needs its - // fixer/version/player arguments, not a world, generator, or live server. + // The native constructor also uses LevelSettings to attach the lifecycle. for (Constructor constructor : type.getDeclaredConstructors()) { if (!Modifier.isPrivate(constructor.getModifiers())) continue; Class[] types = constructor.getParameterTypes(); @@ -182,6 +181,12 @@ private static Object embeddedInfo(Class type, CompoundTag source, int versio if (types[i] == boolean.class) args[i] = false; if (types[i] == long.class) args[i] = 0L; if (types[i] == float.class) args[i] = 0F; + if (types[i] == com.mojang.serialization.Lifecycle.class) args[i] = com.mojang.serialization.Lifecycle.stable(); + if (types[i] == net.minecraft.world.level.LevelSettings.class) { + args[i] = new net.minecraft.world.level.LevelSettings("Test", net.minecraft.world.level.GameType.SURVIVAL, + false, net.minecraft.world.Difficulty.NORMAL, false, new net.minecraft.world.level.GameRules(), + net.minecraft.world.level.DataPackConfig.DEFAULT); + } } args[0] = DataFixers.getDataFixer(); args[1] = version; @@ -200,7 +205,7 @@ private static void rejected(ClassNode node, String target) throws Exception { private static void transform(ClassNode node, String target) throws Exception { ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); assertNotNull("Use the Java 8 test toolchain", engine); - try (InputStreamReader reader = new InputStreamReader(resource("/coremods/poweradvantage_116_migration.js"), StandardCharsets.UTF_8)) { + try (InputStreamReader reader = new InputStreamReader(resource("/coremods/poweradvantage_118_migration.js"), StandardCharsets.UTF_8)) { engine.eval(reader); } engine.put("targetClass", node); diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyRegistryArchiveTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyRegistryArchiveTest.java new file mode 100644 index 00000000..8e28764e --- /dev/null +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyRegistryArchiveTest.java @@ -0,0 +1,81 @@ +package zone.moddev.mc.basepower.migration; + +import java.nio.file.Files; +import java.nio.file.Path; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.resources.ResourceLocation; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import static org.junit.Assert.*; + +public class LegacyRegistryArchiveTest { + @Rule public TemporaryFolder temporary = new TemporaryFolder(); + + @Test public void restartRetainsOriginalRegistryAfterForgeReplacesBothLevelFiles() throws Exception { + AdvantageLegacyMigrations.registerPowerAdvantage(); + Path world = temporary.newFolder().toPath(); + Path level = world.resolve("level.dat"); + CompoundTag source = registrySnapshot(); + NbtIo.writeCompressed(source, level.toFile()); + assertEquals(level.toFile(), LegacyWorldDataHook.legacyRegistrySource(level.toFile())); + byte[] before = Files.readAllBytes(level); + LegacyWorldDataHook.preserveLegacyRegistry(level.toFile(), source); + assertArrayEquals("Archiving must not change level.dat", before, Files.readAllBytes(level)); + + CompoundTag nativeSave = new CompoundTag(); + nativeSave.put("fml", new CompoundTag()); + NbtIo.writeCompressed(nativeSave, level.toFile()); + NbtIo.writeCompressed(nativeSave, world.resolve("level.dat_old").toFile()); + var retained = LegacyWorldDataHook.legacyRegistrySource(level.toFile()); + assertNotNull(retained); + assertNotEquals(level.toFile(), retained); + var archive = NbtIo.readCompressed(retained); + assertEquals(source.getCompound("FML"), archive.getCompound("FML")); + assertFalse(archive.contains("Data")); + LegacyWorldDataHook.preserveLegacyRegistry(level.toFile(), nativeSave); + assertEquals(archive, NbtIo.readCompressed(retained)); + } + + @Test public void terrainGuardProtectsOnlySavedDimensionsAndOldHeights() throws Exception { + Path world = temporary.newFolder().toPath(); + Path region = Files.createDirectories(world.resolve("DIM-1/region")).resolve("r.0.0.mca"); + byte[] header = new byte[4096]; + header[2] = 2; + header[3] = 1; + Files.write(region, header); + try { + assertEquals(1, LegacyWorldDataHook.indexLegacyChunks(world.toFile())); + var nether = new ResourceLocation("the_nether"); + assertTrue(LegacyWorldDataHook.isProtectedChunk(nether, new BlockPos(0, 0, 0))); + assertTrue(LegacyWorldDataHook.isProtectedChunk(nether, new BlockPos(0, 255, 0))); + assertFalse(LegacyWorldDataHook.isProtectedChunk(nether, new BlockPos(0, -1, 0))); + assertFalse(LegacyWorldDataHook.isProtectedChunk(nether, new BlockPos(0, 256, 0))); + assertFalse(LegacyWorldDataHook.isProtectedChunk(nether, new BlockPos(16, 64, 0))); + assertFalse(LegacyWorldDataHook.isProtectedChunk(new ResourceLocation("overworld"), new BlockPos(0, 64, 0))); + } finally { + LegacyWorldDataHook.indexLegacyChunks(temporary.newFolder()); + } + } + + private static CompoundTag registrySnapshot() { + CompoundTag source = new CompoundTag(); + CompoundTag fml = new CompoundTag(); + CompoundTag registries = new CompoundTag(); + CompoundTag blocks = new CompoundTag(); + CompoundTag entry = new CompoundTag(); + entry.putString("K", "poweradvantage:item_conveyor"); + entry.putInt("V", 1420); + ListTag ids = new ListTag(); + ids.add(entry); + blocks.put("ids", ids); + registries.put("minecraft:blocks", blocks); + fml.put("Registries", registries); + source.put("FML", fml); + source.put("Data", new CompoundTag()); + return source; + } +} diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyTransformerOrderTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyTransformerOrderTest.java index 945cf784..145739ca 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyTransformerOrderTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyTransformerOrderTest.java @@ -31,8 +31,11 @@ private static void check(boolean baseFirst, boolean productionNames) throws Exc for (String[] target : targets) { ClassNode node = new ClassNode(); try (InputStream input = getResource("/" + target[0] + ".class")) { new ClassReader(input).accept(node, 0); } - if (productionNames) for (MethodNode method : node.methods) - if (!method.name.startsWith("<")) method.name = "test_srg_" + method.name; + for (MethodNode method : node.methods) { + // Plain JUnit has no Forge name-mapping service for this SRG target. + if (method.name.equals("ensureCanWrite")) method.name = "m_180807_"; + else if (productionNames && !method.name.startsWith("<")) method.name = "test_srg_" + method.name; + } transform(node, target[1], baseFirst); transform(node, target[1], !baseFirst); assertEquals(target[0], 1, calls(node, "basepower", target[2])); @@ -71,7 +74,7 @@ private static void assertEntityPreparationOrder(ClassNode node) { private static void transform(ClassNode node, String target, boolean base) throws Exception { String mod = base ? "basemetals" : "poweradvantage"; - String path = base ? "/coremods/basemetals_117_compatibility.js" : "/coremods/poweradvantage_116_migration.js"; + String path = base ? "/coremods/basemetals_118_compatibility.js" : "/coremods/poweradvantage_118_migration.js"; ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); assertNotNull("Transformer tests require Forge's bundled Nashorn engine", engine); try (InputStreamReader reader = new InputStreamReader(getResource(path), StandardCharsets.UTF_8)) { engine.eval(reader); } diff --git a/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemapsTest.java b/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemapsTest.java index 90991ef3..c2dfdc79 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemapsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemapsTest.java @@ -1,6 +1,11 @@ package zone.moddev.mc.basepower.migration; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.JsonOps; +import net.minecraft.SharedConstants; import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.fixes.References; import org.junit.Test; import static org.junit.Assert.*; @@ -36,6 +41,30 @@ public class VanillaRegistryRemapsTest { expect("worldgen/biome", "nether", "nether_wastes"); } + @Test public void onlyRetiresBiomeIndexesHandledByTheNativeDataFixer() { + ResourceLocation biomes = id("worldgen/biome"); + assertEquals(id("desert"), VanillaRegistryRemaps.vanillaBiomeFixTarget(biomes, id("desert_hills"))); + assertEquals(id("desert"), VanillaRegistryRemaps.vanillaBiomeFixTarget(biomes, id("desert_lakes"))); + assertEquals(id("warm_ocean"), VanillaRegistryRemaps.vanillaBiomeFixTarget(biomes, id("deep_warm_ocean"))); + assertNull(VanillaRegistryRemaps.vanillaBiomeFixTarget(biomes, id("plains"))); + assertNull(VanillaRegistryRemaps.vanillaBiomeFixTarget(biomes, id("unknown_biome"))); + assertNull(VanillaRegistryRemaps.vanillaBiomeFixTarget(biomes, new ResourceLocation("example:desert_hills"))); + assertNull(VanillaRegistryRemaps.vanillaBiomeFixTarget(id("block"), id("desert_hills"))); + } + + @Test public void vanillaDataFixerActuallyConvertsTheMergedBiomeNames() { + SharedConstants.tryDetectVersion(); + assertBiomeFix("desert_hills", "desert"); + assertBiomeFix("desert_lakes", "desert"); + assertBiomeFix("deep_warm_ocean", "warm_ocean"); + } + + private static void assertBiomeFix(String from, String to) { + var input = new Dynamic<>(JsonOps.INSTANCE, JsonOps.INSTANCE.createString(id(from).toString())); + var fixed = DataFixers.getDataFixer().update(References.BIOME, input, 2730, 2975); + assertEquals(id(to).toString(), fixed.asString().result().orElseThrow()); + } + @Test public void leavesOtherModsAndUnrecognizedVanillaEntriesAlone() { assertNull(VanillaRegistryRemaps.replacement(id("item"), new ResourceLocation("othermod:zombie_pigman_spawn_egg"))); assertNull(VanillaRegistryRemaps.replacement(id("item"), id("unknown_item"))); diff --git a/src/test/java/zone/moddev/mc/basepower/recipe/MaterialTagsTest.java b/src/test/java/zone/moddev/mc/basepower/recipe/MaterialTagsTest.java index 767638bb..afab5c5f 100644 --- a/src/test/java/zone/moddev/mc/basepower/recipe/MaterialTagsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/recipe/MaterialTagsTest.java @@ -1,13 +1,14 @@ package zone.moddev.mc.basepower.recipe; -import java.util.LinkedHashMap; +import com.google.gson.JsonParser; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.Map; -import java.util.Optional; import net.minecraft.world.item.Item; import net.minecraft.world.item.Items; import net.minecraft.tags.Tag; -import net.minecraft.tags.TagCollection; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.*; @@ -15,44 +16,44 @@ public class MaterialTagsTest { @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); } - @Test public void updatesNativeImmutableCollectionWithoutLosingUserEntries() { - TagCollection tags = fixture(); - try { - tags.getAllTags().put(new ResourceLocation("test", "must_be_immutable"), null); - fail("Fixture must reproduce native immutable tag maps"); - } catch (UnsupportedOperationException expected) { } - TagCollection original = tags; - tags = MaterialTags.applyPolicy(tags, Items.APPLE, true, true); - assertFalse(tag(original, "forge:rubber").contains(Items.APPLE)); - assertTrue(tag(tags, "forge:rubber").contains(Items.STICK)); - assertTrue(tag(tags, "forge:rubber").contains(Items.APPLE)); - assertTrue(tag(tags, "forge:ingots/rubber").contains(Items.APPLE)); - assertTrue(tag(tags, "forge:solder").contains(Items.IRON_INGOT)); - assertTrue(tag(tags, "forge:solder").contains(Items.GOLD_INGOT)); - assertTrue(tag(tags, "test:unrelated").contains(Items.REDSTONE)); - tags = MaterialTags.applyPolicy(tags, Items.APPLE, true, true); - assertEquals(2, tag(tags, "forge:rubber").getValues().size()); - assertEquals(2, tag(tags, "forge:solder").getValues().size()); + @Test public void addsNativeAliasesWithoutReplacingUserEntries() throws Exception { + MaterialPolicyPack pack = new MaterialPolicyPack(true, true); + Tag rubber = resolve(pack, "rubber", "[\"minecraft:stick\",\"basepower:bioplastic_ingot\"]"); + assertTrue(rubber.getValues().contains(Items.STICK)); + assertTrue(rubber.getValues().contains(Items.APPLE)); + assertEquals(2, rubber.getValues().size()); + assertTrue(resolve(pack, "ingots/rubber", "[]").getValues().contains(Items.APPLE)); + Tag solder = resolve(pack, "solder", "[\"minecraft:redstone\"]"); + assertTrue(solder.getValues().contains(Items.IRON_INGOT)); + assertTrue(solder.getValues().contains(Items.GOLD_INGOT)); + assertTrue(solder.getValues().contains(Items.REDSTONE)); + assertEquals(3, solder.getValues().size()); } - @Test public void disabledOptionsDoNotAddAliases() { - TagCollection tags = fixture(); - assertSame(tags, MaterialTags.applyPolicy(tags, Items.APPLE, false, false)); - assertFalse(tag(tags, "forge:rubber").contains(Items.APPLE)); - assertTrue(tag(tags, "forge:solder").getValues().isEmpty()); - assertTrue(tag(tags, "test:unrelated").contains(Items.REDSTONE)); + @Test public void disabledOptionsDoNotAddAliases() throws Exception { + MaterialPolicyPack pack = new MaterialPolicyPack(false, false); + Tag rubber = resolve(pack, "rubber", "[\"minecraft:stick\"]"); + assertTrue(rubber.getValues().contains(Items.STICK)); + assertFalse(rubber.getValues().contains(Items.APPLE)); + assertTrue(resolve(pack, "solder", "[]").getValues().isEmpty()); } - private static Tag tag(TagCollection tags, String name) { return tags.getTagOrEmpty(new ResourceLocation(name)); } - - private static TagCollection fixture() { - Map> tags = new LinkedHashMap<>(); - tags.put(new ResourceLocation("forge:rubber"), singleton(Items.STICK)); - tags.put(new ResourceLocation("forge:ingots/lead"), singleton(Items.IRON_INGOT)); - tags.put(new ResourceLocation("forge:ingots/tin"), singleton(Items.GOLD_INGOT)); - tags.put(new ResourceLocation("test:unrelated"), singleton(Items.REDSTONE)); - return TagCollection.of(tags); + private static Tag resolve(MaterialPolicyPack pack, String path, String userEntries) throws Exception { + var resource = new ResourceLocation("forge", "tags/items/" + path + ".json"); + var parser = new JsonParser(); + var builder = Tag.Builder.tag(); + try (var reader = new InputStreamReader(pack.getResource(PackType.SERVER_DATA, resource), + StandardCharsets.UTF_8)) { + var json = parser.parse(reader).getAsJsonObject(); + assertFalse(json.get("replace").getAsBoolean()); + builder.addFromJson(json, pack.getName()); + } + builder.addFromJson(parser.parse("{\"values\":" + userEntries + "}").getAsJsonObject(), "user"); + Map items = Map.of("minecraft:stick", Items.STICK, "minecraft:redstone", Items.REDSTONE, + "basepower:bioplastic_ingot", Items.APPLE); + Map> tags = Map.of( + "forge:ingots/lead", new Tag<>(java.util.List.of(Items.IRON_INGOT)), + "forge:ingots/tin", new Tag<>(java.util.List.of(Items.GOLD_INGOT))); + return builder.build(id -> tags.get(id.toString()), id -> items.get(id.toString())).right().get(); } - - private static Tag singleton(Item item) { return Tag.fromSet(java.util.Collections.singleton(item)); } } diff --git a/src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java index 87a359b8..75e7e323 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java @@ -6,6 +6,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Properties; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -27,20 +28,24 @@ public class BuildWorkflowContractTest { } @Test public void pinnedDependencyCheckoutPolicyPrecedesCheckout() throws Exception { + Properties pins = new Properties(); + try (var input = Files.newBufferedReader(Paths.get("gradle.properties"), StandardCharsets.UTF_8)) { + pins.load(input); + } for (String name : new String[] {"ci.yml", "codeql-analysis.yml"}) { String workflow = new String(Files.readAllBytes(Paths.get(".github/workflows", name)), StandardCharsets.UTF_8); int policy = workflow.indexOf("run: git config --global core.autocrlf false"); int checkout = workflow.indexOf("- name: Check out pinned Base Metals"); assertTrue(name, policy >= 0 && checkout > policy); - assertTrue(name, workflow.contains("ref: 0deeeeecdbbc1fbcdecf910c59477de8efe164d7")); - assertTrue(name, workflow.contains("BaseMetals-3.1.1.117011-deobf.jar")); - assertTrue(name, workflow.contains("ref: b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf")); - assertTrue(name, workflow.contains("master-1.17.1")); + assertTrue(name, workflow.contains("ref: " + pins.getProperty("basemetals_source_commit"))); + assertTrue(name, workflow.contains("BaseMetals-" + pins.getProperty("basemetals_version") + "-deobf.jar")); + assertTrue(name, workflow.contains("ref: d31f52f296739f34a0c13f554abf3e1ac071ae25")); + assertTrue(name, workflow.contains("master-1.18.2")); assertTrue(name, workflow.contains("java-version: '25.0.3+9.0.LTS'")); assertTrue(name, workflow.contains("java-version: '17.0.1+12'")); assertTrue(name, workflow.contains("java-version: '8.0.502+7'")); - assertTrue(name, workflow.contains("java-version: '16.0.2+7'")); + assertFalse(name, workflow.contains("JAVA_HOME_16_X64")); assertTrue(name, workflow.contains("-Porg.gradle.java.installations.paths=")); assertTrue(name, workflow.contains("-Porg.gradle.java.installations.auto-detect=false")); assertTrue(name, workflow.contains("-Porg.gradle.java.installations.auto-download=false")); diff --git a/src/test/java/zone/moddev/mc/basepower/resources/NativeBlockEntityFactoryTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativeBlockEntityFactoryTest.java index 2f4dffde..866937f1 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/NativeBlockEntityFactoryTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativeBlockEntityFactoryTest.java @@ -22,6 +22,7 @@ public class NativeBlockEntityFactoryTest { @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); + net.minecraftforge.registries.GameData.unfreezeData(); } @Test public void nativeFactoriesKeepTheActualPositionAndFacing() { diff --git a/src/test/java/zone/moddev/mc/basepower/resources/NativeMachinePropertyTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativeMachinePropertyTest.java index 941ffe0e..dedc7981 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/NativeMachinePropertyTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativeMachinePropertyTest.java @@ -16,7 +16,12 @@ import static org.junit.Assert.*; public class NativeMachinePropertyTest { - @BeforeClass public static void bootstrapNativeRegistries() { Bootstrap.bootStrap(); } + @BeforeClass public static void bootstrapNativeRegistries() { + net.minecraft.SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + // Plain JUnit does not open the registry event that allows block construction. + net.minecraftforge.registries.GameData.unfreezeData(); + } @Test public void materialConstructorsKeepTheLegacyNonSolidDefault() { assertFalse(new MaterialMachine().defaultBlockState().canOcclude()); diff --git a/src/test/java/zone/moddev/mc/basepower/resources/NativePlatformContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativePlatformContractTest.java index e5ac92f8..48aaa1ce 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/NativePlatformContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativePlatformContractTest.java @@ -20,21 +20,28 @@ public class NativePlatformContractTest { String json = new String(Files.readAllBytes(Paths.get("src/main/resources/META-INF/coremods.json")), StandardCharsets.UTF_8); JsonObject coremods = new JsonParser().parse(json).getAsJsonObject(); assertEquals(1, coremods.size()); - assertEquals("coremods/poweradvantage_116_migration.js", coremods.get("poweradvantage_116_migration").getAsString()); + assertEquals("coremods/poweradvantage_118_migration.js", coremods.get("poweradvantage_118_migration").getAsString()); for (String removed : new String[] {"oil_renderer", "material_tags", "parts_loot"}) assertFalse(Files.exists(Paths.get("src/main/resources/coremods/poweradvantage_" + removed + ".js"))); } @Test public void pinnedForgeFiresTagEventAfterBindingNativeCollections() throws Exception { - ClassNode node = read("net/minecraft/tags/TagContainer"); + ClassNode node = read("net/minecraft/server/ReloadableServerResources"); + MethodNode bind = node.methods.stream().filter(method -> method.name.equals("updateRegistryTags") + && method.desc.contains("TagManager$LoadResult")) + .findFirst().orElseThrow(); + assertTrue(java.util.Arrays.stream(bind.instructions.toArray()).anyMatch(instruction -> + instruction instanceof MethodInsnNode call && call.owner.equals("net/minecraft/core/Registry") + && call.name.equals("bindTags"))); int events = 0; for (MethodNode method : node.methods) { boolean installed = false; for (AbstractInsnNode instruction : method.instructions.toArray()) { if (instruction instanceof MethodInsnNode) { MethodInsnNode call = (MethodInsnNode) instruction; - if (call.owner.equals("net/minecraft/tags/StaticTags") - && call.name.equals("resetAll")) installed = true; + if (call.owner.equals("java/lang/Iterable") || call.owner.equals("java/util/List")) { + if (call.name.equals("forEach")) installed = true; + } } if (instruction instanceof TypeInsnNode && ((TypeInsnNode) instruction).desc.equals("net/minecraftforge/event/TagsUpdatedEvent")) { diff --git a/src/test/java/zone/moddev/mc/basepower/resources/PersistenceRegistrationThreadTest.java b/src/test/java/zone/moddev/mc/basepower/resources/PersistenceRegistrationThreadTest.java deleted file mode 100644 index b32e9f5a..00000000 --- a/src/test/java/zone/moddev/mc/basepower/resources/PersistenceRegistrationThreadTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package zone.moddev.mc.basepower.resources; - -import java.io.InputStream; -import org.junit.Test; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.Handle; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.InvokeDynamicInsnNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import static org.junit.Assert.*; - -public class PersistenceRegistrationThreadTest { - @Test public void sharedForgeHookListIsOnlyUpdatedThroughQueuedSetup() throws Exception { - ClassNode node = new ClassNode(); - try (InputStream input = getClass().getResourceAsStream("/zone/moddev/mc/basepower/PowerAdvantage.class")) { - assertNotNull(input); - new ClassReader(input).accept(node, 0); - } - String hook = "zone/moddev/mc/basepower/migration/LegacyWorldDataHook"; - int queuedRegistrations = 0; - for (MethodNode method : node.methods) { - boolean hookRunnable = false; - for (AbstractInsnNode instruction : method.instructions.toArray()) { - if (instruction instanceof MethodInsnNode) { - MethodInsnNode call = (MethodInsnNode) instruction; - assertFalse("Do not mutate Forge's ArrayList during parallel mod construction", - call.owner.equals(hook) && call.name.equals("register")); - if (call.owner.equals("net/minecraftforge/fml/event/lifecycle/FMLCommonSetupEvent") && call.name.equals("enqueueWork") && hookRunnable) { - assertEquals("Queue the hook during common setup", "commonSetup", method.name); - queuedRegistrations++; - hookRunnable = false; - } - } - if (instruction instanceof InvokeDynamicInsnNode) { - for (Object argument : ((InvokeDynamicInsnNode) instruction).bsmArgs) { - if (argument instanceof Handle && ((Handle) argument).getOwner().equals(hook) - && ((Handle) argument).getName().equals("register")) hookRunnable = true; - } - } - } - } - assertEquals("Register the shared persistence hook once, after parallel lifecycle work", 1, queuedRegistrations); - } -} diff --git a/test-rig/README.md b/test-rig/README.md index 786969f7..3af98db2 100644 --- a/test-rig/README.md +++ b/test-rig/README.md @@ -1,11 +1,24 @@ -# Advantage Works 1.16.5 Native Test Rig +# Advantage Works 1.18.2 Test Rig -The native target is Minecraft 1.16.5 / Forge 36.2.34, official mappings +Use Java 17, Forge 40.3.0 and the exact jars in +[`profiles/native118/`](profiles/native118/). These profiles exercise +the 21 Works stations in a disposable world. The current profile uses +the local Base Metals 3.1.2 build for old-world upgrades; the decoupled +profile exercises the same machines without Base Metals. + +Start with the [1.18.2 port and test guide](../docs/1.18/PORT-STATUS.md). +It lists the runners for migration, recipe progression, real machine +operation, network unloading, mining, oil, and the 30-minute test. +The automated results do not replace the manual gameplay checklist. + +## Retained 1.16.5 Reference + +The earlier target is Minecraft 1.16.5 / Forge 36.2.34, official mappings 1.16.5, MCP 20210115.111550, Java 8 bytecode and harness `1.1.0.116051`. Read [the native116 runner and qualification limits](NATIVE116.md) before using this checkout. Build success is not runtime or migration acceptance. -Default destination profiles live only in `profiles/native116/`. The original +The 1.16.5 destination profiles live only in `profiles/native116/`. The original profiles, station specification and legacy migration source adapters remain unchanged. New profiles explicitly reference the historical PA113 source root; the native runner refuses to launch an old profile as a native116 destination. diff --git a/test-rig/build.gradle b/test-rig/build.gradle index 53cb7e69..b782592f 100644 --- a/test-rig/build.gradle +++ b/test-rig/build.gradle @@ -23,15 +23,15 @@ version = project.harness_version base.archivesName = 'AdvantageWorksTestHarness' java { - toolchain.languageVersion = JavaLanguageVersion.of(16) + toolchain.languageVersion = JavaLanguageVersion.of(17) } tasks.withType(JavaCompile).configureEach { javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(16) + languageVersion = JavaLanguageVersion.of(17) } - sourceCompatibility = JavaVersion.VERSION_16 - targetCompatibility = JavaVersion.VERSION_16 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 options.encoding = 'UTF-8' } @@ -66,7 +66,7 @@ repositories { } def powerCompileJar = file(project.findProperty('powerAdvantageDeobfJar') - ?: '../build/libs-dev/basePower-2.4.9.117011-deobf.jar') + ?: '../build/libs-dev/basePower-2.4.10.118021-deobf.jar') dependencies { implementation minecraft.dependency( @@ -101,14 +101,14 @@ tasks.register('verifyWorksCompileInputPin') { inputs.property('sha256', project.poweradvantage_deobf_sha256) inputs.property('sourceCommit', project.poweradvantage_source_commit) doLast { - if (!powerCompileJar.isFile()) throw new GradleException("Missing native117 Power compile input: ${powerCompileJar}") + if (!powerCompileJar.isFile()) throw new GradleException("Missing 1.18.2 Power compile input: ${powerCompileJar}") String actual = java.security.MessageDigest.getInstance('SHA-256') .digest(powerCompileJar.bytes).encodeHex().toString().toUpperCase(Locale.ROOT) if (actual != project.poweradvantage_deobf_sha256) { - throw new GradleException("Native117 Power compile input changed: ${actual}; obtain a new parent pin") + throw new GradleException("Power compile input changed: ${actual}; obtain a new parent pin") } - if (project.poweradvantage_source_commit != '308d42a89483d56d0fad4805cdb65dac483d9e31') { - throw new GradleException('Native117 Power source pin changed; obtain a new parent candidate') + if (project.poweradvantage_source_commit != 'bab9971cc2d593eee1f4a3de6dafc7ec268d25c2') { + throw new GradleException('The harness requires the verified 1.18.2 Power source pin') } } } @@ -174,7 +174,7 @@ def registerWorksRunner = { String taskName, String profileName, boolean automat classpath = sourceSets.runner.runtimeClasspath mainClass = 'zone.moddev.mc.advantageworks.runner.WorksRunner' if (!automated) standardInput = System.in - args '--profile', file("profiles/native117/${profileName}.json").absolutePath + args '--profile', file("profiles/native118/${profileName}.json").absolutePath if (automated) args '--automated' if (project.hasProperty('worksStations')) { args '--stations', project.property('worksStations').toString() @@ -189,7 +189,7 @@ def registerWorksRunner = { String taskName, String profileName, boolean automat } registerWorksRunner('runWorksDev', 'dev-core', false, - 'Builds and launches a disposable interactive 1.17.1 Advantage Works development server.') + 'Builds and launches a disposable interactive 1.18.2 Advantage Works development server.') registerWorksRunner('verifyWorksDev', 'dev-core', true, 'Generates, checks, restarts, and rechecks the disposable development works.') registerWorksRunner('verifyWorksTechProgression', 'dev-core-tech-progression', true, @@ -213,13 +213,13 @@ registerWorksRunner('verifyWorksWorldgenLegacyImport', 'worldgen-legacy-import', registerWorksRunner('runWorksPackagedCurrent', 'packaged-current', false, 'Launches the complete packaged works on localhost for manual inspection.') -tasks.register('verifyWorksNative117Inputs', JavaExec) { +tasks.register('verifyWorksNative118Inputs', JavaExec) { group = 'verification' - description = 'Checks native117 candidate identities and hashes without creating or opening a world.' + description = 'Checks 1.18.2 candidate identities and hashes without creating or opening a world.' dependsOn tasks.named('runnerClasses'), tasks.named('jar'), releaseJar classpath = sourceSets.runner.runtimeClasspath mainClass = 'zone.moddev.mc.advantageworks.runner.WorksRunner' - args '--profile', file("profiles/native117/${project.findProperty('worksProfile') ?: 'packaged-current'}.json").absolutePath, + args '--profile', file("profiles/native118/${project.findProperty('worksProfile') ?: 'packaged-current'}.json").absolutePath, '--validate-only' } @@ -228,7 +228,7 @@ tasks.register('verifyHarnessIsolation') { description = 'Confirms the PowerAdvantage release contains no test-harness content.' doLast { File powerJar = file(project.findProperty('powerAdvantageReleaseJar') - ?: '../build/libs/basePower-2.4.9.117011.jar') + ?: '../build/libs/basePower-2.4.10.118021.jar') if (!powerJar.isFile()) { throw new GradleException("Build PowerAdvantage first: ${powerJar}") } @@ -260,7 +260,7 @@ tasks.register('verifyNativeWolfPlatform', JavaExec) { dependsOn compilePlatformControls, unitBootstrapJar classpath = files(compilePlatformControls.flatMap { it.destinationDirectory }) + nativeControlClasspath mainClass = 'NativeWolfPlatformControl' - javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(16) } + javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) } doFirst { def output = providers.gradleProperty('nativeControlOutput').orNull if (!output || file(output).exists()) throw new GradleException('Supply a new -PnativeControlOutput directory') @@ -274,7 +274,7 @@ tasks.register('verifyNativeLeashKnotPlatform', JavaExec) { dependsOn compilePlatformControls, unitBootstrapJar classpath = files(compilePlatformControls.flatMap { it.destinationDirectory }) + nativeControlClasspath mainClass = 'NativeLeashKnotPlatformControl' - javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(16) } + javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) } doFirst { def output = providers.gradleProperty('nativeControlOutput').orNull if (!output || file(output).exists()) throw new GradleException('Supply a new -PnativeControlOutput directory') diff --git a/test-rig/client-resources/README.md b/test-rig/client-resources/README.md index 4b8de01c..e6be7673 100644 --- a/test-rig/client-resources/README.md +++ b/test-rig/client-resources/README.md @@ -1,27 +1,26 @@ # Native Client Resource Check -This disposable Minecraft 1.17.1 / Forge 37.1.1 client checks models, textures, +This disposable Minecraft 1.18.2 / Forge 40.3.0 client checks models, textures, translations, recipe-book cells and machine screens after Minecraft loads them. It registers no gameplay content, opens no world and closes when finished. The probe is excluded from release jars and ordinary Eclipse launches. -See [port status](../../docs/1.17/PORT-STATUS.md) for current qualification. +See [port status](../../docs/1.18/PORT-STATUS.md) for current qualification. Earlier-version results do not qualify this version. -Use Java 17 for Gradle and the configured Java 16 compilation toolchain. Supply -Java 16 to the client and server runners. The target is official 1.17.1 mappings -with MCP `20210706.113038`. Keep the shared Gradle cache free of other builds. +Use Java 17 for Gradle, compilation and the client and server runners. The +target is official 1.18.2 mappings with MCP `20220404.173914`. ## Build And Run ```powershell ./gradlew.bat check clientResourceProbeJar verifyEclipseProductionClasspath prepareCombinedEclipseLaunch ./test-rig/client-resources/test-launcher.ps1 -./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -MachineGuiLayouts -ChildRendererGeometry -RecipeBookFixtures '/unlock' -PreflightOnly -./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -MachineGuiLayouts -ChildRendererGeometry -RecipeBookFixtures '/unlock' +./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -MachineGuiLayouts -ChildRendererGeometry -RecipeBookFixtures '/unlock' -PreflightOnly +./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -MachineGuiLayouts -ChildRendererGeometry -RecipeBookFixtures '/unlock' ``` -The combined launch requires Power `2.4.8.117011`, Steam `2.2.9.117011` and -Electric `2.2.9.117011`. Pin the exact rebuilt jars in `profiles/native117` +The combined launch requires Power `2.4.10.118021`, Steam `2.2.11.118021` and +Electric `2.2.11.118021`. Pin the exact rebuilt jars in `profiles/native118` before regenerating it. Omitting the combined profile retains Power-only scope. Normal Eclipse launches contain production code only. @@ -40,10 +39,10 @@ is provenance, not proof that the selected loose output equals that jar. ## Recipe Fixtures -Generate fresh exports from the packaged 1.17 candidates, not a historical run: +Generate fresh exports from the packaged 1.18.2 candidates, not a historical run: ```powershell - -B test-rig/migration/native117_recipes.py --target-manifest '' --java16 '/bin/java.exe' --port 25577 --output test-rig/build/native117-recipes-new + -B test-rig/migration/native118_recipes.py --profile test-rig/profiles/native118/packaged-current.json --seed '' --inventory '' --inventory-sha256 '' --java17 '/bin/java.exe' --port 25577 --output test-rig/build/native118-recipes-new ``` Use an existing Python 3.11+ environment with `migration/requirements.txt`. @@ -80,16 +79,11 @@ this is explicitly `NOT_RUN_NO_NATIVE_FIXTURES`. buffers. Hydroturbine registration and texture are checked; its live orientation and spin still require a disposable world. -The probes preserve their earlier pixel, packet and missing-model assertions -through explicit native API translations. Run the static preservation and -negative controls without launching Minecraft: +The live probes retain the pixel, packet and missing-model checks. Run the +current launcher's parser and negative controls without starting Minecraft: ```powershell ./test-rig/client-resources/test-launcher.ps1 -StaticOnly -./test-rig/client-resources/test-launcher.ps1 -StaticOnly -CrossVersionEvidenceRoots -./test-rig/client-resources/test-probe-contract.ps1 -./test-rig/client-resources/test-geometry-contract.ps1 -./test-rig/client-resources/test-probe-contract-negative-controls.ps1 ``` These checks do not certify machine operation, live animated entities, diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ClientResourceProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ClientResourceProbe.java index b9a5a361..74813bc7 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ClientResourceProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ClientResourceProbe.java @@ -45,8 +45,8 @@ public final class ClientResourceProbe { try { result.addProperty("minecraft", net.minecraftforge.versions.mcp.MCPVersion.getMCVersion()); result.addProperty("forge", net.minecraftforge.versions.forge.ForgeVersion.getVersion()); - require("1.17.1".equals(result.get("minecraft").getAsString()) - && "37.1.1".equals(result.get("forge").getAsString()), "Wrong native client target"); + require("1.18.2".equals(result.get("minecraft").getAsString()) + && "40.3.0".equals(result.get("forge").getAsString()), "Wrong native client target"); require(minecraft.level == null && minecraft.player == null && !minecraft.hasSingleplayerServer(), "Resource probe must not open a world or connect to a server"); verifyNetworkBootstrap(result); @@ -65,9 +65,9 @@ public final class ClientResourceProbe { result.add("mod_versions", versions); require(versions.has("basepower") && zone.moddev.mc.basepower.PowerAdvantage.VERSION .equals(versions.get("basepower").getAsString()), "Power metadata does not match the compiled version"); - require("2.4.9.117011".equals(versions.get("basepower").getAsString()), "Wrong native Power candidate"); + require("2.4.10.118021".equals(versions.get("basepower").getAsString()), "Wrong native Power candidate"); for (String id : new String[]{"basesteam", "baseelectric"}) { - if (versions.has(id)) require("2.2.10.117011".equals(versions.get(id).getAsString()), "Wrong native expansion candidate: " + id); + if (versions.has(id)) require("2.2.11.118021".equals(versions.get(id).getAsString()), "Wrong native expansion candidate: " + id); } require(minecraft.getModelManager().getMissingModel() != null, "Timed out before model baking"); require(minecraft.getOverlay() == null, "Timed out before native resource loading completed"); diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/MachineGuiLayoutProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/MachineGuiLayoutProbe.java index 577261cf..84ebff65 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/MachineGuiLayoutProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/MachineGuiLayoutProbe.java @@ -374,7 +374,8 @@ private static void verifyHelperLimits(RecordingScreen screen) throws Exception private static final class GuiWorld extends ClientLevel { private net.minecraft.world.level.block.state.BlockState state; private GuiWorld() { - super(null, null, net.minecraft.world.level.Level.OVERWORLD, null, 0, () -> net.minecraft.util.profiling.InactiveProfiler.INSTANCE, null, false, 0); + super(null, null, net.minecraft.world.level.Level.OVERWORLD, null, 0, 0, + () -> net.minecraft.util.profiling.InactiveProfiler.INSTANCE, null, false, 0); throw new AssertionError("Allocate the read-only GUI fixture without a world constructor"); } @Override public net.minecraft.world.level.block.state.BlockState getBlockState(BlockPos pos) { return state; } diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java index 680e310e..9bca3f7d 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java @@ -17,6 +17,10 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.core.Direction; import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.tags.FluidTags; +import net.minecraft.tags.TagKey; import net.minecraft.world.level.BlockAndTintGetter; import net.minecraft.world.level.LightLayer; import net.minecraft.world.level.ColorResolver; @@ -27,6 +31,7 @@ import java.nio.ByteBuffer; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -36,26 +41,13 @@ private OilRenderProbe() {} static void verify(Minecraft minecraft, JsonObject result) { // Before a world/server is opened, vanilla's fluid tags have not been synchronized. - net.minecraft.tags.TagContainer previousTags = net.minecraft.tags.SerializationTags.getInstance(); - net.minecraft.tags.TagCollection previousFluids = net.minecraft.tags.FluidTags.getAllTags(); - java.util.Map>, - net.minecraft.tags.TagCollection> restoredCollections = new java.util.HashMap<>(previousTags.collections); - restoredCollections.put(net.minecraft.core.Registry.FLUID_REGISTRY, previousFluids); - net.minecraft.tags.TagContainer restore = new net.minecraft.tags.TagContainer(restoredCollections); - net.minecraft.tags.StaticTagHelper fluidRegistry = net.minecraft.tags.StaticTags.get(new ResourceLocation("fluid")); - Map> tags = new HashMap<>(previousFluids.getAllTags()); - tags.put(new ResourceLocation("minecraft", "water"), net.minecraft.tags.Tag.fromSet( - new java.util.HashSet<>(java.util.Arrays.asList(net.minecraft.world.level.material.Fluids.WATER, net.minecraft.world.level.material.Fluids.FLOWING_WATER)))); - tags.put(new ResourceLocation("minecraft", "lava"), net.minecraft.tags.Tag.fromSet( - new java.util.HashSet<>(java.util.Arrays.asList(net.minecraft.world.level.material.Fluids.LAVA, net.minecraft.world.level.material.Fluids.FLOWING_LAVA)))); - java.util.Map>, - net.minecraft.tags.TagCollection> temporaryCollections = new java.util.HashMap<>(previousTags.collections); - temporaryCollections.put(net.minecraft.core.Registry.FLUID_REGISTRY, net.minecraft.tags.TagCollection.of(tags)); - net.minecraft.tags.TagContainer temporary = new net.minecraft.tags.TagContainer(temporaryCollections); - net.minecraft.tags.SerializationTags.bind(temporary); - // The initial manager contains named wrappers, not resolved collections. Resetting - // every registry would bind those wrappers to themselves and recurse indefinitely. - fluidRegistry.reset(temporary); + Map, List>> previous = fluidTags(); + Map, List>> synthetic = new HashMap<>(previous); + synthetic.put(FluidTags.WATER, List.of(net.minecraft.world.level.material.Fluids.WATER.builtInRegistryHolder(), + net.minecraft.world.level.material.Fluids.FLOWING_WATER.builtInRegistryHolder())); + synthetic.put(FluidTags.LAVA, List.of(net.minecraft.world.level.material.Fluids.LAVA.builtInRegistryHolder(), + net.minecraft.world.level.material.Fluids.FLOWING_LAVA.builtInRegistryHolder())); + Registry.FLUID.bindTags(synthetic); try { requireBucket(Items.crude_oil_bucket, Fluids.crude_oil); requireBucket(Items.refined_oil_bucket, Fluids.refined_oil); @@ -72,12 +64,21 @@ static void verify(Minecraft minecraft, JsonObject result) { valid &= check(minecraft, net.minecraft.world.level.material.Fluids.LAVA.defaultFluidState(), "minecraft:block/lava_still", false, cases); if (!valid) throw new IllegalStateException("Native fluid renderer used the wrong sprite or tint: " + cases); } finally { - fluidRegistry.reset(restore); - net.minecraft.tags.SerializationTags.bind(previousTags); - require(net.minecraft.tags.FluidTags.getAllTags() == previousFluids, "Synthetic fluid tags were not restored"); + Registry.FLUID.resetTags(); + Registry.FLUID.bindTags(previous); + require(fluidTags().equals(previous), "Synthetic fluid tag membership was not restored"); } } + private static Map, List>> fluidTags() { + Map, List>> tags = new HashMap<>(); + Registry.FLUID.getTags().forEach(tag -> { + List> values = tag.getSecond().stream().toList(); + if (!values.isEmpty()) tags.put(tag.getFirst(), values); + }); + return tags; + } + private static void requireBucket(net.minecraft.world.item.Item bucket, Fluid expected) { net.minecraftforge.fluids.FluidStack contents = net.minecraftforge.fluids.FluidUtil.getFluidContained(new ItemStack(bucket)) .orElseThrow(() -> new IllegalStateException("Bucket has no native fluid capability: " + bucket.getRegistryName())); @@ -105,7 +106,8 @@ private static boolean check(Minecraft minecraft, FluidState state, String textu }; BufferBuilder buffer = new BufferBuilder(4096); buffer.begin(com.mojang.blaze3d.vertex.VertexFormat.Mode.QUADS, DefaultVertexFormat.BLOCK); - boolean rendered = minecraft.getBlockRenderer().renderLiquid(BlockPos.ZERO, world, buffer, state); + boolean rendered = minecraft.getBlockRenderer().renderLiquid( + BlockPos.ZERO, world, buffer, world.getBlockState(BlockPos.ZERO), state); buffer.end(); com.mojang.datafixers.util.Pair built = buffer.popNextBuffer(); int vertices = built.getFirst().vertexCount(); diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/RecipeBookGroupingProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/RecipeBookGroupingProbe.java index d4d5e2d6..b5ebaac3 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/RecipeBookGroupingProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/RecipeBookGroupingProbe.java @@ -54,15 +54,15 @@ static void verify(Minecraft minecraft, JsonObject report) { net.minecraft.nbt.CompoundTag saved; try (java.io.InputStream input = Files.newInputStream(root.resolve("server-registries.dat"))) { net.minecraft.nbt.CompoundTag level = net.minecraft.nbt.NbtIo.readCompressed(input); - require("1.17.1".equals(level.getCompound("Data").getCompound("Version").getString("Name")), - "Recipe fixture is not a native 1.17.1 server export"); + require("1.18.2".equals(level.getCompound("Data").getCompound("Version").getString("Name")), + "Recipe fixture is not a native 1.18.2 server export"); net.minecraft.nbt.ListTag mods = level.getCompound("fml").getList("LoadingModList", 10); boolean nativeForge = false; for (int i = 0; i < mods.size(); i++) { net.minecraft.nbt.CompoundTag mod = mods.getCompound(i); - if ("forge".equals(mod.getString("ModId"))) nativeForge = "37.1.1".equals(mod.getString("ModVersion")); + if ("forge".equals(mod.getString("ModId"))) nativeForge = "40.3.0".equals(mod.getString("ModVersion")); } - require(nativeForge, "Recipe fixture was not saved by Forge 37.1.1"); + require(nativeForge, "Recipe fixture was not saved by Forge 40.3.0"); saved = level.getCompound("fml").getCompound("Registries"); } String itemRegistry = net.minecraftforge.registries.ForgeRegistries.ITEMS.getRegistryName().toString(); diff --git a/test-rig/client-resources/resources/META-INF/mods.toml b/test-rig/client-resources/resources/META-INF/mods.toml index faf1ad2a..d17d3f5e 100644 --- a/test-rig/client-resources/resources/META-INF/mods.toml +++ b/test-rig/client-resources/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[37,38)" +loaderVersion="[40,41)" license="GPL-3.0-only" [[mods]] @@ -11,20 +11,20 @@ description='''Build-only model, texture and translation checks. Never distribut [[dependencies.advantageclientresources]] modId="basepower" mandatory=true -versionRange="[2.4.9.117011]" +versionRange="[2.4.10.118021]" ordering="AFTER" side="CLIENT" [[dependencies.advantageclientresources]] modId="forge" mandatory=true -versionRange="[37.1.1]" +versionRange="[40.3.0]" ordering="NONE" side="CLIENT" [[dependencies.advantageclientresources]] modId="minecraft" mandatory=true -versionRange="[1.17.1]" +versionRange="[1.18.2]" ordering="NONE" side="CLIENT" diff --git a/test-rig/client-resources/run.ps1 b/test-rig/client-resources/run.ps1 index 72ec23f2..f5e4115a 100644 --- a/test-rig/client-resources/run.ps1 +++ b/test-rig/client-resources/run.ps1 @@ -30,22 +30,22 @@ function Assert-NativeMetadata([string]$Metadata, [string]$Id) { # Match the known single-mod header, not dependency modId/versionRange fields. $header = [regex]::Match($Metadata, '(?ms)^\[\[mods\]\]\s*(.*?)(?=^\[|\z)').Groups[1].Value if ($header -notmatch ('(?m)^modId\s*=\s*"' + [regex]::Escape($Id) + '"[ \t]*(?:#[^\r\n]*)?\r?$') -or - $header -notmatch '(?m)^version\s*=\s*"[^"\r\n]*\.117011"[ \t]*(?:#[^\r\n]*)?\r?$' -or - $Metadata -notmatch '(?m)^loaderVersion\s*=\s*"\[37,') { throw "Not a native Forge 37 candidate: $Id" } - $expected = @{ basepower = '2.4.9.117011'; basesteam = '2.2.10.117011'; baseelectric = '2.2.10.117011' } + $header -notmatch '(?m)^version\s*=\s*"[^"\r\n]*\.118021"[ \t]*(?:#[^\r\n]*)?\r?$' -or + $Metadata -notmatch '(?m)^loaderVersion\s*=\s*"\[40,') { throw "Not a native Forge 40 candidate: $Id" } + $expected = @{ basepower = '2.4.10.118021'; basesteam = '2.2.11.118021'; baseelectric = '2.2.11.118021' } if ($expected.ContainsKey($Id) -and $header -notmatch ('(?m)^version\s*=\s*"' + [regex]::Escape($expected[$Id]) + '"[ \t]*(?:#[^\r\n]*)?\r?$')) { throw "Unexpected native candidate version for $Id; expected $($expected[$Id])" } } function Assert-NativeLaunch([hashtable]$Environment, [hashtable]$Attributes) { - if ($Environment.MC_VERSION -ne '1.17.1' -or $Environment.FORGE_VERSION -ne '37.1.1' -or - $Environment.MCP_MAPPINGS -ne 'official_1.17.1' -or $Environment.MCP_VERSION -ne '20210706.113038' -or - $Environment.target -ne 'fmluserdevclient') { throw 'Regenerate the native 1.17.1 / Forge 37.1.1 official client launch.' } + if ($Environment.MC_VERSION -ne '1.18.2' -or $Environment.FORGE_VERSION -ne '40.3.0' -or + $Environment.MCP_MAPPINGS -ne 'official_1.18.2' -or $Environment.MCP_VERSION -ne '20220404.173914' -or + $Environment.target -ne 'fmluserdevclient') { throw 'Regenerate the native 1.18.2 / Forge 40.3.0 official client launch.' } $program = $Attributes['org.eclipse.jdt.launching.PROGRAM_ARGUMENTS'] - if ($program -notmatch '1\.17\.1-37\.1\.1' -or + if ($program -notmatch '1\.18\.2-40\.3\.0' -or $program -notmatch '--main cpw\.mods\.bootstraplauncher\.BootstrapLauncher\b' -or $program -notmatch '--launchTarget forgeclientuserdev\b' -or - $program -match '1\.(?:13\.2|14\.4|15\.2|16\.5)|--(?:server|port|world)\b' -or + $program -match '1\.(?:13\.2|14\.4|15\.2|16\.5|17\.1)|--(?:server|port|world)\b' -or @([regex]::Matches($program, '--gameDir\s+\.(?=\s|$)')).Count -ne 1 -or @([regex]::Matches($program, '--gameDir\b')).Count -ne 1 -or $Attributes['org.eclipse.jdt.launching.MAIN_TYPE'] -ne 'net.minecraftforge.launcher.Main') { @@ -53,21 +53,21 @@ function Assert-NativeLaunch([hashtable]$Environment, [hashtable]$Attributes) { } } function Assert-NativeArchive([string]$Path) { - if ($Path -match '(?:forge|client-extra)-1\.(?:13\.2|14\.4|15\.2|16\.5)|[\\/]PA11[3456][\\/]') { throw "Legacy runtime archive: $Path" } + if ($Path -match '(?:forge|client-extra)-1\.(?:13\.2|14\.4|15\.2|16\.5|17\.1)|[\\/]PA11[34567][\\/]') { throw "Legacy runtime archive: $Path" } } function Quote-Argument([string]$Value) { '"' + [regex]::Replace([regex]::Replace($Value, '(\\*)"', '$1$1\"'), '(\\+)$', '$1$1') + '"' } $probe = Join-Path $project 'build/client-resource-probe/advantageclientresources-deobf.jar' $runtime = Join-Path $project ('run-client-resource-runs/' + [DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N').Substring(0, 8)) -$inputReport = [ordered]@{ minecraft = '1.17.1'; forge = '37.1.1'; mappings = 'official_1.17.1'; mcp = '20210706.113038'; qualification = 'UNQUALIFIED'; output_source = $OutputSource; launch_profile = $LaunchProfile; require_extensions = [bool]$RequireExtensions; child_renderer_geometry = [bool]$ChildRendererGeometry } +$inputReport = [ordered]@{ minecraft = '1.18.2'; forge = '40.3.0'; mappings = 'official_1.18.2'; mcp = '20220404.173914'; qualification = 'UNQUALIFIED'; output_source = $OutputSource; launch_profile = $LaunchProfile; require_extensions = [bool]$RequireExtensions; child_renderer_geometry = [bool]$ChildRendererGeometry } $inputReport.machine_gui_layouts = [bool]$MachineGuiLayouts $phase = 'launch-preflight' New-Item -ItemType Directory -Path $runtime | Out-Null try { if ($ChildRendererGeometry -and -not $RequireExtensions) { throw 'Child renderer geometry requires -RequireExtensions.' } -if (-not (Test-Path -LiteralPath $java)) { throw "Java 16 runtime not found: $java" } -if ((Get-Content -LiteralPath (Join-Path $JavaHome 'release') -Raw) -notmatch '(?m)^JAVA_VERSION="16(?:\.|\+|")') { throw 'Supply Java 16 for the native client.' } +if (-not (Test-Path -LiteralPath $java)) { throw "Java 17 runtime not found: $java" } +if ((Get-Content -LiteralPath (Join-Path $JavaHome 'release') -Raw) -notmatch '(?m)^JAVA_VERSION="17(?:\.|\+|")') { throw 'Supply Java 17 for the native client.' } if (-not (Test-Path -LiteralPath $probe)) { throw 'Run gradlew.bat clientResourceProbeJar verifyEclipseProductionClasspath first.' } [xml]$launch = Get-Content -LiteralPath (Join-Path $project $LaunchProfile) -Raw $attributes = @{} @@ -76,8 +76,8 @@ $inputReport.java = Get-Record $java $inputReport.launch = Get-Record (Join-Path $project $LaunchProfile) $inputReport.probe = Get-Record $probe $probeMetadata = Read-JarMetadata $probe -if ($probeMetadata -notmatch 'loaderVersion="\[37,38\)"' -or $probeMetadata -notmatch 'versionRange="\[1\.17\.1\]"' -or - $probeMetadata -notmatch 'versionRange="\[37\.1\.1\]"' -or $probeMetadata -notmatch 'versionRange="\[2\.4\.9\.117011\]"') { throw 'Rebuild the native 1.17 probe; do not substitute a legacy probe.' } +if ($probeMetadata -notmatch 'loaderVersion="\[40,41\)"' -or $probeMetadata -notmatch 'versionRange="\[1\.18\.2\]"' -or + $probeMetadata -notmatch 'versionRange="\[40\.3\.0\]"' -or $probeMetadata -notmatch 'versionRange="\[2\.4\.10\.118021\]"') { throw 'Rebuild the native 1.18 probe; do not substitute a legacy probe.' } Copy-Item -LiteralPath (Join-Path $project $LaunchProfile) -Destination (Join-Path $runtime 'launch.xml') $environment = @{} foreach ($map in $launch.launchConfiguration.mapAttribute) { @@ -108,7 +108,7 @@ $outputFiles = @(Get-ChildItem -LiteralPath $modOutput -Recurse -File | Sort-Obj if (@($outputFiles | Where-Object { $_.path -like '*.class' }).Count -eq 0) { throw 'Selected output has no compiled production classes.' } $outputFiles | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $runtime 'production-files.json') -Encoding utf8 $inputReport.production_files = Get-Record (Join-Path $runtime 'production-files.json') -if (-not $PowerCandidate) { $PowerCandidate = Join-Path $project 'build/libs-dev/basePower-2.4.9.117011-deobf.jar' } +if (-not $PowerCandidate) { $PowerCandidate = Join-Path $project 'build/libs-dev/basePower-2.4.10.118021-deobf.jar' } $PowerCandidate = (Resolve-Path -LiteralPath $PowerCandidate).Path Assert-NativeMetadata (Read-JarMetadata $PowerCandidate) 'basepower' $inputReport.power_candidate = Get-Record $PowerCandidate @@ -174,7 +174,7 @@ foreach ($archive in $archives) { $archives = $launchArchives $inputReport.archives = $archiveRecords $inputReport.runtime_candidates = $runtimeCandidates -if (-not ($archives -match 'forge-1\.17\.1-37\.1\.1\.jar$')) { throw 'Missing actual native mapped Forge runtime.' } +if (-not ($archives -match 'forge-1\.18\.2-40\.3\.0\.jar$')) { throw 'Missing actual native mapped Forge runtime.' } if ($RequireExtensions -and (-not $runtimeCandidates.ContainsKey('basesteam') -or -not $runtimeCandidates.ContainsKey('baseelectric'))) { throw 'Combined launch must explicitly contain both native expansion candidates.' } $arguments = @([regex]::Matches($attributes['org.eclipse.jdt.launching.PROGRAM_ARGUMENTS'], '"[^"]*"|\S+') | ForEach-Object { $_.Value.Trim('"') }) $vmArguments = @([regex]::Matches($attributes['org.eclipse.jdt.launching.VM_ARGUMENTS'], '"[^"]*"|\S+') | ForEach-Object { $_.Value.Trim('"') }) @@ -214,7 +214,7 @@ $resultPath = Join-Path $runtime 'client-resources.json' if (-not (Test-Path -LiteralPath $resultPath)) { throw "Client did not produce a result; inspect $runtime/logs" } $result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json if ($result.status -ne 'PASS') { throw "Client resource check failed: $($result.error); inspect $runtime" } -if ($result.minecraft -ne '1.17.1' -or $result.forge -ne '37.1.1') { throw 'Result is not native 1.17.1 / Forge 37.1.1 evidence.' } +if ($result.minecraft -ne '1.18.2' -or $result.forge -ne '40.3.0') { throw 'Result is not native 1.18.2 / Forge 40.3.0 evidence.' } if ($MachineGuiLayouts -and $result.machine_gui_layout_status -ne 'PASS') { throw 'Machine GUI layouts did not pass; inspect the retained images and JSON.' } $expectedGuiTextChecks = if ($RequireExtensions) { 351 } else { 126 } if ($MachineGuiLayouts -and ($result.machine_gui_text_spec -ne 2 -or $result.machine_gui_text_status -ne 'PASS' -or diff --git a/test-rig/client-resources/test-launcher.ps1 b/test-rig/client-resources/test-launcher.ps1 index 360dc644..e5d9e3e3 100644 --- a/test-rig/client-resources/test-launcher.ps1 +++ b/test-rig/client-resources/test-launcher.ps1 @@ -76,33 +76,33 @@ if ($CrossVersionEvidenceRoots) { $native = @' modLoader="javafml" -loaderVersion="[37,)" # native Forge +loaderVersion="[40,)" # native Forge [[mods]] # mandatory header modId="basepower" # inline comment -version="2.4.9.117011" # native candidate +version="2.4.10.118021" # native candidate [[dependencies.basepower]] modId="minecraft" mandatory=true -versionRange="[1.17.1]" +versionRange="[1.18.2]" '@ Assert-NativeMetadata $native 'basepower' $checks++ -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.9.117011','2.4.4.113021')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.9.117011','2.4.5.114041')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.9.117011','2.4.5.115021')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('[37,)','[36,)')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.9.117011','2.4.7.116051')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.9.117011','${version}')) 'basepower' } +Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.10.118021','2.4.4.113021')) 'basepower' } +Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.10.118021','2.4.5.114041')) 'basepower' } +Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.10.118021','2.4.5.115021')) 'basepower' } +Assert-Throws { Assert-NativeMetadata ($native.Replace('[40,)','[36,)')) 'basepower' } +Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.10.118021','2.4.7.116051')) 'basepower' } +Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.10.118021','${version}')) 'basepower' } Assert-Throws { Assert-NativeMetadata $native 'baseelectric' } Assert-Throws { Assert-NativeMetadata '' 'basepower' } foreach ($id in 'basesteam','baseelectric') { - $expansion = $native.Replace('basepower', $id).Replace('2.4.9.117011','2.2.10.117011') + $expansion = $native.Replace('basepower', $id).Replace('2.4.10.118021','2.2.11.118021') Assert-NativeMetadata $expansion $id $checks++ - Assert-Throws { Assert-NativeMetadata ($expansion.Replace('2.2.10.117011','2.2.6.115021')) $id } + Assert-Throws { Assert-NativeMetadata ($expansion.Replace('2.2.11.118021','2.2.6.115021')) $id } } -$environment = @{ MC_VERSION = '1.17.1'; FORGE_VERSION = '37.1.1'; MCP_MAPPINGS = 'official_1.17.1'; MCP_VERSION = '20210706.113038'; target = 'fmluserdevclient' } -$attributes = @{ 'org.eclipse.jdt.launching.PROGRAM_ARGUMENTS' = '--main cpw.mods.bootstraplauncher.BootstrapLauncher --cache D:\native\1.17.1-37.1.1 -- --launchTarget forgeclientuserdev --gameDir .'; 'org.eclipse.jdt.launching.MAIN_TYPE' = 'net.minecraftforge.launcher.Main' } +$environment = @{ MC_VERSION = '1.18.2'; FORGE_VERSION = '40.3.0'; MCP_MAPPINGS = 'official_1.18.2'; MCP_VERSION = '20220404.173914'; target = 'fmluserdevclient' } +$attributes = @{ 'org.eclipse.jdt.launching.PROGRAM_ARGUMENTS' = '--main cpw.mods.bootstraplauncher.BootstrapLauncher --cache D:\native\1.18.2-40.3.0 -- --launchTarget forgeclientuserdev --gameDir .'; 'org.eclipse.jdt.launching.MAIN_TYPE' = 'net.minecraftforge.launcher.Main' } Assert-NativeLaunch $environment $attributes $checks++ foreach ($key in 'MC_VERSION','FORGE_VERSION','MCP_MAPPINGS','MCP_VERSION','target') { @@ -118,7 +118,7 @@ foreach ($argument in '--server localhost','--port 25565','--gameDir D:\protecte $invalid = $attributes.Clone() $invalid['org.eclipse.jdt.launching.MAIN_TYPE'] = 'net.minecraft.client.main.Main' Assert-Throws { Assert-NativeLaunch $environment $invalid } -Assert-NativeArchive 'D:\native\forge-1.17.1-37.1.1.jar' +Assert-NativeArchive 'D:\native\forge-1.18.2-40.3.0.jar' $checks++ foreach ($path in 'D:\PA114\client.jar','D:\PA113\client.jar','D:\PA116\client.jar','D:\native\forge-1.16.5-36.2.34.jar','D:\native\forge-1.14.4-28.2.26.jar','D:\native\client-extra-1.13.2.jar') { Assert-Throws { Assert-NativeArchive $path } @@ -135,7 +135,7 @@ if ($StaticOnly) { return } $project = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path -$power = Join-Path $project 'build/libs-dev/basePower-2.4.9.117011-deobf.jar' +$power = Join-Path $project 'build/libs-dev/basePower-2.4.10.118021-deobf.jar' Assert-NativeMetadata (Read-JarMetadata $power) 'basepower' $checks++ [xml]$launch = Get-Content -LiteralPath (Join-Path $project 'runClientAllAdvantage.launch') -Raw diff --git a/test-rig/gradle.properties b/test-rig/gradle.properties index 5756da63..e824be5c 100644 --- a/test-rig/gradle.properties +++ b/test-rig/gradle.properties @@ -1,12 +1,12 @@ org.gradle.jvmargs=-Xmx2G org.gradle.daemon=false -minecraft_version=1.17.1 -forge_version=37.1.1 +minecraft_version=1.18.2 +forge_version=40.3.0 mapping_channel=official -mapping_version=1.17.1 -mcp_version=20210706.113038 -harness_version=1.1.1.117011 -poweradvantage_deobf_sha256=312FA3012CB57C91F5F22079DA4778D481BA45EF7B8A0587D9993899E1B6D08E -poweradvantage_source_commit=308d42a89483d56d0fad4805cdb65dac483d9e31 -poweradvantage_pin_status=API_PIN_VERIFIED_RUNTIME_UNSEALED +mapping_version=1.18.2 +mcp_version=20220404.173914 +harness_version=1.1.2.118021 +poweradvantage_deobf_sha256=C00F835A7A80BAF332355EF1F181F252B603A017A979D2F41EA3BBEAE314E280 +poweradvantage_source_commit=bab9971cc2d593eee1f4a3de6dafc7ec268d25c2 +poweradvantage_pin_status=SOURCE_AND_ARTIFACT_PINNED diff --git a/test-rig/migration/audit_native118_endurance.py b/test-rig/migration/audit_native118_endurance.py new file mode 100644 index 00000000..a81ef413 --- /dev/null +++ b/test-rig/migration/audit_native118_endurance.py @@ -0,0 +1,111 @@ +"""Replay every station checkpoint from a completed 1.18.2 endurance run.""" + +import argparse +from collections import Counter +from datetime import datetime +from pathlib import Path +from unittest.mock import patch + +import endurance +from matrix import digest, write_json +from native117 import require_disjoint +from native117_broad import read_json +import native118 + + +def replay(run, profile, specification): + report = read_json(run / "native118-endurance-results.json") + modes = ("fresh118", "retained-upgrade-native118") + require = endurance.require + require(report["status"] == "PASS" and report["minecraft"] == native118.MINECRAFT + and report["duration_seconds"] == 1800 and report["interval_seconds"] == 60 + and report["retained_unchanged"] is True and set(report["profiles"]) == set(modes), + "Expected a complete fresh and migrated 30-minute run") + require(digest(profile) == report["profileSha256"] and digest(specification) == report["stationDefinitionSha256"], + "The profile or station definition differs from the run") + require(report["checked_stations"] == endurance.CHECKED, "Wrong station coverage") + definitions = {row["id"]: row for row in read_json(specification)["stations"]} + pins = read_json(profile)["mods"] + expected_jars = {row["file"]: row["sha256"].upper() for row in pins} + mod_versions = {row["id"]: row["version"] for row in pins} + fresh = report["fresh_fixture_proof"] + require(fresh["existing_world_absent"] is True and fresh["critical_block"]["actualName"] == "minecraft:air", + "Missing pre-construction fresh-world evidence") + for name, expected in fresh["files"].items(): + require(digest(run / modes[0] / name) == expected, "Fresh-world evidence changed: " + name) + require(read_json(run / modes[1] / "retained-comparison.json")["status"] == "PASS", + "Migrated world was not checked before station construction") + + labels = [f"cycle-{i:03d}" for i in range(1, 31)] + [ + "before-restart", "after-restart", "restart-resumed"] + results = {} + for mode in modes: + runtime = run / mode + station_path = runtime / "advantage-works-results" + native118.verify_runtime(runtime) + require({jar.name: digest(jar) for jar in (runtime / "mods").glob("*.jar")} == expected_jars, + mode + ": staged artifacts differ from the approved profile") + for boot in ("endurance", "endurance-restart"): + native118.require_native_boot(runtime / f"console-{boot}.log") + rig = endurance.Rig(mode, runtime, None, None, definitions) + + def snapshot(station, label): + document = read_json(station_path / f"{station}-checkpoint-{label}.json") + require(document["minecraft"] == native118.MINECRAFT and document["forge"] == native118.FORGE + and all(document["mods"].get(key) == value for key, value in mod_versions.items()), + mode + ": checkpoint runtime differs from profile") + return document + + setup = snapshot("E-02", "setup") + rig.static_batteries = endurance.parse_snapshot(setup)[rig.pos("E-02", (3, 1, 12))]["data"]["Items"] + ledger = read_json(runtime / "endurance-ledger.json") + require([row["label"] for row in ledger] == labels, mode + ": missing or misordered checkpoints") + times = {} + totals = Counter() + for row in ledger: + label = row["label"] + for station in endurance.CHECKED: + document = snapshot(station, label) + rig.documents[station], rig.snapshots[station] = document, endurance.parse_snapshot(document) + with patch.object(endurance, "write_json"): + verified = rig.batch(label, power="basepower") + require(verified == row, mode + "/" + label + ": raw result differs from the reported ledger") + times[label] = datetime.fromisoformat(rig.documents["W-01"]["recordedAtUtc"]) + if label.startswith("cycle-"): + totals.update(verified["outputs"]) + setup_time = snapshot("W-01", "setup")["worldTime"] + ticks = ledger[29]["world_time"] - setup_time + require(ticks == report["profiles"][mode]["game_ticks"], + mode + ": reported ticks differ from the setup-to-end checkpoints") + require(all(before < after for before, after in zip(times.values(), list(times.values())[1:])), + mode + ": nonmonotonic checkpoint times") + sampled_seconds = (times["cycle-030"] - times["cycle-001"]).total_seconds() + sampled_ticks = ledger[29]["world_time"] - ledger[0]["world_time"] + require(report["profiles"][mode]["elapsed_seconds"] >= 1800 + and sampled_seconds >= 1735, + mode + ": checkpoints do not span the measured endurance period") + require(sampled_ticks >= 18 * sampled_seconds, mode + ": less than 18 measured game ticks per second") + require((times["restart-resumed"] - times["after-restart"]).total_seconds() >= 60, + mode + ": missing minute of resumed operation") + require(dict(totals) == report["profiles"][mode]["endurance_outputs"], mode + ": output totals differ") + results[mode] = dict(checkpoints=len(ledger), stationSnapshots=len(ledger) * len(endurance.CHECKED), + setupToEndTicks=ticks, sampledTicks=sampled_ticks, sampledSeconds=sampled_seconds, + sampledTicksPerSecond=sampled_ticks / sampled_seconds, outputs=dict(totals)) + return dict(status="PASS", inputReceiptSha256=digest(run / "native118-endurance-results.json"), profiles=results) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("run", "profile", "specification", "output"): + parser.add_argument("--" + name, required=True, type=Path) + args = parser.parse_args() + for path in (args.run, args.profile, args.specification): + require_disjoint(args.output, path) + if args.output.exists(): + raise FileExistsError(args.output) + write_json(args.output, replay(args.run, args.profile, args.specification)) + print("PASS: both 30-minute ledgers replayed from native station snapshots") + + +if __name__ == "__main__": + main() diff --git a/test-rig/migration/native117_actors.py b/test-rig/migration/native117_actors.py index e79303c1..f0e9cb09 100644 --- a/test-rig/migration/native117_actors.py +++ b/test-rig/migration/native117_actors.py @@ -14,7 +14,6 @@ from matrix import WORLD, copy_fixture, digest, fingerprint, write_json from native117_code import Server from native117 import output_path, pinned_profile, require_clean_log, require_disjoint -from native117 import boot from native117_actor_io import capture_actors, capture_dimensions, source_identities from native117_dimension_assessment import assess, load_native_proof, summarize_checks import native117_dimension_assessment @@ -135,7 +134,7 @@ def await_natural_ticks(server, runtime, kind, released): raise TimeoutError(f"Actor world did not advance 80 natural ticks after {released}") -def ready_entities(server, runtime, kind, label): +def ready_entities(server, runtime, kind, label, minecraft="1.17.1"): definition = json.loads((runtime / ( 'dimension-fixture.json' if kind == 'dimensions' else 'actor-fixture.json')).read_text()) dimension = DIMENSIONS[definition['dimension']] if kind == 'dimensions' else DIMENSIONS[0] @@ -143,20 +142,25 @@ def ready_entities(server, runtime, kind, label): console = SimpleNamespace(send=lambda command: server.send('execute in ' + dimension + ' run ' + command), wait=server.wait) console.send('forceload add 96 -32 223 47') - receipt = await_ready(console, runtime, (6, -2, 13, 2), 'entities-' + label, dimension) + receipt = await_ready(console, runtime, (6, -2, 13, 2), 'entities-' + label, dimension, minecraft=minecraft) if receipt['dimension'] != dimension: raise AssertionError('Entity readiness observed the wrong dimension') return receipt -def run_route(source, runtime, target, args): +def run_route(source, runtime, target, args, *, stage=None, server_type=None, + stop_audit=None, check_log=None, minecraft="1.17.1"): from native117 import boot + server_type = server_type or Server + stop_audit = stop_audit or audit_stopped + check_log = check_log or require_clean_log + java = getattr(args, "java17", None) or args.java8 before = fingerprint(source) approved = source / "fixture-hashes.json" if approved.is_file() and fingerprint(source / WORLD) != json.loads(approved.read_text()): raise AssertionError(f"Protected fixture differs from approved hashes: {source}") expected, source_label = baseline(source, args.kind) - launcher = boot(runtime, target, args.java8, args.port) + launcher = stage(runtime, target, args.port) if stage else boot(runtime, target, java, args.port) (runtime / 'advantage-works.allow').touch() copy_fixture(source, runtime) fixture_name = "dimension-fixture.json" if args.kind == "dimensions" else "actor-fixture.json" @@ -178,7 +182,7 @@ def run_route(source, runtime, target, args): checks, assessments = {}, {} def corroborate_stopped(label): - receipt = audit_stopped(runtime, runtime / 'stopped-entity-evidence' / label) + receipt = stop_audit(runtime, runtime / 'stopped-entity-evidence' / label) checks['saved-entities-' + label] = dict(status=receipt['status'], snapshotSha256=receipt['snapshotSha256'], matchedEntities=receipt['matchedEntities'], auditedDimensions=receipt['auditedDimensions']) @@ -192,9 +196,9 @@ def compare(key, before, after, survival=False, adapter=True): "survival" if survival else "strict", adapter) return strict try: - server = Server(runtime, args.java8, launcher, "conversion") + server = server_type(runtime, java, launcher, "conversion") try: - checks['readiness-conversion'] = ready_entities(server, runtime, args.kind, 'conversion') + checks['readiness-conversion'] = ready_entities(server, runtime, args.kind, 'conversion', minecraft) if args.kind == "dimensions": release(server, runtime, args.kind, "conversion-release") await_natural_ticks(server, runtime, args.kind, "conversion-release") @@ -211,10 +215,10 @@ def compare(key, before, after, survival=False, adapter=True): finally: server.close() corroborate_stopped('conversion') - require_clean_log(runtime / "console-conversion.log") - server = Server(runtime, args.java8, launcher, "restart") + check_log(runtime / "console-conversion.log") + server = server_type(runtime, java, launcher, "restart") try: - checks['readiness-restart'] = ready_entities(server, runtime, args.kind, 'restart') + checks['readiness-restart'] = ready_entities(server, runtime, args.kind, 'restart', minecraft) if args.kind == "dimensions": release(server, runtime, args.kind, "restart-load-release") await_natural_ticks(server, runtime, args.kind, "restart-load-release") @@ -231,10 +235,10 @@ def compare(key, before, after, survival=False, adapter=True): finally: server.close() corroborate_stopped('restart') - require_clean_log(runtime / "console-restart.log") - server = Server(runtime, args.java8, launcher, "runtime-restart") + check_log(runtime / "console-restart.log") + server = server_type(runtime, java, launcher, "runtime-restart") try: - checks['readiness-runtime-restart'] = ready_entities(server, runtime, args.kind, 'runtime-restart') + checks['readiness-runtime-restart'] = ready_entities(server, runtime, args.kind, 'runtime-restart', minecraft) release(server, runtime, args.kind, "restart-released") await_natural_ticks(server, runtime, args.kind, "restart-released") operated_again = command(server, runtime, args.kind, "read", "runtime-restart") @@ -243,7 +247,7 @@ def compare(key, before, after, survival=False, adapter=True): finally: server.close() corroborate_stopped('runtime-restart') - require_clean_log(runtime / "console-runtime-restart.log") + check_log(runtime / "console-runtime-restart.log") checks["backup-stability"] = {"status": "PASS" if actors.backup_state(runtime) == backups else "FAIL", "backups": backups} return checks finally: diff --git a/test-rig/migration/native117_broad.py b/test-rig/migration/native117_broad.py index 29803aac..bcc60bba 100644 --- a/test-rig/migration/native117_broad.py +++ b/test-rig/migration/native117_broad.py @@ -127,13 +127,13 @@ def restarted_state(before, after): return dict(status="PASS", drills=len(after), exactEnergy=True) -def assertions_state(records, stations): +def assertions_state(records, stations, minecraft="1.17.1"): if set(records) != {station["id"] for station in stations}: raise AssertionError("The broad sweep omitted a bay") counts = Counter() manual = [] for key, document in records.items(): - if document["station"] != key or document["minecraft"] != "1.17.1": + if document["station"] != key or document["minecraft"] != minecraft: raise AssertionError("Wrong bay/target report") if document["status"] not in ("PASS", "MANUAL_REQUIRED"): raise AssertionError(key + ": " + document["status"]) @@ -179,7 +179,7 @@ def configure_broad(runtime, port): path.write_text("\n".join(lines) + "\n" + "".join(f"{key}={value}\n" for key, value in overrides.items())) -class BroadServer(Server): +class BroadCommands: def command(self, command, token): self.send(command) self.wait(token, 150) @@ -196,23 +196,47 @@ def check_all(self, stations, phase): return records -def run_profile(args, profile, output, name, port, stations): - if name == "packaged-decoupled": - profile = (profile[0], profile[1], [p for p in profile[2] if not p.name.startswith("BaseMetals-")]) +class BroadServer(BroadCommands, Server): + pass + + +def run_profile(args, profile, output, name, port, stations, *, native118_target=None): runtime = output / name - launcher = stage(runtime, profile) + if native118_target is None: + if name == "packaged-decoupled": + profile = (profile[0], profile[1], [p for p in profile[2] if not p.name.startswith("BaseMetals-")]) + launcher = stage(runtime, profile) + pins = read_json(args.target_manifest)["target117"]["jars"] + verify = lambda: verify_staged(runtime, profile) + require_boot = require_native_boot + server_type, java = BroadServer, args.java8 + versions = dict(minecraft="1.17.1", forge="37.1.1") + else: + import native118 + + class NativeBroadServer(BroadCommands, native118.Server): + pass + + seed, code, entries = native118_target + if name == "packaged-decoupled": + entries = {key: value for key, value in entries.items() if key != "basemetals"} + launcher = native118.stage(runtime, (seed, code, entries), port) + pins = entries.values() + verify = lambda: native118.verify_runtime(runtime) + require_boot = native118.require_clean_log + server_type, java = NativeBroadServer, args.java17 + versions = dict(minecraft=native118.MINECRAFT, forge=native118.FORGE) configure_broad(runtime, port) authorize_broad(runtime) result = dict(status="FAIL", profile=name, port=port, phases={}) - pins = read_json(args.target_manifest)["target117"]["jars"] expected_mods = {pin["id"]: pin["version"] for pin in pins if name != "packaged-decoupled" or pin["id"] != "basemetals"} - expected_mods.update(minecraft="1.17.1", forge="37.1.1") + expected_mods.update(versions) result["expectedMods"] = expected_mods server = None try: - verify_staged(runtime, profile) - server = BroadServer(runtime, args.java8, launcher, "first") + verify() + server = server_type(runtime, java, launcher, "first") server.command("advworks build all", "Advantage Works built 21 station(s)") # Named finite stock and tile data are commissioning witnesses, not repairs # performed after operation or restart. Every destination is unchanged later. @@ -239,7 +263,7 @@ def run_profile(args, profile, output, name, port, stations): server.command("advworks stop " + sid, "Stopped " + sid) first = server.check_all(stations, "first") loaded_mods(first, expected_mods) - result["phases"]["first"] = assertions_state(first, stations) + result["phases"]["first"] = assertions_state(first, stations, versions["minecraft"]) before = {} for station in stations: if station["id"] in DRILL_BAYS: @@ -248,14 +272,14 @@ def run_profile(args, profile, output, name, port, stations): result["beforeRestart"] = before server.close() server = None - require_native_boot(runtime / "console-first.log") - verify_staged(runtime, profile) - server = BroadServer(runtime, args.java8, launcher, "restarted") + require_boot(runtime / "console-first.log") + verify() + server = server_type(runtime, java, launcher, "restarted") # No start, reset, data merge, replacement, or refill on the second boot. time.sleep(args.run_seconds) second = server.check_all(stations, "restarted") loaded_mods(second, expected_mods) - result["phases"]["restarted"] = assertions_state(second, stations) + result["phases"]["restarted"] = assertions_state(second, stations, versions["minecraft"]) after = {} for station in stations: if station["id"] in DRILL_BAYS: @@ -264,8 +288,8 @@ def run_profile(args, profile, output, name, port, stations): result["phases"]["persistence"] = restarted_state(before, after) server.close() server = None - require_native_boot(runtime / "console-restarted.log") - verify_staged(runtime, profile) + require_boot(runtime / "console-restarted.log") + verify() result["status"] = "PASS" except Exception: result["error"] = traceback.format_exc() diff --git a/test-rig/migration/native117_broad_audit_fixtures.py b/test-rig/migration/native117_broad_audit_fixtures.py index 1a850ea0..b701a0d5 100644 --- a/test-rig/migration/native117_broad_audit_fixtures.py +++ b/test-rig/migration/native117_broad_audit_fixtures.py @@ -14,6 +14,9 @@ LINE = Path(__file__).resolve().parents[3].name.removeprefix("PA") +# This is a historical audit fixture, not an attestation about the active port. +if LINE == "118": + LINE = "117" def write(path, data): diff --git a/test-rig/migration/native117_broad_result_audit.py b/test-rig/migration/native117_broad_result_audit.py index c81e9b62..2a339054 100644 --- a/test-rig/migration/native117_broad_result_audit.py +++ b/test-rig/migration/native117_broad_result_audit.py @@ -19,7 +19,7 @@ PROFILES = ("packaged-current", "packaged-decoupled") VERSIONS = {"113": ("1.13.2", "25.0.223"), "114": ("1.14.4", "28.2.26"), "115": ("1.15.2", "31.2.57"), "116": ("1.16.5", "36.2.34"), - "117": ("1.17.1", "37.1.1")} + "117": ("1.17.1", "37.1.1"), "118": ("1.18.2", "40.3.0")} NATIVE117_LAUNCHERS = { "libraries/net/minecraftforge/forge/1.17.1-37.1.1/win_args.txt", "libraries/net/minecraftforge/forge/1.17.1-37.1.1/unix_args.txt", @@ -29,6 +29,15 @@ "libraries/cpw/mods/bootstraplauncher/0.1.17/bootstraplauncher-0.1.17.jar", "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-extra.jar", } +NATIVE118_LAUNCHERS = { + "libraries/net/minecraftforge/forge/1.18.2-40.3.0/win_args.txt", + "libraries/net/minecraftforge/forge/1.18.2-40.3.0/unix_args.txt", +} +NATIVE118_REQUIRED_CODE = { + "libraries/net/minecraftforge/fmlloader/1.18.2-40.3.0/fmlloader-1.18.2-40.3.0.jar", + "libraries/cpw/mods/bootstraplauncher/1.0.0/bootstraplauncher-1.0.0.jar", + "libraries/net/minecraft/server/1.18.2-20220404.173914/server-1.18.2-20220404.173914-extra.jar", +} STATIONS = tuple(["W-%02d" % n for n in range(1, 5)] + ["S-%02d" % n for n in range(1, 9)] + ["E-%02d" % n for n in range(1, 9)] + ["P-API-01"]) @@ -259,6 +268,16 @@ def log_evidence(evidence, path, line, phase, records): r"Advantage Works command failed") for row in text.splitlines(): require(not error.search(row), str(path) + ": native117 runtime error: " + row) + elif line == "118": + from native118 import log_findings + require("with MCP 20220404.173914" in text, str(path) + ": missing native118 MCP identity") + require("ThreadedAnvilChunkStorage: All dimensions are saved" in shutdown, + str(path) + ": missing completed all-dimension shutdown") + failures, diagnostics = log_findings(text) + require(not failures, str(path) + ": native118 runtime error: " + repr(failures[:5])) + if diagnostics: + require(evidence.json(path.with_suffix(".platform-diagnostics.json")) == diagnostics, + str(path) + ": changed or unrecorded Netty capability diagnostics") return dict(status="PASS", saveAndAllDimensionShutdown=True, explicitSaveCommand=saved >= 0, processExit="NOT_INDEPENDENTLY_RECORDED_BY_HISTORICAL_BROAD_WRITER") @@ -275,11 +294,14 @@ def staged_pins(evidence, runtime, pins, name, line=None): require(sha(evidence.read(runtime / pins["launcher"])) == pins["launcher_sha256"].upper(), name + ": staged launcher changed") sealed = pins.get("serverCode", {}).get("files") - if line == "117": - require(pins["launcher"] in NATIVE117_LAUNCHERS, name + ": native117 requires the modular Forge launcher") - require(isinstance(sealed, dict) and sealed, name + ": missing sealed native117 engine inventory") - require(NATIVE117_REQUIRED_CODE | {pins["launcher"]} <= set(sealed), - name + ": native117 engine inventory omits required code") + if line in ("117", "118"): + label = "native" + line + launchers, required = ((NATIVE117_LAUNCHERS, NATIVE117_REQUIRED_CODE) if line == "117" + else (NATIVE118_LAUNCHERS, NATIVE118_REQUIRED_CODE)) + require(pins["launcher"] in launchers, name + ": " + label + " requires the modular Forge launcher") + require(isinstance(sealed, dict) and sealed, name + ": missing sealed " + label + " engine inventory") + require(required | {pins["launcher"]} <= set(sealed), + name + ": " + label + " engine inventory omits required code") folded = set() for relative, entry in sealed.items(): require(relative.startswith("libraries/") and "\\" not in relative @@ -296,12 +318,13 @@ def staged_pins(evidence, runtime, pins, name, line=None): name + ": linked or escaping native117 engine file") import tomllib for pin in jars: - require(pin["version"].endswith(".117011"), name + ": non-native117 mod version") + require(pin["version"].endswith(".117011" if line == "117" else ".118021"), + name + ": non-" + label + " mod version") with zipfile.ZipFile(runtime / "mods" / Path(pin["path"]).name) as archive: metadata = tomllib.loads(archive.read("META-INF/mods.toml").decode("utf-8")) matching = [mod for mod in metadata.get("mods", []) if mod.get("modId") == pin["id"]] require(len(matching) == 1 and matching[0].get("version") == pin["version"], - name + ": native117 staged mod identity differs from pins") + name + ": " + label + " staged mod identity differs from pins") if sealed: paths = list(runtime.glob("*.jar")) + list((runtime / "libraries").rglob("*")) actual = {path.relative_to(runtime).as_posix(): dict(sha256=sha(evidence.read(path)), size=path.stat().st_size) @@ -362,8 +385,8 @@ def profile_audit(evidence, runtime, name, report, pins, definitions, line): for phase in ("first", "restarted")} result = dict(status="PASS", stations=21, checks=checks, pins=custody, shutdown=shutdown, manualProcedures="NOT_AUTOMATED_BY_BROAD_SWEEP") - if line == "117": - result["native117LogArchives"] = { + if line in ("117", "118"): + result["native" + line + "LogArchives"] = { phase: {kind: log_evidence(evidence, runtime / (kind + "-" + phase + ".log"), line, phase, records[phase]) for kind in ("latest", "debug")} for phase in ("first", "restarted")} diff --git a/test-rig/migration/native117_code.py b/test-rig/migration/native117_code.py index dc02c4bb..30c5cace 100644 --- a/test-rig/migration/native117_code.py +++ b/test-rig/migration/native117_code.py @@ -52,6 +52,8 @@ def close(self): class NativeLaunch(HistoricalServer): + forge_version = "1.17.1-37.1.1" + def validate_native_log(self, path): from native117 import require_clean_log require_clean_log(path) @@ -60,7 +62,7 @@ def __init__(self, runtime, java, launcher, label): runtime = Path(runtime) self.native_label = label self.native_runtime = runtime - logging_jar = runtime / "libraries/net/minecraftforge/fmlloader/1.17.1-37.1.1/fmlloader-1.17.1-37.1.1.jar" + logging_jar = runtime / f"libraries/net/minecraftforge/fmlloader/{self.forge_version}/fmlloader-{self.forge_version}.jar" if not logging_jar.is_file(): raise FileNotFoundError("Missing pinned Forge logging configuration") logging_configuration = "jar:" + logging_jar.resolve().as_uri() + "!/log4j2.xml" self.lines = queue.Queue() diff --git a/test-rig/migration/native117_endurance.py b/test-rig/migration/native117_endurance.py index 003a947a..dd47d25b 100644 --- a/test-rig/migration/native117_endurance.py +++ b/test-rig/migration/native117_endurance.py @@ -96,6 +96,8 @@ def prove_fresh_fixture_before_build(rig, configuration): class NativeEnduranceRig(endurance.Rig): + server_type = Server + def command(self, command, response=None): return super().command(native117_commands.native(command_ids(command)), response) @@ -112,7 +114,7 @@ def batch(self, label): def boot(self, phase): from native117_code import verify_if_sealed verify_if_sealed(self.runtime) - self.server = Server(self.runtime, self.java, self.launcher, phase) + self.server = self.server_type(self.runtime, self.java, self.launcher, phase) if phase != "endurance-restart": return # Restore only chunk tickets, never station actions or machine NBT. diff --git a/test-rig/migration/native117_entity_storage.py b/test-rig/migration/native117_entity_storage.py index 7ad8a690..eec8403f 100644 --- a/test-rig/migration/native117_entity_storage.py +++ b/test-rig/migration/native117_entity_storage.py @@ -154,14 +154,14 @@ def _payloads(data, rx, rz): yield (rx * 32 + (slot & 31), rz * 32 + (slot >> 5)), data[start:start + 4 + length] -def _entity_chunk(payload, coordinates): +def _entity_chunk(payload, coordinates, version=DATA_VERSION): if len(payload) < 6 or int.from_bytes(payload[:4], "big") != len(payload) - 4: raise ValueError("Malformed entity chunk payload") root = _nbt(_decompress(payload[5:], payload[4])) if set(root) != {"DataVersion", "Position", "Entities"}: raise ValueError("Unexpected EntityStorage root keys (terrain Level is not entity storage)") - if not isinstance(root["DataVersion"], nbtlib.Int) or int(root["DataVersion"]) != DATA_VERSION: - raise ValueError("EntityStorage must have native DataVersion 2730") + if not isinstance(root["DataVersion"], nbtlib.Int) or int(root["DataVersion"]) != version: + raise ValueError(f"EntityStorage must have native DataVersion {version}") position = root["Position"] if not isinstance(position, nbtlib.IntArray) or len(position) != 2 or tuple(map(int, position)) != coordinates: raise ValueError("EntityStorage Position int[2] differs from its actual region slot") @@ -202,7 +202,7 @@ def _snapshots(record): return expected, rows, roots -def audit(snapshot_record, world_path, evidence_folder): +def audit(snapshot_record, world_path, evidence_folder, *, data_version=DATA_VERSION): """Retain stopped raw regions and require complete NBT equality for every UUID. world_path is the explicit dimension directory. All its entity regions are @@ -255,7 +255,7 @@ def retain(relative, data): relative = "chunks/%d.%d.chunk" % coordinates retain(relative, payload) receipt["chunks"][relative] = dict(position=list(coordinates), sha256=_sha(payload), source=path.name) - for root in _entity_chunk(payload, coordinates): + for root in _entity_chunk(payload, coordinates, data_version): root_id = _identity(root) if _chunk(root) != coordinates: raise AssertionError("Root entity saved Pos differs from its EntityStorage chunk") diff --git a/test-rig/migration/native117_geometry.py b/test-rig/migration/native117_geometry.py index 74924d6b..d2c44161 100644 --- a/test-rig/migration/native117_geometry.py +++ b/test-rig/migration/native117_geometry.py @@ -44,13 +44,13 @@ def verify_engine_sources(archive): sources=dict(ENGINE_SOURCES)) -def require_geometry(snapshot, forced=False): +def require_geometry(snapshot, forced=False, *, max_distance=MAX_DISTANCE): geometry = snapshot.get("ticketGeometry", {}) expected = dict(fullLevel=FULL_LEVEL, entityTickingLevel=FORCED_LEVEL, - maxDistance=MAX_DISTANCE, lastHolderLevel=LAST_HOLDER_LEVEL, - holderRadius=HOLDER_RADIUS) + maxDistance=max_distance, lastHolderLevel=FULL_LEVEL + max_distance, + holderRadius=FULL_LEVEL + max_distance - FORCED_LEVEL) if any(geometry.get(key) != value for key, value in expected.items()): - raise AssertionError("Live native117 ticket geometry differs from the verified engine: " + str(geometry)) + raise AssertionError("Live ticket geometry differs from the verified engine: " + str(geometry)) levels = geometry.get("observedForcedLevels") if levels not in ([], [FORCED_LEVEL]) or forced and levels != [FORCED_LEVEL]: raise AssertionError("Actual native forced-ticket level was not verified: " + str(levels)) diff --git a/test-rig/migration/native117_harvest.py b/test-rig/migration/native117_harvest.py index eecfa5b4..cf498337 100644 --- a/test-rig/migration/native117_harvest.py +++ b/test-rig/migration/native117_harvest.py @@ -37,10 +37,10 @@ HARVEST_CHUNKS = (74, 63, 75, 64) -def require_checks(document, label): +def require_checks(document, label, *, minecraft="1.17.1"): rows = document.get("checks", []) actual = {row["id"]: row for row in rows} - if (document.get("label") != label or document.get("minecraft") != "1.17.1" + if (document.get("label") != label or document.get("minecraft") != minecraft or actual.keys() != EXPECTED.keys() | {"solar/native-solidity"} or len(rows) != len(EXPECTED) + 1): raise AssertionError("Incomplete native117 harvest witness set") diff --git a/test-rig/migration/native117_helper_contract.py b/test-rig/migration/native117_helper_contract.py index 7e5f7487..4128090a 100644 --- a/test-rig/migration/native117_helper_contract.py +++ b/test-rig/migration/native117_helper_contract.py @@ -103,14 +103,14 @@ def validate(document, contract, lifecycle=None): observationGameTime=lifecycle['gameTime']) -def raw_helpers_absent(files, helpers): +def raw_helpers_absent(files, helpers, *, data_version=native.DATA_VERSION): """Reparse retained regions; no no-save helper may masquerade as saved proof.""" for name, data in files.items(): if '/regions/' not in name: continue rx, rz = map(int, native.REGION_NAME.fullmatch(name.rsplit('/', 1)[1]).groups()) for coordinates, payload in native._payloads(data, rx, rz): - for root in native._entity_chunk(payload, coordinates): + for root in native._entity_chunk(payload, coordinates, data_version): for identity, tag, _ in native._tree(root): if identity in helpers or str(tag['id']) == ID: raise AssertionError('Native no-save helper unexpectedly appears in saved entity storage') diff --git a/test-rig/migration/native117_holder_networks.py b/test-rig/migration/native117_holder_networks.py index 9eadeb77..877e6c5d 100644 --- a/test-rig/migration/native117_holder_networks.py +++ b/test-rig/migration/native117_holder_networks.py @@ -15,13 +15,13 @@ ANCHOR_DISTANCE = 32 -def holder_halo(fixture, group): +def holder_halo(fixture, group, radius=HOLDER_RADIUS): return [[x, z] for x in range(fixture["min"][0] >> 4, (fixture["max"][0] >> 4) + 1) for z in range(fixture["min"][2] >> 4, (fixture["max"][2] >> 4) + 1) - if any(max(abs(x-a), abs(z-b)) <= HOLDER_RADIUS for a, b in fixture["groups"][group])] + if any(max(abs(x-a), abs(z-b)) <= radius for a, b in fixture["groups"][group])] -def linear_fixtures(): +def linear_fixtures(radius=HOLDER_RADIUS): result = [] for index, name in enumerate(("C-STEAM", "C-ELECTRIC", "C-FLUID")): base = 32768 + index * 1024 @@ -31,12 +31,12 @@ def linear_fixtures(): witnesses=[origin, *pipes[:3], sink, *pipes[3:]], pipes=pipes, groups=dict(supply=[[base >> 4, 128]], machine=[[(base >> 4) + ANCHOR_DISTANCE, 128]])) for group in ("supply", "machine"): - f["groups"][group + "halo"] = holder_halo(f, group) + f["groups"][group + "halo"] = holder_halo(f, group, radius) result.append(f) return result -def bridge_fixtures(): +def bridge_fixtures(radius=HOLDER_RADIUS): result = [] for index, name in enumerate(("C-STEAM", "C-ELECTRIC", "C-FLUID")): base = 36864 + index * 1024 @@ -47,13 +47,13 @@ def bridge_fixtures(): f = dict(id=name, nativeTicketNetwork=True, min=[base, 72, 2048], max=[base + 31, 90, 2591], witnesses=[origin, sink, *path], pipes=path, groups=dict(bridge=[[base >> 4, 160]], endpoints=[[base >> 4, 128]])) - f["groups"]["bridgehalo"] = holder_halo(f, "bridge") + f["groups"]["bridgehalo"] = holder_halo(f, "bridge", radius) result.append(f) return result -def extended_fixtures(): - return [f for f in fixture_ids(historical.fixtures()) if "route" in f] + linear_fixtures() +def extended_fixtures(radius=HOLDER_RADIUS): + return [f for f in fixture_ids(historical.fixtures()) if "route" in f] + linear_fixtures(radius) def main(): diff --git a/test-rig/migration/native117_legacy_world.py b/test-rig/migration/native117_legacy_world.py index 917ad0de..7b1dae91 100644 --- a/test-rig/migration/native117_legacy_world.py +++ b/test-rig/migration/native117_legacy_world.py @@ -24,13 +24,13 @@ def read(path): return json.loads(path.read_text(encoding="utf-8")) -def turbine(world, position): +def turbine(world, position, *, data_version=2730): x, _, z = position cx, cz = x // 16, z // 16 rx, rz = cx // 32, cz // 32 region = world / "entities" / f"r.{rx}.{rz}.mca" payloads = dict(_payloads(region.read_bytes(), rx, rz)) - entities = _entity_chunk(payloads[(cx, cz)], (cx, cz)) + entities = _entity_chunk(payloads[(cx, cz)], (cx, cz), data_version) actors = [actor for actor in entities if str(actor["id"]) == "baseelectric:hydroturbine"] if len(actors) != 1: raise AssertionError("Expected one saved hydroelectric turbine") diff --git a/test-rig/migration/native117_lifecycle.py b/test-rig/migration/native117_lifecycle.py index 7f28a16b..3e311d83 100644 --- a/test-rig/migration/native117_lifecycle.py +++ b/test-rig/migration/native117_lifecycle.py @@ -20,7 +20,7 @@ from native117 import stage from native117 import (LAUNCHER, compare_upgraded, output_path, pinned_profile, require_clean_log, require_disjoint) -from native117_geometry import require_geometry +from native117_geometry import MAX_DISTANCE, require_geometry from native117_identity import command_ids, fixture_ids from native117_readiness import await_ready @@ -220,6 +220,15 @@ def unload_event_issues(before, after): class NativeLifecycleRun(lifecycle.LifecycleRun): """Native transport readiness only; inherited fixtures, operations and oracles.""" + minecraft = "1.17.1" + max_ticket_distance = MAX_DISTANCE + require_boot = staticmethod(require_native_boot) + + def open_server(self, label): + verify_if_sealed(self.runtime) + from native117_code import Server + return Server(self.runtime, self.java, self.launcher, label) + def __init__(self, *args): super().__init__(*args) self.observations = [] @@ -233,7 +242,7 @@ def _raw_probe(self, operation, label): if path.exists(): raise FileExistsError("Refusing to overwrite native observation: " + str(path)) result = super().probe(operation, label) - require_geometry(result, forced=operation == "load") + require_geometry(result, forced=operation == "load", max_distance=self.max_ticket_distance) return result def _record(self): @@ -245,7 +254,8 @@ def _round_trip_ready(self, label): for fixture in self.definition) # FULL terrain may be loaded while its entity sections are still pending. # Observe only; the Java probe still owns the first eligible START/END. - return [await_ready(self.server, self.runtime, bounds, f"{label}-chunks-{index}")["label"] + return [await_ready(self.server, self.runtime, bounds, f"{label}-chunks-{index}", + minecraft=self.minecraft)["label"] for index, bounds in enumerate(areas)] def _wait_ready(self, label, initial, reload_before=None): @@ -288,12 +298,10 @@ def _wait_ready(self, label, initial, reload_before=None): raise def boot(self, label): - verify_if_sealed(self.runtime) - from native117_code import Server - self.server = Server(self.runtime, self.java, self.launcher, label) + self.server = self.open_server(label) if label not in ("lifecycle-restart", "midblock-restart"): return - require_native_boot(self.runtime / f"console-{label}.log") + self.require_boot(self.runtime / f"console-{label}.log") before = self._raw_probe("snapshot", label + "-native-unrestored") loaded = self._raw_probe("load", label + "-native-initial") self._wait_ready(label + "-native", loaded, reload_before=before) @@ -400,7 +408,7 @@ def exercise(runner, label, progress, trace_loads=False): """Original sequence/fixtures/oracles, with native observation readiness gates.""" definition = runner.definition runner.boot("lifecycle") - require_native_boot(runner.runtime / "console-lifecycle.log") + runner.require_boot(runner.runtime / "console-lifecycle.log") if trace_loads: runner.probe("trace-loads", "tracing-loads") runner.build() @@ -428,7 +436,7 @@ def exercise(runner, label, progress, trace_loads=False): runner.command("save-all flush") runner.close() runner.boot("lifecycle-restart") - require_native_boot(runner.runtime / "console-lifecycle-restart.log") + runner.require_boot(runner.runtime / "console-lifecycle-restart.log") time.sleep(1) after = runner.probe("snapshot", "after-restart") for fixture in definition: diff --git a/test-rig/migration/native117_oil.py b/test-rig/migration/native117_oil.py index a7cf8ece..2533b6d8 100644 --- a/test-rig/migration/native117_oil.py +++ b/test-rig/migration/native117_oil.py @@ -35,19 +35,19 @@ def read_json(path): return json.loads(path.read_text(encoding="utf-8")) -def fixtures(pins): +def fixtures(pins, *, mineralogy_version=MINERALOGY_VERSION, mineralogy_source=MINERALOGY_SOURCE_COMMIT): definition = deepcopy(oil_transfer.fixtures("basepower")) mineralogy = [jar for jar in pins["jars"] if jar["id"] == "mineralogy"] if mineralogy: - if (len(mineralogy) != 1 or mineralogy[0]["version"] != MINERALOGY_VERSION - or mineralogy[0].get("sourceRef") != MINERALOGY_SOURCE_COMMIT): - raise ValueError("Mineralogy oil lanes require the parent-pinned native 6.1.4.117011 candidate") + if (len(mineralogy) != 1 or mineralogy[0]["version"] != mineralogy_version + or mineralogy[0].get("sourceRef") != mineralogy_source): + raise ValueError("Mineralogy oil lanes require the parent-pinned native candidate") pin = mineralogy[0] if not re.fullmatch(r"[A-Fa-f0-9]{64}", pin.get("sha256") or ""): raise ValueError("Mineralogy artifact SHA-256 is pending") if digest(Path(pin["path"])) != pin["sha256"].upper(): raise ValueError("Mineralogy oil input differs from the exact parent hash") - jar_identity(Path(pin["path"]), "mineralogy", MINERALOGY_VERSION) + jar_identity(Path(pin["path"]), "mineralogy", mineralogy_version) for template in definition[:2]: lane = deepcopy(template) for pos in [lane["min"], lane["max"], lane["source"], lane["drain"], *lane["pipes"], lane["output"]]: @@ -160,6 +160,14 @@ def construction_commands(definition): class NativeOilRun(LifecycleRun): + expected_launcher = LAUNCHER + + def open_server(self, label): + verify_staged(self.runtime, self.pins, self.seed_before) + return Server(self.runtime, self.java, self.launcher, label) + + require_boot = staticmethod(require_native_boot) + def __init__(self, runtime, java, launcher, definition, pins, seed_before): super().__init__(runtime, java, launcher, definition) self.pins, self.seed_before = pins, seed_before @@ -170,16 +178,15 @@ def __init__(self, runtime, java, launcher, definition, pins, seed_before): self.bucket_placements = 0 def boot(self, label): - if self.server is not None or self.launcher != LAUNCHER or self.boots != list(BOOT_LABELS[:len(self.boots)]): + if self.server is not None or self.launcher != self.expected_launcher or self.boots != list(BOOT_LABELS[:len(self.boots)]): raise AssertionError("Invalid native oil process lifecycle") if len(self.boots) >= 3 or label != BOOT_LABELS[len(self.boots)]: raise AssertionError("Only the three ordered native oil boots are permitted") if self.boots and not self.construction_sealed: raise AssertionError("Restart before initial construction was sealed") - verify_staged(self.runtime, self.pins, self.seed_before) - self.server = Server(self.runtime, self.java, self.launcher, label) + self.server = self.open_server(label) self.boots.append(label) - require_native_boot(self.runtime / f"console-{label}.log") + self.require_boot(self.runtime / f"console-{label}.log") def seal_construction(self): if self.pending_construction or self.construction_sealed or len(self.boots) != 1: diff --git a/test-rig/migration/native117_partial.py b/test-rig/migration/native117_partial.py index d80f0d38..c1e53666 100644 --- a/test-rig/migration/native117_partial.py +++ b/test-rig/migration/native117_partial.py @@ -155,7 +155,7 @@ def probe(self, operation, label): def boot(self, label): super().boot(label) - require_native_boot(self.runtime / f"console-{label}.log") + self.require_boot(self.runtime / f"console-{label}.log") if label == "blocked-restart": self.probe("load", "blocked-restart-native-restored") diff --git a/test-rig/migration/native117_readiness.py b/test-rig/migration/native117_readiness.py index 95546501..e1927e5e 100644 --- a/test-rig/migration/native117_readiness.py +++ b/test-rig/migration/native117_readiness.py @@ -21,10 +21,10 @@ def chunk_set(bounds): return {(x, z) for x in range(min_x, max_x + 1) for z in range(min_z, max_z + 1)} -def require_ready(document, bounds, label, dimension="minecraft:overworld"): +def require_ready(document, bounds, label, dimension="minecraft:overworld", minecraft="1.17.1"): expected = chunk_set(bounds) if (document.get("protocol") != PROTOCOL or document.get("status") != "PASS" - or document.get("label") != label or document.get("minecraft") != "1.17.1" + or document.get("label") != label or document.get("minecraft") != minecraft or document.get("dimension") != dimension): raise AssertionError("Missing or failed native readiness receipt") if document.get("bounds") != dict(zip(("minX", "minZ", "maxX", "maxZ"), bounds)): @@ -78,7 +78,7 @@ def states(rows): return document -def await_ready(server, runtime, bounds, label, dimension="minecraft:overworld"): +def await_ready(server, runtime, bounds, label, dimension="minecraft:overworld", minecraft="1.17.1"): chunk_set(bounds) if not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", label): raise AssertionError("Unsafe native readiness label") @@ -87,6 +87,6 @@ def await_ready(server, runtime, bounds, label, dimension="minecraft:overworld") server.send("advreadiness chunks " + " ".join(map(str, bounds)) + " " + label) server.wait("ADVREADINESS RESULT " + label + " status=", 150) document = json.loads(path.read_text(encoding="utf-8")) - require_ready(document, bounds, label, dimension) + require_ready(document, bounds, label, dimension, minecraft) server.wait("ADVREADINESS COMPLETE " + label, 10) return document diff --git a/test-rig/migration/native117_recipes.py b/test-rig/migration/native117_recipes.py index f9eca6cd..74d28648 100644 --- a/test-rig/migration/native117_recipes.py +++ b/test-rig/migration/native117_recipes.py @@ -148,7 +148,7 @@ def validate_unlock(result, definition, operation): "skipped": sum(row["status"] != "PASS" for row in rows), "potatoCraftingCases": 59} -def validate_progression(result, mode, rubber): +def validate_progression(result, mode, rubber, *, verify_global_loot=False): validate_tags(result, mode, rubber, power="basepower", electric="baseelectric") if result["mode"] != mode or result["plasticEqualsRubber"] != rubber: raise AssertionError("Native progression ignored its requested mode/rubber configuration") @@ -159,28 +159,30 @@ def validate_progression(result, mode, rubber): raise AssertionError("Empty native manufacture evidence") if not result["lootDataPackOverrideVerified"]: raise AssertionError("Player loot override was not preserved") + if verify_global_loot and result.get("lootOverrideWithModifiersVerified") is not True: + raise AssertionError("Missing gameplay loot-modifier observation") -def verify_level(runtime, entry): +def verify_level(runtime, entry, minecraft="1.17.1", forge="37.1.1"): level_path = runtime / WORLD / "level.dat" level = nbtlib.load(level_path) - if str(level["Data"]["Version"]["Name"]) != "1.17.1": - raise AssertionError("Server did not write a native 1.17.1 level header") + if str(level["Data"]["Version"]["Name"]) != minecraft: + raise AssertionError(f"Server did not write a native {minecraft} level header") mods = {str(row["ModId"]): str(row["ModVersion"]) for row in level["fml"]["LoadingModList"]} - if mods.get("forge") != "37.1.1" or any(mods.get(jar["id"]) != jar["version"] for jar in entry["jars"]): + if mods.get("forge") != forge or any(mods.get(jar["id"]) != jar["version"] for jar in entry["jars"]): raise AssertionError(f"Saved native mod identities differ from the target manifest: {mods}") registry = level["fml"]["Registries"] if any(not registry.get(name, {}).get("ids") for name in ("minecraft:item", "minecraft:block")): raise AssertionError("Missing native item/block registry snapshot; legacy plural registry names are not valid") - return {"path": str(level_path), "sha256": digest(level_path), "minecraft": "1.17.1", "mods": mods} + return {"path": str(level_path), "sha256": digest(level_path), "minecraft": minecraft, "mods": mods} -def audit_unlock_log(path, previous_recipes, current_recipes): +def audit_unlock_log(path, previous_recipes, current_recipes, errors=runtime_error_lines): """Witness vanilla's exact mode-conditional saved-book pruning, not arbitrary errors.""" removed = set(previous_recipes) - set(current_recipes) observed = set() events = [] - for original in runtime_error_lines(path.read_text(encoding="utf-8", errors="replace")): + for original in errors(path.read_text(encoding="utf-8", errors="replace")): line = re.sub(r"\x1b\[[0-9;]*m", "", original) match = re.search(r"\[Server thread/ERROR\] \[(?:minecraft/ServerRecipeBook|net\.minecraft\.stats\.ServerRecipeBook/?)\]: Tried to load unrecognized recipe: ([a-z0-9_]+:[a-z0-9_./-]+) removed now\.$", line) if not match or match[1] not in removed: @@ -192,7 +194,9 @@ def audit_unlock_log(path, previous_recipes, current_recipes): return {"expected_removed_ids": sorted(removed), "observed_removed_ids": sorted(observed), "events": events} -class RecipeServer(Server): +class RecipePruning: + errors = staticmethod(runtime_error_lines) + def __init__(self, runtime, java, launcher, label, previous): self.previous = set(previous) self.current = None @@ -202,21 +206,29 @@ def __init__(self, runtime, java, launcher, label, previous): def validate_native_log(self, path): if self.current is None: return super().validate_native_log(path) - self.pruning[path.name] = audit_unlock_log(path, self.previous, self.current) + self.pruning[path.name] = audit_unlock_log(path, self.previous, self.current, self.errors) + +class RecipeServer(RecipePruning, Server): + pass -def unlock_campaign(root, target, entry, java, port, definition, report): + +def unlock_campaign(root, target, entry, java, port, definition, report, *, engine=None): + stage_runtime = stage_native if engine is None else engine.stage + server_type = RecipeServer if engine is None else engine.recipe_server + version = ("1.17.1", "37.1.1") if engine is None else engine.version + result_name = "native117-recipes-results.json" if engine is None else engine.result_name runtime = root / "unlock" - launcher = stage_native(runtime, target, port, "unlock") + launcher = stage_runtime(runtime, target, port, "unlock") write_json(runtime / "recipe-fixture.json", definition) previous_recipes = set() for mode, label, operations in UNLOCK_BOOTS: configure(runtime, mode) row = {"mode": mode, "label": label, "operations": [], "status": "RUNNING"} report["unlock_boots"].append(row) - write_json(root / "native117-recipes-results.json", report) - print(f"Native117 unlock boot {label}: {', '.join(operations)}", flush=True) - server = RecipeServer(runtime, java, launcher, label, previous_recipes) + write_json(root / result_name, report) + print(f"{version[0]} unlock boot {label}: {', '.join(operations)}", flush=True) + server = server_type(runtime, java, launcher, label, previous_recipes) try: for operation in operations: name = f"{label}-{operation}" @@ -238,15 +250,15 @@ def unlock_campaign(root, target, entry, java, port, definition, report): raise AssertionError("Recipe pruning was not verified in all three native logs") row["vanilla_recipe_pruning"] = server.pruning previous_recipes = current_recipes - verify_level(runtime, entry) + verify_level(runtime, entry, *version) row["status"] = "PASS" artifacts = [runtime / f"recipes-{mode}-sweep.{extension}" for mode in ("normal", "tech", "apocalyptic") for extension in ("bin", "json")] - report["recipe_fixture"] = {"directory": str(runtime), "registry": verify_level(runtime, entry), + report["recipe_fixture"] = {"directory": str(runtime), "registry": verify_level(runtime, entry, *version), "files": [{"file": path.name, "sha256": digest(path)} for path in artifacts]} -def prepare_progression(server, runtime, bounds, label, reload): +def prepare_progression(server, runtime, bounds, label, reload, minecraft="1.17.1"): if reload: server.send("reload") server.wait("Reloading!", 60) @@ -255,17 +267,22 @@ def prepare_progression(server, runtime, bounds, label, reload): server.send(f"forceload add {low[0]} {low[2]} {high[0]} {high[2]}") chunks = (low[0] // 16, low[2] // 16, high[0] // 16, high[2] // 16) readiness = "progression-" + label - await_ready(server, runtime, chunks, readiness) + await_ready(server, runtime, chunks, readiness, minecraft=minecraft) return runtime / "advantage-works-results" / f"native-readiness-{readiness}.json" -def progression_campaign(root, target, entry, java, port, report): +def progression_campaign(root, target, entry, java, port, report, *, engine=None): + stage_runtime = stage_native if engine is None else engine.stage + server_type = Server if engine is None else engine.server + version = ("1.17.1", "37.1.1") if engine is None else engine.version + result_name = "native117-recipes-results.json" if engine is None else engine.result_name + pack_format = 7 if engine is None else engine.pack_format runtime = root / "progression" - launcher = stage_native(runtime, target, port, "progression") + launcher = stage_runtime(runtime, target, port, "progression") definition = fixture_ids(progression_fixture()) write_json(runtime / "progression-fixture.json", definition) pack = runtime / WORLD / "datapacks/progression-override" - write_json(pack / "pack.mcmeta", {"pack": {"pack_format": 7, "description": "Disposable loot-override witness"}}) + write_json(pack / "pack.mcmeta", {"pack": {"pack_format": pack_format, "description": "Disposable loot-override witness"}}) write_json(pack / "data/minecraft/loot_tables/chests/simple_dungeon.json", { "pools": [{"name": "test_override", "rolls": 1, "entries": [{"type": "item", "name": "minecraft:dirt"}]}]}) for mode in MODES: @@ -273,28 +290,28 @@ def progression_campaign(root, target, entry, java, port, report): configure(runtime, mode, rubber) for phase in ("initial", "restart"): label = mode.lower() + ("-rubber" if rubber else "-no-rubber") + "-" + phase - print(f"Native117 progression boot {label}", flush=True) - server = Server(runtime, java, launcher, label) + print(f"{version[0]} progression boot {label}", flush=True) + server = server_type(runtime, java, launcher, label) try: for reload in ((False, True) if phase == "initial" else (False,)): name = label + ("-reload" if reload else "") row = {"mode": mode, "rubber": rubber, "phase": "reload" if reload else phase, "report": f"progression-{name}.json", "status": "RUNNING"} report["progression_cases"].append(row) - write_json(root / "native117-recipes-results.json", report) - readiness = prepare_progression(server, runtime, definition["machineBounds"], name, reload) + write_json(root / result_name, report) + readiness = prepare_progression(server, runtime, definition["machineBounds"], name, reload, version[0]) row["readiness"] = {"file": str(readiness.relative_to(runtime)), "sha256": digest(readiness)} server.send(f"advprogression {name}") server.wait(f"ADVPROGRESSION COMPLETE {name}", 240) result = read_json(runtime / row["report"]) - validate_progression(result, mode, rubber) + validate_progression(result, mode, rubber, verify_global_loot=engine is not None) row.update(status="PASS", targets=len(result["targets"]), witnesses=len(result["processWitnesses"]), blocked=result["blocked"]) server.send("save-all flush") finally: server.close() require_clean_log(runtime / f"console-{label}.log") - verify_level(runtime, entry) + verify_level(runtime, entry, *version) def run(args): diff --git a/test-rig/migration/native117_respawn.py b/test-rig/migration/native117_respawn.py index a1c184f7..1f363250 100644 --- a/test-rig/migration/native117_respawn.py +++ b/test-rig/migration/native117_respawn.py @@ -132,11 +132,15 @@ def native_respawn_verdict(record, registry, world_spawn): return respawn_verdict(value, registry, world_spawn) -def run(args): +def run(args, *, target=None, stage=None, server_type=None, check_log=None, approved_sources=None): from native117 import boot + server_type = server_type or Server + check_log = check_log or require_clean_log + java = getattr(args, "java17", None) or args.java8 root = output_path(args.output) require_disjoint(root, args.fixtures) - target = pinned_profile(args.target_manifest.resolve(), "target117") + if target is None: + target = pinned_profile(args.target_manifest.resolve(), "target117") with socket.socket() as port: port.bind(("127.0.0.1", args.port)) root.mkdir(parents=True) @@ -147,11 +151,13 @@ def run(args): for route in args.routes: source = args.fixtures / route before = fingerprint(source) + if approved_sources is not None and before != approved_sources[route]: + raise AssertionError("Protected respawn source differs from its prior receipt") sources[route] = {"path": str(source.resolve()), "fingerprint": before, "baseFixtureSha256": digest(source / "dimension-fixture.json")} write_json(root / "source-fingerprints.json", sources) runtime = root / route - launcher = boot(runtime, target, args.java8, args.port) + launcher = stage(runtime, target, args.port) if stage else boot(runtime, target, java, args.port) copy_fixture(source, runtime) shutil.copy2(source / "dimension-fixture.json", runtime / "dimension-fixture.json") props = runtime / "server.properties" @@ -166,7 +172,7 @@ def run(args): migrated_player = None try: for boot_label in ("respawn", "respawn-restart"): - server = Server(runtime, args.java8, launcher, boot_label) + server = server_type(runtime, java, launcher, boot_label) try: if migrated_player is None: # Let the real pre-datafix world hook migrate the copy once. @@ -227,7 +233,7 @@ def run(args): print(route + "/" + label + "=" + verdict["status"], flush=True) finally: server.close() - require_clean_log(runtime / ("console-" + boot_label + ".log")) + check_log(runtime / ("console-" + boot_label + ".log")) verify_request_files(runtime, CASE_LABELS) finally: unchanged = fingerprint(source) == before diff --git a/test-rig/migration/native117_stopped_entities.py b/test-rig/migration/native117_stopped_entities.py index 8b720f51..67455048 100644 --- a/test-rig/migration/native117_stopped_entities.py +++ b/test-rig/migration/native117_stopped_entities.py @@ -29,7 +29,8 @@ def _unique_object(pairs): return result -def audit_stopped(runtime, evidence_folder): +def audit_stopped(runtime, evidence_folder, *, minecraft_version="1.17.1", data_version=2730, + stopping_phase="FMLServerStoppingEvent"): """Call after clean process shutdown; retain a NEW immutable snapshot copy. Empty dimensions are explicit skips, not inferred-empty saves. An all-empty @@ -57,9 +58,9 @@ def audit_stopped(runtime, evidence_folder): output.write(data) document = json.loads(data, object_pairs_hook=_unique_object) if (not isinstance(document, dict) or type(document.get("format")) is not int or document["format"] not in (1, 2) - or document.get("status") != "CAPTURED" or document.get("phase") != "FMLServerStoppingEvent" - or document.get("minecraftVersion") != "1.17.1" or type(document.get("dataVersion")) is not int - or document["dataVersion"] != 2730 or document.get("worldName") != "AdvantageWorksMigration" + or document.get("status") != "CAPTURED" or document.get("phase") != stopping_phase + or document.get("minecraftVersion") != minecraft_version or type(document.get("dataVersion")) is not int + or document["dataVersion"] != data_version or document.get("worldName") != "AdvantageWorksMigration" or document.get("enumeration") != "ServerLevel.getAllEntities"): raise ValueError("Not a complete native stopping-event snapshot") if str(uuid.UUID(document.get("bootId", ""))) != document["bootId"]: @@ -107,7 +108,8 @@ def audit_stopped(runtime, evidence_folder): _, native_rows, _ = entity_storage._snapshots(record) if any(entry.get("id") != str(native_rows[entry["uuid"]]["id"]) for entry in record["entities"]): raise AssertionError("Stopping entity type attestation differs from its complete native NBT") - result = entity_storage.audit(record, world / directory, evidence / (directory or "overworld")) + result = entity_storage.audit(record, world / directory, evidence / (directory or "overworld"), + data_version=data_version) if result.get("status") != "PASS": raise AssertionError("Raw entity audit did not pass: " + key) identities = set(result["matched"]) @@ -123,7 +125,8 @@ def audit_stopped(runtime, evidence_folder): if document['format'] == 2: from native117_helper_contract import raw_helpers_absent raw_helpers_absent({p.relative_to(evidence).as_posix(): p.read_bytes() - for p in evidence.rglob('*.mca')}, receipt['helperLifecycle']['helpers']) + for p in evidence.rglob('*.mca')}, receipt['helperLifecycle']['helpers'], + data_version=data_version) receipt.update(status="PASS" if audited else "NO_NONEMPTY_DIMENSIONS", bootId=document["bootId"], auditedDimensions=audited, snapshotRecords=count, matchedEntities=len(matched)) except Exception as error: diff --git a/test-rig/migration/native118.py b/test-rig/migration/native118.py new file mode 100644 index 00000000..f5d2ab5f --- /dev/null +++ b/test-rig/migration/native118.py @@ -0,0 +1,155 @@ +"""Run the 1.18.2 destination with exact, disposable server inputs.""" + +import json +import os +import re +from pathlib import Path + +from matrix import digest, stage as stage_server, write_json +from native117 import jar_identity, require_disjoint, runtime_error_lines +from native117_code import NativeLaunch, code_files + + +MINECRAFT = "1.18.2" +FORGE = "40.3.0" +FORGE_VERSION = f"{MINECRAFT}-{FORGE}" +MCP = "20220404.173914" +RIG = Path(__file__).resolve().parents[1] +LAUNCHER = f"libraries/net/minecraftforge/forge/{FORGE_VERSION}/" + ( + "win_args.txt" if os.name == "nt" else "unix_args.txt") +INVENTORY = "native118-code-inventory.json" +NETTY_PROBES = { + "direct buffer constructor: unavailable": "java.lang.UnsupportedOperationException: Reflective setAccessible(true) disabled", + "jdk.internal.misc.Unsafe.allocateUninitializedArray(int): unavailable": ( + "java.lang.IllegalAccessException: class io.netty.util.internal.PlatformDependent0$6"), +} + + +def log_findings(text): + """Separate Netty's DEBUG capability probes from runtime exceptions.""" + failures, diagnostics = [], [] + current_probe = None + for line in text.splitlines(): + header = re.fullmatch(r"\[[^\]]+\] \[[^\]]+/DEBUG\] " + r"\[io\.netty\.util\.internal\.PlatformDependent0/\]: (.*)", line) + if header and header[1] in NETTY_PROBES: + current_probe = header[1] + elif current_probe in NETTY_PROBES and line.startswith(NETTY_PROBES[current_probe]): + diagnostics.append({"probe": current_probe, "detail": line}) + current_probe = "frames" + elif current_probe == "frames" and line.startswith("\tat "): + continue + else: + current_probe = None + failures.extend(runtime_error_lines(line)) + return failures, diagnostics + + +def require_clean_log(path): + failures, diagnostics = log_findings(path.read_text(encoding="utf-8", errors="replace")) + if failures: + raise AssertionError(f"Native runtime errors in {path}: {failures[:5]}") + return diagnostics + + +def require_boot_identity(path): + text = path.read_text(encoding="utf-8", errors="replace") + for marker in (f"Starting minecraft server version {MINECRAFT}", + f"Forge mod loading, version {FORGE}, for MC {MINECRAFT} with MCP {MCP}", "Done ("): + if marker not in text: + raise AssertionError("Missing native 1.18.2 startup witness: " + marker) + + +def require_native_boot(path): + require_clean_log(path) + require_boot_identity(path) + + +def seal(seed, receipt): + if receipt.exists() or not (seed / LAUNCHER).is_file(): + raise ValueError("Supply an installed 1.18.2 server and a new inventory path") + require_disjoint(receipt, seed) + write_json(receipt, {"forge": FORGE_VERSION, "files": code_files(seed)}) + + +def read_target(profile, seed, inventory, inventory_hash): + seed = seed.resolve() + data = json.loads(profile.read_text(encoding="utf-8")) + if (data.get("minecraftVersion") != MINECRAFT or data.get("forge", {}).get("version") != FORGE_VERSION + or data.get("mode") != "packaged" or data.get("portStatus") != "PORTED_UNQUALIFIED" + or data.get("qualificationStatus") != "UNQUALIFIED"): + raise ValueError("Use a pinned, unqualified Forge 40.3.0 destination profile") + if digest(inventory) != inventory_hash.upper(): + raise AssertionError("Forge server inventory hash changed") + code = json.loads(inventory.read_text(encoding="utf-8")) + if code.get("forge") != FORGE_VERSION or code_files(seed) != code.get("files"): + raise AssertionError("Forge server does not match its sealed code") + entries = {} + for mod in data["mods"]: + jar = (RIG / mod["path"]).resolve() + identity = mod["id"] + if (identity in entries or not mod["version"].endswith(".118021") + or mod["file"] != jar.name or "deobf" in jar.name + or not re.fullmatch("[A-Fa-f0-9]{64}", mod["sha256"])): + raise ValueError(f"Invalid packaged 1.18.2 mod pin: {identity}") + if digest(jar) != mod["sha256"].upper(): + raise AssertionError(f"Pinned mod changed: {jar}") + jar_identity(jar, identity, mod["version"]) + entries[identity] = dict(mod, path=jar) + if not {"basepower", "basesteam", "baseelectric", "orespawn", "advantageworkstest"} <= entries.keys(): + raise ValueError("The combined test requires the three base mods, OreSpawn and Advantage Works") + return seed, code, entries + + +def stage(runtime, target, port, *, normal=False): + seed, code, entries = target + for protected in (seed, *(entry["path"] for entry in entries.values())): + require_disjoint(runtime, protected) + expected = {entry["file"]: entry["sha256"].upper() for entry in entries.values()} + if {entry["file"]: digest(entry["path"]) for entry in entries.values()} != expected: + raise AssertionError("Pinned mod jar changed before staging") + launcher = stage_server(runtime, (seed, LAUNCHER, [entry["path"] for entry in entries.values()])) + staged = json.loads((runtime / "input-jars.json").read_text(encoding="utf-8")) + if {entry["file"]: entry["sha256"] for entry in staged} != expected: + raise AssertionError("Pinned mod jar changed during staging") + write_json(runtime / INVENTORY, code) + path = runtime / "server.properties" + overrides = {"level-type": "default" if normal else "advantageworkstest:test_flat", + "generator-settings": "", "server-port": str(port), "generate-structures": "false", + "simulation-distance": "4", "allow-nether": "true"} + lines = [line for line in path.read_text().splitlines() if line.partition("=")[0] not in overrides] + path.write_text("\n".join(lines) + "\n" + "".join(f"{k}={v}\n" for k, v in overrides.items())) + verify_runtime(runtime) + return launcher + + +def verify_runtime(runtime): + code = json.loads((runtime / INVENTORY).read_text(encoding="utf-8")) + if code_files(runtime) != code["files"]: + raise AssertionError("Staged server code changed") + entries = json.loads((runtime / "input-jars.json").read_text(encoding="utf-8")) + if {f.name: digest(f) for f in (runtime / "mods").glob("*.jar")} != { + item["file"]: item["sha256"] for item in entries}: + raise AssertionError("Staged mod jars changed") + + +class Server(NativeLaunch): + forge_version = FORGE_VERSION + + def __init__(self, runtime, java, launcher, label): + if launcher != LAUNCHER: + raise ValueError("Wrong server launch target") + verify_runtime(runtime) + super().__init__(runtime, java, launcher, label) + + def validate_native_log(self, path): + diagnostics = require_clean_log(path) + if diagnostics: + write_json(path.with_suffix(".platform-diagnostics.json"), diagnostics) + + def close(self): + try: + super().close() + require_boot_identity(self.native_runtime / f"console-{self.native_label}.log") + finally: + verify_runtime(self.native_runtime) diff --git a/test-rig/migration/native118_actors.py b/test-rig/migration/native118_actors.py new file mode 100644 index 00000000..37aee05c --- /dev/null +++ b/test-rig/migration/native118_actors.py @@ -0,0 +1,65 @@ +"""Check retained player and dimension fixtures on the pinned Forge 40 server.""" + +import argparse +from functools import partial +from pathlib import Path +import socket +import traceback + +from matrix import digest, fingerprint, write_json +from native117 import output_path, require_disjoint +from native117_actors import run_route +from native117_broad import read_json +from native117_stopped_entities import audit_stopped +import native118 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("fixture", "fixture-receipt", "profile", "seed", "inventory", "java17", "output"): + parser.add_argument("--" + name, type=Path, required=True) + parser.add_argument("--fixture-receipt-sha256", required=True) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--kind", choices=("actors", "dimensions"), required=True) + parser.add_argument("--port", type=int, required=True) + args = parser.parse_args() + output = output_path(args.output) + source = args.fixture.resolve() + for path in (source, args.fixture_receipt, args.profile, args.seed, args.inventory): + require_disjoint(output, path) + if digest(args.fixture_receipt) != args.fixture_receipt_sha256.upper(): + raise AssertionError("Historical fixture receipt has changed") + approved = read_json(args.fixture_receipt) + before = fingerprint(source) + if Path(approved["source"]).resolve() != source or approved["sourceFingerprint"] != before: + raise AssertionError("Historical fixture no longer matches its approved source") + target = native118.read_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + with socket.socket() as port: + port.bind(("127.0.0.1", args.port)) + output.mkdir() + result = dict(status="RUNNING", kind=args.kind, source=str(source), realClientLogin="NOT_TESTED", + fixtureReceiptSha256=digest(args.fixture_receipt), profileSha256=digest(args.profile), + inventorySha256=digest(args.inventory), checks={}) + report = output / "native118-actors-results.json" + write_json(report, result) + try: + args.target_manifest = args.profile + result["checks"] = run_route( + source, output / "server", target, args, stage=native118.stage, server_type=native118.Server, + stop_audit=partial(audit_stopped, minecraft_version=native118.MINECRAFT, data_version=2975, + stopping_phase="ServerStoppingEvent"), + check_log=native118.require_native_boot, minecraft=native118.MINECRAFT) + result["status"] = "PASS" if all(c["status"] == "PASS" for c in result["checks"].values()) else "FAIL" + except Exception: + result.update(status="FAIL", error=traceback.format_exc()) + raise + finally: + result["sourceUnchanged"] = fingerprint(source) == before + if not result["sourceUnchanged"]: + result["status"] = "FAIL" + write_json(report, result) + return int(result["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native118_broad.py b/test-rig/migration/native118_broad.py new file mode 100644 index 00000000..770ad371 --- /dev/null +++ b/test-rig/migration/native118_broad.py @@ -0,0 +1,79 @@ +"""Commission and restart all 21 Works stations with the pinned 1.18.2 jars.""" + +import argparse +from pathlib import Path +import shutil +import traceback + +from matrix import digest, write_json +from native117 import output_path, require_disjoint +from native117_broad import campaign_document, read_json, run_profile +import native118 + + +def freeze_mods(entries, destination): + destination.mkdir() + frozen = {} + for mod, entry in entries.items(): + original = entry["path"] + jar = destination / entry["file"] + shutil.copy2(original, jar) + if digest(jar) != entry["sha256"].upper() or digest(original) != entry["sha256"].upper(): + raise AssertionError("A pinned mod changed while freezing broad test inputs") + frozen[mod] = dict(entry, path=jar, originPath=str(original)) + return frozen + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("profile", "seed", "inventory", "output", "java17"): + parser.add_argument("--" + name, required=True, type=Path) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--port", type=int, default=25642) + parser.add_argument("--run-seconds", type=int, default=8) + args = parser.parse_args() + if not 1 <= args.run_seconds <= 60 or not 1024 <= args.port < 65535: + parser.error("Bounded run duration and unprivileged consecutive ports required") + output = output_path(args.output) + target = native118.read_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + if "basemetals" not in target[2]: + raise ValueError("Use the complete profile; the decoupled profile is derived from it") + for path in (args.profile, args.seed, args.inventory, *(item["path"] for item in target[2].values())): + require_disjoint(output, path) + definition = native118.RIG / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json" + stations = read_json(definition)["stations"] + if len(stations) != 21: + raise AssertionError("The complete 21-bay specification is required") + output.mkdir(parents=True) + manifest = output / "target118.json" + seed, code, entries = target + entries = freeze_mods(entries, output / "pinned-inputs") + target = seed, code, entries + write_json(manifest, {"target118": { + "seed": str(seed), "launcher": native118.LAUNCHER, + "launcher_sha256": digest(seed / native118.LAUNCHER), "serverCode": code, + "jars": [{**item, "path": str(item["path"])} for item in entries.values()], + }}) + runner = output / "runner-source.py" + shutil.copy2(Path(__file__), runner) + proof = dict(profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), + stationDefinitionSha256=digest(definition), runnerSha256=digest(runner), + targetManifest=str(manifest), targetManifestSha256=digest(manifest), + minecraft=native118.MINECRAFT) + names = ("packaged-current", "packaged-decoupled") + results = {} + report = output / "native118-broad-results.json" + for index, name in enumerate(names): + write_json(report, campaign_document(names, results, proof, finished=False)) + try: + results[name] = run_profile(args, None, output, name, args.port + index, stations, native118_target=target) + except Exception: + results[name] = dict(status="FAIL", profile=name, error=traceback.format_exc()) + print(name, results[name]["status"], flush=True) + final = campaign_document(names, results, proof, finished=True) + write_json(report, final) + return int(final["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native118_endurance.py b/test-rig/migration/native118_endurance.py new file mode 100644 index 00000000..674df303 --- /dev/null +++ b/test-rig/migration/native118_endurance.py @@ -0,0 +1,150 @@ +"""Run the measured Works feed, conservation and cold-restart protocol on 1.18.2.""" + +import argparse +from collections import Counter +from pathlib import Path +import shutil +import time + +import endurance +from matrix import WORLD, compare, digest, fingerprint, write_json +from native117 import native_normalized, output_path, require_disjoint +from native117_broad import read_json +from native117_endurance import NativeEnduranceRig, configure_new_fresh_fixture, prove_fresh_fixture_before_build +import native118 + + +class Rig(NativeEnduranceRig): + server_type = native118.Server + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("profile", "seed", "inventory", "output", "java17"): + parser.add_argument("--" + name, required=True, type=Path) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--seconds", type=int, default=1800) + parser.add_argument("--retained", type=Path) + parser.add_argument("--port", type=int, default=25658) + args = parser.parse_args() + endurance.require(args.seconds >= 60 and args.seconds % 60 == 0, "Use whole one-minute batches") + root = output_path(args.output) + target = native118.read_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + for protected in (args.profile, args.seed, args.inventory, *(item["path"] for item in target[2].values())): + require_disjoint(root, protected) + source_before = None + if args.retained: + require_disjoint(root, args.retained) + prior = read_json(args.retained / "native118-legacy-world-results.json") + if (prior["status"] != "PASS" or prior["profileSha256"] != digest(args.profile) + or prior["inventorySha256"] != digest(args.inventory) + or digest(args.retained / "migration-restarted.json") != prior["phases"]["restarted"]["snapshotSha256"]): + raise AssertionError("Retained input does not match the passing pinned migration") + source_before = fingerprint(args.retained / WORLD) + spec = native118.RIG / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json" + definitions = {row["id"]: row for row in read_json(spec)["stations"]} + report = dict(status="RUNNING", minecraft=native118.MINECRAFT, duration_seconds=args.seconds, + interval_seconds=60, profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), + stationDefinitionSha256=digest(spec), active_stations=endurance.ACTIVE, + checked_stations=endurance.CHECKED, profiles={}, retained=str(args.retained) if args.retained else None) + receipt = root / "native118-endurance-results.json" + write_json(receipt, report) + rigs = [] + try: + cases = (("fresh118", None), ("retained-upgrade-native118", args.retained)) if args.retained else (("fresh118", None),) + for index, (name, source) in enumerate(cases): + runtime = root / name + launcher = native118.stage(runtime, target, args.port + index) + rig = Rig(name, runtime, args.java17, launcher, definitions) + rigs.append(rig) + if source: + for path in (WORLD, "config"): + shutil.copytree(source / path, runtime / path) + shutil.copy2(source / "migration-fixture.json", runtime / "migration-fixture.json") + if fingerprint(runtime / WORLD) != source_before: + raise AssertionError("The retained fixture copy differs from the source") + rig.boot("retained-before-build") + rig.server.snapshot("retained-before-build") + rig.shutdown() + preservation = compare(source / "migration-restarted.json", + runtime / "migration-retained-before-build.json", native_normalized) + write_json(runtime / "retained-comparison.json", preservation) + endurance.require(preservation["status"] == "PASS", "Retained fixture changed before operation") + else: + creation = configure_new_fresh_fixture(runtime) + (runtime / "advantage-works.allow").touch() + (runtime / "migration-operate.allow").touch() + rig.boot("endurance") + if source is None: + report["fresh_fixture_proof"] = prove_fresh_fixture_before_build(rig, creation) + rig.start(build=True) + initial = {rig.name: rig.documents["W-01"]["worldTime"] for rig in rigs} + started = time.monotonic() + cycles = args.seconds // 60 + for cycle in range(1, cycles + 1): + deadline = started + cycle * 60 + while time.monotonic() < deadline: + for rig in rigs: + endurance.require(rig.server.process.poll() is None, rig.name + ": server died") + time.sleep(min(1, max(0, deadline - time.monotonic()))) + for rig in rigs: + label = f"cycle-{cycle:03}" + rig.checkpoint(label) + rig.batch(label) + if cycle < cycles: + rig.seed() + report["profiles"][rig.name] = dict(status="RUNNING", completed_batches=len(rig.ledger), + latest_world_time=rig.documents["W-01"]["worldTime"]) + write_json(receipt, report) + print(f"{cycle * 60}s: all requested profiles passed batch {cycle}", flush=True) + elapsed = time.monotonic() - started + ticks = {rig.name: rig.documents["W-01"]["worldTime"] - initial[rig.name] for rig in rigs} + for rig in rigs: + endurance.require(ticks[rig.name] >= args.seconds * 18, rig.name + ": fewer than 18 game ticks per second") + rig.structure() + rig.checkpoint("before-restart") + rig.batch("before-restart") + rig.command("save-all flush", "Saved the game") + rig.shutdown() + rig.boot("endurance-restart") + rig.checkpoint("after-restart") + rig.batch("after-restart") + rig.structure() + rig.seed() + time.sleep(60) + for rig in rigs: + rig.checkpoint("restart-resumed") + rig.batch("restart-resumed") + statuses = rig.structure() + rig.command("save-all flush", "Saved the game") + rig.shutdown() + totals = Counter() + for batch in rig.ledger: + if batch["label"].startswith("cycle-"): + totals.update(batch["outputs"]) + report["profiles"][rig.name] = dict(status="PASS", elapsed_seconds=elapsed, endurance_batches=cycles, + game_ticks=ticks[rig.name], verified_checkpoints=len(rig.ledger), + endurance_outputs=dict(totals), station_statuses=statuses, + restart="PASS", resumed_processing="PASS") + report["status"] = "PASS" if args.seconds >= 1800 else "SMOKE_PASS_NOT_ENDURANCE" + except BaseException as error: + report.update(status="FAIL", failure=repr(error)) + raise + finally: + for rig in rigs: + try: + rig.shutdown() + native118.verify_runtime(rig.runtime) + except Exception as error: + report.update(status="FAIL", shutdown_or_audit_error=repr(error)) + if args.retained: + report["retained_unchanged"] = fingerprint(args.retained / WORLD) == source_before + if not report["retained_unchanged"]: + report["status"] = "FAIL" + write_json(receipt, report) + print(report["status"] + ": " + str(receipt), flush=True) + return int(report["status"] == "FAIL") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native118_legacy_world.py b/test-rig/migration/native118_legacy_world.py new file mode 100644 index 00000000..3fbbe991 --- /dev/null +++ b/test-rig/migration/native118_legacy_world.py @@ -0,0 +1,136 @@ +"""Compare original and released legacy fixtures against live and saved 1.18.2 state.""" + +import argparse +from pathlib import Path +import shutil +import sys +import time + +from matrix import WORLD, compare, digest, fingerprint, parse_nbt, write_json +from native117 import native_normalized, output_path, require_disjoint +from native117_broad import read_json +from native117_commands import native +from native117_legacy_world import turbine +import native118 +import native118_saved + + +def portable(hashes): + return {key.replace("\\", "/"): value.upper() for key, value in hashes.items()} + + +def operate(runtime, frozen, target, java, port, positions, expected): + launcher = native118.stage(runtime, target, port) + for path in (WORLD, "config"): + shutil.copytree(frozen / path, runtime / path) + shutil.copy2(frozen / "migration-fixture.json", runtime / "migration-fixture.json") + (runtime / "migration-operate.allow").touch() + jobs = (("sourceChest", "destination", "dirt", "dirt", 8), + ("steamCrusher", "steamCrusher", "stone", "cobblestone", 1), + ("electricFurnace", "electricFurnace", "iron_ore", "iron_ingot", 1)) + server = native118.Server(runtime, java, launcher, "operation") + try: + for source, _, item, _, count in jobs: + x, y, z = positions[source] + server.send(native(f"replaceitem block {x} {y} {z} container.0 minecraft:{item} {count}")) + server.wait("Replaced a slot", 30) + time.sleep(30) + server.snapshot("operation") + server.send("save-all flush") + server.wait("Saved the game", 30) + finally: + server.close() + rows = {row["name"]: row for row in read_json(runtime / "migration-operation.json")} + outputs = {} + for source, sink, item, product, count in jobs: + old = sum(entry["count"] for entry in expected["witnesses"][sink].get("items", ()) + if entry["item"] == "minecraft:" + product) + live = sum(int(entry["Count"]) for entry in parse_nbt(rows[sink]).get("Items", ()) + if entry["id"] == "minecraft:" + product) + remaining = sum(int(entry["Count"]) for entry in parse_nbt(rows[source]).get("Items", ()) + if entry["id"] == "minecraft:" + item) + if live != old + count or remaining: + raise AssertionError(f"{sink}: expected {old + count} {product} and no input, got {live}/{remaining}") + outputs[sink] = dict(before=old, after=live, item="minecraft:" + product) + return outputs + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("prior-run", "fixture", "legacy-helper", "profile", "seed", "inventory", "java17", "output"): + parser.add_argument("--" + name, type=Path, required=True) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--port", type=int, required=True) + parser.add_argument("--fixture-phase", choices=("source", "restarted"), default="source") + args = parser.parse_args() + sys.path.append(str(args.legacy_helper.resolve())) + from native116_rebrand import verify_snapshot + + previous = read_json(args.prior_run) + fixture = read_json(args.fixture / "rebrand-upgrade-results.json") + if previous["status"] != "PASS" or not previous["sourceUnchanged"] or fixture["status"] != "PASS": + raise AssertionError("Legacy source has no passing, unchanged historical receipt") + source = Path(previous["source"]).resolve() + if source != (args.fixture / ("source" if args.fixture_phase == "source" else "target")).resolve(): + raise AssertionError("Legacy receipt and fixture path disagree") + expected = fixture["phases"][args.fixture_phase] + positions = fixture["positions"] + before = portable(fingerprint(source)) + if before != portable(previous["sourceFingerprint"]): + raise AssertionError("Legacy source changed since the previous run") + output = output_path(args.output) + target = native118.read_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + for protected in (source, args.profile, args.fixture, args.seed, args.inventory): + require_disjoint(output, protected) + launcher = native118.stage(output, target, args.port) + shutil.copytree(source / WORLD, output / WORLD) + shutil.copytree(source / "config", output / "config") + if fingerprint(source / WORLD) != fingerprint(output / WORLD): + raise AssertionError("The disposable copy differs from its source") + write_json(output / "migration-fixture.json", [ + dict(name=name, x=pos[0], y=pos[1], z=pos[2]) for name, pos in positions.items()]) + result = dict(status="RUNNING", source=str(source), sourceFingerprint=before, + priorReceiptSha256=digest(args.prior_run), profileSha256=digest(args.profile), + inventorySha256=digest(args.inventory), phases={}) + report = output / "native118-legacy-world-results.json" + try: + saves = {} + for phase in ("converted", "restarted"): + server = native118.Server(output, args.java17, launcher, phase) + try: + server.snapshot(phase) + server.send("save-all flush") + server.wait("Saved the game", 30) + finally: + server.close() + snapshot = output / f"migration-{phase}.json" + rows = read_json(snapshot) + verify_snapshot(expected, rows) + saves[phase] = native118_saved.capture(output, phase, rows) + observed = turbine(output / WORLD, positions["hydroGenerator"], data_version=native118_saved.DATA_VERSION) + if observed != dict(expected["turbine"], id="baseelectric:hydroturbine"): + raise AssertionError("Saved turbine identity, position or parent changed") + result["phases"][phase] = dict(status="PASS", witnesses=len(rows), turbine=observed, + snapshotSha256=digest(snapshot), serialized=saves[phase]) + write_json(report, result) + if saves["converted"]["worldGenSettings"] != saves["restarted"]["worldGenSettings"]: + raise AssertionError("Native world generation settings changed during restart") + repeated = compare(output / "migration-converted.json", output / "migration-restarted.json", native_normalized) + if repeated["status"] != "PASS": + raise AssertionError(f"Converted machine state changed after restart: {repeated}") + result["phases"]["operation"] = operate(output / "operation", output, target, args.java17, + args.port, positions, expected) + result["status"] = "PASS" + except Exception as error: + result.update(status="FAIL", failure=repr(error)) + raise + finally: + result["sourceUnchanged"] = portable(fingerprint(source)) == before + if not result["sourceUnchanged"]: + result["status"] = "FAIL" + write_json(report, result) + return int(result["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native118_machines.py b/test-rig/migration/native118_machines.py new file mode 100644 index 00000000..679f9637 --- /dev/null +++ b/test-rig/migration/native118_machines.py @@ -0,0 +1,142 @@ +"""Exercise 1.18.2 machines using the historical lifecycle and harvest oracles.""" + +import argparse +from pathlib import Path +import traceback + +import lifecycle +from matrix import digest, fingerprint, write_json +from native117 import jar_identity, output_path, require_disjoint +from native117_harvest import HARVEST_CHUNKS, mark_runtime, require_checks, require_isolation +from native117_identity import fixture_ids +from native117_lifecycle import NativeLifecycleRun, authorize_lifecycle, exercise, read_json +from native117_oil import NativeOilRun, exercise as exercise_oil, fixtures as oil_fixtures +from native117_readiness import await_ready +import native118 + +MINERALOGY = { + "id": "mineralogy", + "version": "6.1.4.118021", + "sourceRef": "a5f019592420d3f0de5e23585b7ee75cde2e84b3", + "file": "Mineralogy-6.1.4.118021.jar", + "sha256": "30BA2DECBA56F79EE137A1C825E2F7E4D9299DB109FAFD30A2F66D26C45F2E74", +} + + +class LifecycleRun(NativeLifecycleRun): + minecraft = native118.MINECRAFT + max_ticket_distance = 12 + require_boot = staticmethod(native118.require_native_boot) + + def open_server(self, label): + return native118.Server(self.runtime, self.java, self.launcher, label) + + +class OilRun(NativeOilRun): + expected_launcher = native118.LAUNCHER + require_boot = staticmethod(native118.require_native_boot) + + def open_server(self, label): + return native118.Server(self.runtime, self.java, self.launcher, label) + + +def harvest(runtime, java, launcher, report, progress): + mark_runtime(runtime) + for label in ("exercise", "restored"): + server = native118.Server(runtime, java, launcher, label) + try: + server.send("forceload add 1198 1022 1202 1026") + ready = await_ready(server, runtime, HARVEST_CHUNKS, "harvest-" + label, + minecraft=native118.MINECRAFT) + server.send("advharvest " + label) + server.wait("ADVHARVEST COMPLETE " + label, 120) + document = read_json(runtime / "advantage-works-results" / f"native-harvest-{label}.json") + if (type(document.get("worldTime")) is not int + or document["worldTime"] < ready["completedWorldTime"]): + raise AssertionError("Harvest preceded native chunk/entity readiness") + report[label] = {"observation": document, "readiness": ready} + require_checks(document, label, minecraft=native118.MINECRAFT) + require_isolation(document) + server.send("save-all flush") + server.wait("Saved the game", 30) + finally: + server.close() + progress(report) + return dict(report, status="PASS") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("profile", "seed", "inventory", "output", "java17"): + parser.add_argument("--" + name, required=True, type=Path) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--port", type=int, required=True) + parser.add_argument("--campaign", choices=("lifecycle", "harvest", "oil"), required=True) + parser.add_argument("--mineralogy-jar", type=Path, help="Include the pinned Mineralogy build in the oil test") + args = parser.parse_args() + target = native118.read_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + if args.mineralogy_jar: + jar = args.mineralogy_jar.resolve() + if args.campaign != "oil" or jar.name != MINERALOGY["file"] or digest(jar) != MINERALOGY["sha256"]: + raise ValueError("Mineralogy oil test requires the pinned player jar") + jar_identity(jar, MINERALOGY["id"], MINERALOGY["version"]) + target[2]["mineralogy"] = dict(MINERALOGY, path=jar) + output = output_path(args.output) + for path in (args.profile, args.seed, args.inventory, *(item["path"] for item in target[2].values())): + require_disjoint(output, path) + helpers = [Path(__file__), Path(lifecycle.__file__), + Path(__file__).with_name(f"native117_{args.campaign}.py")] + helper_hashes = {path.name: digest(path) for path in helpers} + seed_hashes = fingerprint(target[0]) + launcher = native118.stage(output, target, args.port) + path = output / f"native118-{args.campaign}-results.json" + report = dict(status="RUNNING", minecraft=native118.MINECRAFT, forge=native118.FORGE, + profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), + javaSha256=digest(args.java17), helperSha256=helper_hashes) + + def progress(values): + report["observations"] = values + write_json(path, report) + + runner = None + try: + if args.campaign == "harvest": + result = harvest(output, args.java17, launcher, {}, progress) + elif args.campaign == "oil": + pins = {"jars": list(target[2].values())} + definition = oil_fixtures(pins, mineralogy_version=MINERALOGY["version"], + mineralogy_source=MINERALOGY["sourceRef"]) + write_json(output / "lifecycle-fixture.json", definition) + authorize_lifecycle(output) + runner = OilRun(output, args.java17, launcher, definition, pins, seed_hashes) + exercise_oil(runner, 8, progress) + result = dict(report["observations"], laneCount=len(definition), status="PASS") + else: + definition = fixture_ids(lifecycle.fixtures()) + write_json(output / "lifecycle-fixture.json", definition) + authorize_lifecycle(output) + runner = LifecycleRun(output, args.java17, launcher, definition) + result = exercise(runner, "fresh118", progress) + report.update(status=result["status"], observations=result) + except Exception as error: + report.update(status="FAIL", failure=repr(error), traceback=traceback.format_exc()) + raise + finally: + try: + if runner is not None: + runner.close() + native118.verify_runtime(output) + report["seedUnchanged"] = fingerprint(target[0]) == seed_hashes + report["helpersUnchanged"] = {path.name: digest(path) for path in helpers} == helper_hashes + if not report["seedUnchanged"] or not report["helpersUnchanged"]: + raise AssertionError("Protected server seed or test code changed during the test") + except Exception as error: + report.update(status="FAIL", auditError=repr(error)) + raise + finally: + write_json(path, report) + return int(report["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native118_modern_upgrade.py b/test-rig/migration/native118_modern_upgrade.py new file mode 100644 index 00000000..bb96d72f --- /dev/null +++ b/test-rig/migration/native118_modern_upgrade.py @@ -0,0 +1,97 @@ +"""Load and operate a stopped, already-qualified 1.17.1 save on 1.18.2.""" + +import argparse +from pathlib import Path +import shutil + +from matrix import WORLD, compare, digest, fingerprint, parse_nbt, write_json +from native117 import native_normalized, output_path, require_disjoint +from native117_legacy_world import turbine +from native117_lifecycle import read_json +import native118 +from native118_legacy_world import operate +import native118_saved + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("source", "profile", "seed", "inventory", "output", "java17"): + parser.add_argument("--" + name, type=Path, required=True) + parser.add_argument("--receipt-sha256", required=True) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--port", type=int, required=True) + args = parser.parse_args() + source = args.source.resolve() + prior = source / "native117-legacy-world-results.json" + if digest(prior) != args.receipt_sha256.upper(): + raise AssertionError("The protected 1.17.1 receipt changed") + receipt = read_json(prior) + if receipt["status"] != "PASS" or not receipt["sourceUnchanged"]: + raise AssertionError("Source lacks a passing independent migration receipt") + historical = source / "migration-restarted.json" + phase = receipt["phases"]["restarted"] + if phase["status"] != "PASS" or digest(historical) != phase["snapshotSha256"]: + raise AssertionError("Source lacks its passing restart snapshot") + historical_rows = read_json(historical) + expected = {"witnesses": {}} + for row in historical_rows: + expected["witnesses"][row["name"]] = {"items": [ + {"item": stack["id"], "count": int(stack["Count"])} + for stack in (parse_nbt(row).get("Items", ()) if row.get("nbtBase64") else ())]} + before = fingerprint(source) + positions = {row["name"]: [row["x"], row["y"], row["z"]] + for row in read_json(source / "migration-fixture.json")} + target = native118.read_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + output = output_path(args.output) + for path in (source, args.profile, args.seed, args.inventory, + *(item["path"] for item in target[2].values())): + require_disjoint(output, path) + launcher = native118.stage(output, target, args.port) + for path in (WORLD, "config"): + shutil.copytree(source / path, output / path) + if fingerprint(source / WORLD) != fingerprint(output / WORLD): + raise AssertionError("The staged 1.17.1 save differs from the source") + shutil.copy2(source / "migration-fixture.json", output / "migration-fixture.json") + (output / "advantage-migration.allow").touch() + result = dict(status="RUNNING", minecraft=native118.MINECRAFT, forge=native118.FORGE, + source=str(source), sourceReceiptSha256=digest(prior), sourceFingerprint=before, + profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), phases={}) + result_path = output / "native118-modern-upgrade-results.json" + try: + for label in ("converted", "restarted"): + server = native118.Server(output, args.java17, launcher, label) + try: + server.snapshot(label) + server.send("save-all flush") + server.wait("Saved the game", 30) + finally: + server.close() + snapshot = output / f"migration-{label}.json" + comparison = compare(historical, snapshot, native_normalized) + result["phases"][label] = comparison + if comparison["status"] != "PASS": + raise AssertionError("The native 1.17.1 witnesses changed in " + label) + saved = native118_saved.capture(output, label, read_json(snapshot)) + observed = turbine(output / WORLD, positions["hydroGenerator"], + data_version=native118_saved.DATA_VERSION) + if observed != phase["turbine"]: + raise AssertionError("The saved 1.17.1 turbine changed") + result["phases"][label].update(serialized=saved, turbine=observed, + snapshotSha256=digest(snapshot)) + write_json(result_path, result) + result["phases"]["operation"] = operate(output / "operation", output, target, args.java17, + args.port, positions, expected) + result["status"] = "PASS" + except Exception as error: + result.update(status="FAIL", failure=repr(error)) + raise + finally: + result["sourceUnchanged"] = fingerprint(source) == before + if not result["sourceUnchanged"]: + result["status"] = "FAIL" + write_json(result_path, result) + return int(result["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native118_networks.py b/test-rig/migration/native118_networks.py new file mode 100644 index 00000000..4c9d11bc --- /dev/null +++ b/test-rig/migration/native118_networks.py @@ -0,0 +1,110 @@ +"""Run the original partial-network, bridge and native interop oracles on Forge 40.""" + +import argparse +from pathlib import Path +import traceback + +from matrix import digest, fingerprint, write_json +from native117 import output_path, require_disjoint +import native117_extended +from native117_geometry import FORCED_LEVEL, FULL_LEVEL +from native117_holder_networks import bridge_fixtures, extended_fixtures, linear_fixtures +from native117_interop import mark_runtime, require_checks, require_probe_order +from native117_lifecycle import authorize_lifecycle, read_json +from native117_partial import exercise_bridge, exercise_partial +from native117_readiness import await_ready +from native117_ticket_networks import NativeTicketExtendedRun +import native118 +from native118_machines import LifecycleRun + + +class PartialRun(NativeTicketExtendedRun, LifecycleRun): + pass + + +HOLDER_RADIUS = FULL_LEVEL + PartialRun.max_ticket_distance - FORCED_LEVEL + + +def interop(runtime, launcher, java, progress): + mark_runtime(runtime) + observations = {} + for phase in ("exercise", "restored"): + server = native118.Server(runtime, java, launcher, phase) + try: + server.send("forceload add 1023 1023 1100 1030") + ready = await_ready(server, runtime, (63, 63, 68, 64), "interop-" + phase, + minecraft=native118.MINECRAFT) + server.send("advinterop " + phase) + server.wait("ADVINTEROP COMPLETE " + phase, 120) + report = read_json(runtime / "advantage-works-results" / f"native-interop-{phase}.json") + observations[phase] = dict(observation=report, readiness=ready) + require_probe_order(report, ready) + require_checks(report, phase) + server.send("save-all flush") + server.wait("Saved the game", 30) + finally: + server.close() + progress(observations) + return dict(observations, status="PASS") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("profile", "seed", "inventory", "output", "java17"): + parser.add_argument("--" + name, type=Path, required=True) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--port", type=int, required=True) + parser.add_argument("--campaign", choices=("partial", "bridge", "extended", "interop"), required=True) + args = parser.parse_args() + target = native118.read_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + runtime = output_path(args.output) + for path in (args.profile, args.seed, args.inventory, *(item["path"] for item in target[2].values())): + require_disjoint(runtime, path) + protected = fingerprint(target[0]) + launcher = native118.stage(runtime, target, args.port) + report = dict(status="RUNNING", minecraft=native118.MINECRAFT, forge=native118.FORGE, + profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), + javaSha256=digest(args.java17), runnerSha256=digest(Path(__file__))) + receipt = runtime / f"native118-{args.campaign}-results.json" + + def progress(value): + report["observations"] = value + write_json(receipt, report) + + runner = None + try: + if args.campaign == "interop": + result = interop(runtime, launcher, args.java17, progress) + else: + layout, exercise = { + "partial": (linear_fixtures, exercise_partial), + "bridge": (bridge_fixtures, exercise_bridge), + "extended": (extended_fixtures, native117_extended.exercise), + }[args.campaign] + definition = layout(HOLDER_RADIUS) + write_json(runtime / "lifecycle-fixture.json", definition) + authorize_lifecycle(runtime) + runner = PartialRun(runtime, args.java17, launcher, definition) + result = exercise(runner, progress) + report.update(status=result["status"], observations=result) + except Exception as error: + report.update(status="FAIL", failure=repr(error), traceback=traceback.format_exc()) + raise + finally: + try: + if runner is not None: + runner.close() + native118.verify_runtime(runtime) + report["seedUnchanged"] = fingerprint(target[0]) == protected + if not report["seedUnchanged"]: + raise AssertionError("Protected server seed changed during the test") + except Exception as error: + report.update(status="FAIL", auditError=repr(error)) + raise + finally: + write_json(receipt, report) + return int(report["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native118_recipes.py b/test-rig/migration/native118_recipes.py new file mode 100644 index 00000000..f218b1f0 --- /dev/null +++ b/test-rig/migration/native118_recipes.py @@ -0,0 +1,77 @@ +"""Run recipe discovery and survival progression against the 1.18.2 player jars.""" + +import argparse +from pathlib import Path + +from matrix import digest, fingerprint, write_json +from native117 import output_path, require_disjoint +from native117_recipes import MOD_COUNTS, RecipePruning, progression_campaign, recipe_fixture, unlock_campaign +import native118 + + +class RecipeServer(RecipePruning, native118.Server): + @staticmethod + def errors(text): + return native118.log_findings(text)[0] + + +class Engine: + version = (native118.MINECRAFT, native118.FORGE) + pack_format = 9 + server = native118.Server + recipe_server = RecipeServer + result_name = "native118-recipes-results.json" + + @staticmethod + def stage(runtime, target, port, campaign): + launcher = native118.stage(runtime, target, port) + (runtime / ("advantage-recipes.allow" if campaign == "unlock" else "advantage-progression.allow")).touch() + if campaign == "progression": + (runtime / "advantage-works.allow").touch() + (runtime / "migration-operate.allow").touch() + return launcher + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("profile", "seed", "inventory", "output", "java17"): + parser.add_argument("--" + name, required=True, type=Path) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--port", type=int, default=25650) + parser.add_argument("--campaign", choices=("all", "unlock", "progression"), default="all") + args = parser.parse_args() + target = native118.read_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + root = output_path(args.output) + for path in (args.profile, args.seed, args.inventory, *(item["path"] for item in target[2].values())): + require_disjoint(root, path) + definition, sources = recipe_fixture([item["path"] for item in target[2].values()]) + entry = {"jars": [{**item, "path": str(item["path"])} for item in target[2].values()]} + before = fingerprint(target[0]) + report = dict(status="RUNNING", minecraft=native118.MINECRAFT, forge=native118.FORGE, campaign=args.campaign, + profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), + javaSha256=digest(args.java17), target_jars=entry["jars"], recipe_inventory=MOD_COUNTS, + unlock_boots=[], progression_cases=[]) + write_json(root / "packaged-recipe-sources.json", sources) + try: + if args.campaign in ("all", "unlock"): + unlock_campaign(root, target, entry, args.java17, args.port, definition, report, engine=Engine) + if len(report["unlock_boots"]) != 5: + raise AssertionError("Incomplete 1.18.2 unlock campaign") + if args.campaign in ("all", "progression"): + progression_campaign(root, target, entry, args.java17, args.port, report, engine=Engine) + if len(report["progression_cases"]) != 18: + raise AssertionError("Incomplete 1.18.2 progression campaign") + report["status"] = "PASS" + except BaseException as failure: + report.update(status="FAIL", error=repr(failure)) + raise + finally: + report["seed_unchanged"] = before == fingerprint(target[0]) + if not report["seed_unchanged"]: + report["status"] = "FAIL" + write_json(root / Engine.result_name, report) + return int(report["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native118_respawn.py b/test-rig/migration/native118_respawn.py new file mode 100644 index 00000000..ef272d91 --- /dev/null +++ b/test-rig/migration/native118_respawn.py @@ -0,0 +1,37 @@ +"""Run the retained twelve-case native death and respawn oracle on Forge 40.""" + +import argparse +from pathlib import Path + +from matrix import digest +from native117 import output_path, require_disjoint +from native117_respawn import read_json, run +import native118 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("source-receipt", "profile", "seed", "inventory", "java17", "output"): + parser.add_argument("--" + name, required=True, type=Path) + parser.add_argument("--source-receipt-sha256", required=True) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--route", required=True) + parser.add_argument("--port", required=True, type=int) + args = parser.parse_args() + output = output_path(args.output) + for path in (args.source_receipt, args.profile, args.seed, args.inventory): + require_disjoint(output, path) + if digest(args.source_receipt) != args.source_receipt_sha256.upper(): + raise AssertionError("Respawn source receipt has changed") + record = read_json(args.source_receipt)[args.route] + source = Path(record["path"]).resolve(strict=True) + if source.name != args.route or digest(source / "dimension-fixture.json") != record["baseFixtureSha256"]: + raise AssertionError("Respawn fixture identity or definition differs from prior receipt") + target = native118.read_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + args.fixtures, args.routes, args.target_manifest = source.parent, [source.name], args.profile + return run(args, target=target, stage=native118.stage, server_type=native118.Server, + check_log=native118.require_native_boot, approved_sources={source.name: record["fingerprint"]}) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native118_saved.py b/test-rig/migration/native118_saved.py new file mode 100644 index 00000000..59e2670f --- /dev/null +++ b/test-rig/migration/native118_saved.py @@ -0,0 +1,83 @@ +"""Verify the actual flattened terrain and block entity data written by 1.18.2.""" + +import gzip +import io +import shutil +import zlib + +import nbtlib + +from matrix import WORLD, digest, parse_nbt, plain, write_json +from native117 import native_normalized +from native117_format import decode_palette +from transformer_order import read_region + + +DATA_VERSION = 2975 + + +def decode_chunk(payload, coordinates): + if len(payload) < 6 or int.from_bytes(payload[:4], "big") != len(payload) - 4: + raise ValueError("Malformed 1.18 chunk length") + decoders = {1: gzip.decompress, 2: zlib.decompress, 3: lambda data: data} + if payload[4] not in decoders: + raise ValueError("Unknown chunk compression") + chunk = plain(nbtlib.File.parse(io.BytesIO(decoders[payload[4]](payload[5:]))).unpack()) + if chunk.get("DataVersion") != DATA_VERSION or "Level" in chunk: + raise ValueError("Chunk was not written in native 1.18.2 format") + if (chunk["xPos"], chunk["zPos"]) != coordinates: + raise ValueError("Saved chunk coordinates do not match the region slot") + sections = {} + for section in chunk["sections"]: + y = section["Y"] + if y in sections or any(key in section for key in ("Blocks", "Palette", "BlockStates")): + raise ValueError("Duplicate section or legacy block encoding") + states = section.get("block_states") + sections[y] = decode_palette(states["palette"], states.get("data", [])) if states else None + tiles = {} + for tile in chunk.get("block_entities", ()): + pos = (tile["x"], tile["y"], tile["z"]) + if pos in tiles: + raise AssertionError("Duplicate block entity in saved chunk") + tiles[pos] = tile + return sections, tiles + + +def capture(runtime, label, rows): + world = runtime / WORLD + evidence = runtime / "native118-save" / label + evidence.mkdir(parents=True) + shutil.copy2(world / "level.dat", evidence / "level.dat") + level = nbtlib.load(evidence / "level.dat")["Data"] + if level["DataVersion"] != DATA_VERSION: + raise AssertionError("Saved level does not identify 1.18.2") + regions, chunks = {}, {} + for row in rows: + x, y, z = pos = tuple(int(row[field]) for field in ("x", "y", "z")) + key = (x // 16, z // 16) + if key not in chunks: + cx, cz = key + rx, rz = region = (cx // 32, cz // 32) + if region not in regions: + _, regions[region] = read_region(world / "region" / f"r.{rx}.{rz}.mca") + payload = regions[region][(cx & 31) + (cz & 31) * 32] + (evidence / f"{cx}_{cz}.chunk").write_bytes(payload) + chunks[key] = decode_chunk(payload, key) + sections, tiles = chunks[key] + blocks = sections.get(y // 16) + block = blocks[((y & 15) << 8) | ((z & 15) << 4) | (x & 15)] if blocks else {"Name": "minecraft:air"} + if (block["Name"], block.get("Properties", {})) != (row["actualName"], row["properties"]): + raise AssertionError(f"Saved block differs from live snapshot at {pos}: {block}") + if "nbtBase64" in row: + # These fixtures are frozen while inspected. keepPacked is chunk bookkeeping. + disk = dict(tiles[pos]) + disk.pop("keepPacked", None) + if native_normalized(disk) != native_normalized(plain(parse_nbt(row))): + raise AssertionError(f"Saved block entity differs from live snapshot at {pos}") + elif pos in tiles: + raise AssertionError(f"Unexpected saved block entity at {pos}") + receipt = dict(status="PASS", witnesses=len(rows), nativeDataVersion=DATA_VERSION, + worldGenSettings=plain(level["WorldGenSettings"].unpack()), + files={file.name: digest(file) for file in evidence.iterdir()}) + write_json(evidence / "receipt.json", receipt) + return receipt diff --git a/test-rig/migration/native118_wolf_control.py b/test-rig/migration/native118_wolf_control.py new file mode 100644 index 00000000..92cafa3a --- /dev/null +++ b/test-rig/migration/native118_wolf_control.py @@ -0,0 +1,88 @@ +"""Measure native tame-wolf loading on a fresh Forge world with no mod JARs.""" + +import argparse +from pathlib import Path +import socket + +import nbtlib +from matrix import WORLD, digest, plain, write_json +from native117 import output_path, require_disjoint +import native118 + + +CASES = (("tame8", 8, True, 20), ("tame20", 20, True, 20), ("wild8", 8, False, 8)) +OWNER = (2026, -417, 118, 41) +PREFIX = "has the following entity data: " + + +def observe(server, runtime, phase, case, identity): + name, starting_health, tame, expected_health = case + selector = f"@e[type=minecraft:wolf,tag=wolf_control_{name},limit=1]" + server.send(f"data get entity {selector}") + server.wait(PREFIX, 30) + log = runtime / f"console-{phase}.log" + raw = next(line.partition(PREFIX)[2] for line in reversed(log.read_text(encoding="utf-8").splitlines()) + if PREFIX in line) + saved = plain(nbtlib.parse_nbt(raw).unpack()) + if (saved.get("Health") != expected_health or saved.get("UUID") != list(identity) + or saved.get("Owner") != (list(OWNER) if tame else None) + or f"wolf_control_{name}" not in saved.get("Tags", ())): + raise AssertionError(f"Unexpected native wolf load: {name} / {phase}") + return dict(inputHealth=starting_health, health=saved["Health"], uuid=saved["UUID"], + owner=saved.get("Owner"), observedNBT=raw) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("profile", "seed", "inventory", "java17", "output"): + parser.add_argument("--" + name, required=True, type=Path) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--port", required=True, type=int) + args = parser.parse_args() + output = output_path(args.output) + for path in (args.profile, args.seed, args.inventory): + require_disjoint(output, path) + seed, inventory, _ = native118.read_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + with socket.socket() as port: + port.bind(("127.0.0.1", args.port)) + launcher = native118.stage(output, (seed, inventory, {}), args.port, normal=True) + report = dict(status="RUNNING", scope="FRESH_FORGE_40_NO_MOD_JARS", nativeDataVersion=2975, + inventorySha256=digest(args.inventory), boots={}) + try: + for phase in ("summon", "restarted"): + server = native118.Server(output, args.java17, launcher, phase) + try: + server.send("forceload add 0 0") + server.send("say WOLF-CONTROL-CHUNK-TICKET") + server.wait("WOLF-CONTROL-CHUNK-TICKET", 30) + observed = {} + for index, case in enumerate(CASES): + name, health, tame, _ = case + identity = (2026, 11, 9, 7100 + index) + if phase == "summon": + owner = ",Owner:[I;" + ",".join(map(str, OWNER)) + "]" if tame else "" + uuid = ",".join(map(str, identity)) + tags = (f'{{UUID:[I;{uuid}]{owner},Health:{health}.0f,NoAI:1b,NoGravity:1b,' + f'Invulnerable:1b,Tags:["wolf_control_{name}"],Attributes:' + f'[{{Name:"minecraft:generic.max_health",Base:{20 if tame else 8}.0d}}]}}') + server.send(f"summon minecraft:wolf {2 + index * 4} 200 4 {tags}") + server.wait("Summoned new", 30) + observed[name] = observe(server, output, phase, case, identity) + server.send("save-all flush") + server.wait("Saved the game", 60) + finally: + server.close() + report["boots"][phase] = observed + if int(nbtlib.load(output / WORLD / "level.dat")["Data"]["DataVersion"]) != 2975: + raise AssertionError("The control world is not native 1.18.2") + report["status"] = "PASS" + except BaseException as error: + report.update(status="FAIL", error=repr(error)) + raise + finally: + write_json(output / "native118-wolf-control.json", report) + print("PASS: Forge 40 without any mods reproduced the native tame-wolf heal") + + +if __name__ == "__main__": + main() diff --git a/test-rig/migration/test_native117_actor_readiness.py b/test-rig/migration/test_native117_actor_readiness.py index be95f687..75463187 100644 --- a/test-rig/migration/test_native117_actor_readiness.py +++ b/test-rig/migration/test_native117_actor_readiness.py @@ -31,6 +31,14 @@ def test_overworld_readiness_cannot_stand_in_for_a_nether_fixture(self): with self.assertRaisesRegex(AssertionError, 'wrong dimension'): ready_entities(Mock(), runtime, 'dimensions', 'converted') + def test_native_port_requires_its_own_readiness_version(self): + with tempfile.TemporaryDirectory() as temporary: + runtime = Path(temporary) + (runtime / 'actor-fixture.json').write_text('{}') + with patch('native117_actors.await_ready', return_value={'dimension': 'minecraft:overworld'}) as ready: + ready_entities(Mock(), runtime, 'actors', 'converted', '1.18.2') + self.assertEqual('1.18.2', ready.call_args.kwargs['minecraft']) + if __name__ == '__main__': unittest.main() diff --git a/test-rig/migration/test_native117_dimension_assessment.py b/test-rig/migration/test_native117_dimension_assessment.py index c60b46d5..7e132366 100644 --- a/test-rig/migration/test_native117_dimension_assessment.py +++ b/test-rig/migration/test_native117_dimension_assessment.py @@ -187,7 +187,7 @@ def test_route_assessment_order_is_stable_for_independent_replay(self): def test_invalid_proof_preflight_happens_before_any_boot(self): with tempfile.TemporaryDirectory() as temp: proof = Path(temp) / "wrong.log"; proof.write_text("wrong") - with patch.object(native117_actors, "boot") as boot: + with patch("native117.boot") as boot: with self.assertRaises(AssertionError): native117_actors.run(SimpleNamespace(kind="dimensions", native_wolf_proof=proof)) boot.assert_not_called() diff --git a/test-rig/migration/test_native117_endurance.py b/test-rig/migration/test_native117_endurance.py index ab259edb..0e9b8d7e 100644 --- a/test-rig/migration/test_native117_endurance.py +++ b/test-rig/migration/test_native117_endurance.py @@ -53,7 +53,7 @@ def checkpoint(label): rig.documents = {"W-01": {"worldTime": next(ticks)}} rig.checkpoint = checkpoint rig.command = lambda command, response=None: commands.append(command) - with patch("native117_endurance.Server"), patch("native117_endurance.time.sleep"): + with patch.object(rig, "server_type"), patch("native117_endurance.time.sleep"): rig.boot("endurance-restart") self.assertEqual(len(observations), 3) self.assertEqual(len(commands), len(endurance.CHECKED)) @@ -67,14 +67,14 @@ def test_restart_without_measured_tick_fails(self): rig = NativeEnduranceRig("test", Path(directory), None, None, definitions) rig.command = lambda *args: None rig.checkpoint = lambda label: rig.documents.update({"W-01": {"worldTime": 200}}) - with patch("native117_endurance.Server"), patch("native117_endurance.time.sleep"): + with patch.object(rig, "server_type"), patch("native117_endurance.time.sleep"): with self.assertRaisesRegex(TimeoutError, "No actual native tick"): rig.boot("endurance-restart") def test_initial_boot_does_not_restore_or_rebuild_anything(self): rig = NativeEnduranceRig("test", Path("unused"), None, None, {}) rig.command = lambda *args: self.fail("Initial boot issued a command") - with patch("native117_endurance.Server") as server: + with patch.object(rig, "server_type") as server: rig.boot("endurance") server.assert_called_once_with(rig.runtime, rig.java, rig.launcher, 'endurance') diff --git a/test-rig/migration/test_native117_entity_storage.py b/test-rig/migration/test_native117_entity_storage.py index 07c3fac1..e05bae44 100644 --- a/test-rig/migration/test_native117_entity_storage.py +++ b/test-rig/migration/test_native117_entity_storage.py @@ -107,6 +107,15 @@ def test_actual_negative_saved_pos_not_fixture_coordinates_selects_chunk(self): self.assertEqual([[-1, -33]], [row["chunk"] for row in receipt["matched"].values()]) self.assertIn("chunks/-1.-33.chunk", receipt["files"]) + def test_118_reader_requires_the_exact_entity_region_version(self): + native = root((0, 0), [self.actor]) + native["DataVersion"] = n.Int(2975) + put(self.world, (0, 0), value=native) + with self.assertRaisesRegex(ValueError, "DataVersion"): + self.audit() + receipt = storage.audit(self.record, self.world, self.base / "native-118", data_version=2975) + self.assertEqual(1, receipt["matchedEntities"]) + def test_nether_and_end_dimension_directories(self): for name, key, identity in (("DIM-1", "minecraft:the_nether", -1), ("DIM1", "minecraft:the_end", 1)): world = self.world / name diff --git a/test-rig/migration/test_native117_geometry.py b/test-rig/migration/test_native117_geometry.py index 9ca00055..53b9b611 100644 --- a/test-rig/migration/test_native117_geometry.py +++ b/test-rig/migration/test_native117_geometry.py @@ -207,13 +207,16 @@ def test_native_client_adaptations_match_the_approved_preservation_inputs(self): with self.subTest(path=name): revision = ledger['documentationRevisions'].get(name) expected = revision['nativeSha256'] if revision else record['auditedNativeSha256'] - active = (RIG / name).read_bytes().replace(b"\r\n", b"\n") + # Audit the frozen 1.17 input; active client adapters now target 1.18. + saved = git_blob("222ba6e", name) + active = saved.replace(b"\r\n", b"\n") if name not in { "client-resources/resources/META-INF/mods.toml", "client-resources/run.ps1", "client-resources/test-geometry-contract.ps1", "client-resources/test-launcher.ps1", "client-resources/test-probe-contract.ps1", }: - self.assertEqual(hashlib.sha256((RIG / name).read_bytes()).hexdigest().upper(), expected) + self.assertIn(expected, {hashlib.sha256(data).hexdigest().upper() + for data in (active, active.replace(b"\n", b"\r\n"))}) continue original = git_blob(ADVANTAGE117, name).replace(b"\r\n", b"\n") # The original Windows review hashed three text files with CRLF. diff --git a/test-rig/migration/test_native117_lifecycle.py b/test-rig/migration/test_native117_lifecycle.py index 8bd907a7..caf00db5 100644 --- a/test-rig/migration/test_native117_lifecycle.py +++ b/test-rig/migration/test_native117_lifecycle.py @@ -82,12 +82,12 @@ def test_full_seven_groups_cannot_pass_missing_or_failed_drills(self): def test_optional_load_tracing_precedes_build_and_does_not_replace_runner(self): runner = Mock(runtime=self.root, definition=lifecycle.fixtures()) runner.build.side_effect = RuntimeError("stop before any real fixture") - with patch.object(native, "require_native_boot"): - with self.assertRaisesRegex(RuntimeError, "stop before"): - native.exercise(runner, "unit", lambda result: None, trace_loads=True) + with self.assertRaisesRegex(RuntimeError, "stop before"): + native.exercise(runner, "unit", lambda result: None, trace_loads=True) runner.boot.assert_called_once_with("lifecycle") + runner.require_boot.assert_called_once_with(self.root / "console-lifecycle.log") runner.probe.assert_called_once_with("trace-loads", "tracing-loads") - self.assertEqual([call[0] for call in runner.mock_calls], ["boot", "probe", "build"]) + self.assertEqual([call[0] for call in runner.mock_calls], ["boot", "require_boot", "probe", "build"]) def test_source_preflight_is_read_only(self): args = self.source_args() @@ -228,7 +228,7 @@ def test_round_trip_readiness_watches_exact_fixture_chunks(self): runner.server = Mock() with patch.object(native, "await_ready", return_value={"label": "first-chunks-0"}) as ready: self.assertEqual(["first-chunks-0"], runner._round_trip_ready("first")) - ready.assert_called_once_with(runner.server, self.root, (0, 0, 0, 0), "first-chunks-0") + ready.assert_called_once_with(runner.server, self.root, (0, 0, 0, 0), "first-chunks-0", minecraft="1.17.1") runner.server.send.assert_not_called() def test_readiness_does_not_inspect_machine_nbt_block_or_progress(self): @@ -439,10 +439,11 @@ def test_wall_guard_cannot_be_passed_by_late_tick(self): def test_native_restart_only_restores_tickets_not_fixtures(self): absent, loaded, ready = self.snapshot(loaded=False, loads=0), self.snapshot(), self.snapshot(101) runner = self.runner([absent, loaded, ready]) - with patch("native117_code.Server") as boot, patch.object(native, "require_native_boot"), \ + with patch("native117_code.Server") as boot, patch.object(runner, "require_boot") as require_boot, \ patch.object(native.time, "sleep"): runner.boot("midblock-restart") boot.assert_called_once_with(runner.runtime, runner.java, runner.launcher, "midblock-restart") + require_boot.assert_called_once_with(runner.runtime / "console-midblock-restart.log") self.assertEqual([call.args[0] for call in runner._raw_probe.call_args_list], ["snapshot", "load", "snapshot"]) self.assertEqual(runner.observations[-1]["reload_before_label"], "midblock-restart-native-unrestored") diff --git a/test-rig/migration/test_native117_readiness.py b/test-rig/migration/test_native117_readiness.py index 1ba53ec4..65659b69 100644 --- a/test-rig/migration/test_native117_readiness.py +++ b/test-rig/migration/test_native117_readiness.py @@ -76,8 +76,12 @@ def test_command_is_read_only_and_native_end_checks_are_literal(self): source = (Path(__file__).resolve().parents[1] / "src/main/java/zone/moddev/mc/advantageworks/Native117Readiness.java").read_text() self.assertIn("TickEvent.Phase.END", source) - self.assertIn("world.isPositionTickingWithEntitiesLoaded(pos)", source) - self.assertIn("world.isPositionEntityTicking(new ChunkPos(x, z))", source) + self.assertIn("Native118ChunkTicking.withEntitiesLoaded(world, pos)", source) + self.assertIn("world.isPositionEntityTicking(pos)", source) + native = (Path(__file__).resolve().parents[1] / + "src/main/java/zone/moddev/mc/advantageworks/Native118ChunkTicking.java").read_text() + self.assertIn("long key = ChunkPos.asLong(pos)", native) + self.assertIn("world.areEntitiesLoaded(key) && world.getChunkSource().isPositionTicking(key)", native) for forbidden in ("setChunkForced(", "setChanged(", "setBlock(", "getChunk(", "addFreshEntity(", "getPrivateValue(", "Thread.sleep("): self.assertNotIn(forbidden, source) diff --git a/test-rig/migration/test_native117_recipes.py b/test-rig/migration/test_native117_recipes.py index bbfceda9..9abe25fb 100644 --- a/test-rig/migration/test_native117_recipes.py +++ b/test-rig/migration/test_native117_recipes.py @@ -27,7 +27,7 @@ def test_progression_waits_for_reload_and_native_entity_readiness(self): call.send("forceload add 16384 16384 16415 16415"), ], server.mock_calls) ready.assert_called_once_with(server, self.root, (1024, 1024, 1025, 1025), - "progression-normal-initial-reload") + "progression-normal-initial-reload", minecraft="1.17.1") def packaged(self, counts=None, bad_predicate=False, legacy_predicate=False): jars = [] diff --git a/test-rig/migration/test_native117_stopped_entities.py b/test-rig/migration/test_native117_stopped_entities.py index 7dc85723..0e6a203f 100644 --- a/test-rig/migration/test_native117_stopped_entities.py +++ b/test-rig/migration/test_native117_stopped_entities.py @@ -128,8 +128,8 @@ def test_stop_file_change_during_audit_fails(self): self.stage() original = stopped.entity_storage.audit - def overwrite(*args): - result = original(*args) + def overwrite(*args, **kwargs): + result = original(*args, **kwargs) self.source.write_text("reused by another boot") return result diff --git a/test-rig/migration/test_native118_log_policy.py b/test-rig/migration/test_native118_log_policy.py new file mode 100644 index 00000000..94a9bd9e --- /dev/null +++ b/test-rig/migration/test_native118_log_policy.py @@ -0,0 +1,27 @@ +import unittest + +from native118 import log_findings + + +DEBUG = "[09Nov2026 12:44:32.901] [worker/DEBUG] [io.netty.util.internal.PlatformDependent0/]: " +PROBE = "direct buffer constructor: unavailable" +DETAIL = "java.lang.UnsupportedOperationException: Reflective setAccessible(true) disabled" +FRAME = "\tat io.netty.util.internal.ReflectionUtil.trySetAccessible(ReflectionUtil.java:31)" + + +class NativeLogPolicyTests(unittest.TestCase): + def test_known_capability_fallback_is_recorded_without_a_runtime_failure(self): + failures, diagnostics = log_findings("\n".join((DEBUG + PROBE, DETAIL, FRAME))) + self.assertEqual([], failures) + self.assertEqual([dict(probe=PROBE, detail=DETAIL)], diagnostics) + + def test_same_exception_without_the_exact_debug_probe_is_a_failure(self): + for header in ("", DEBUG + "different probe", DEBUG.replace("/DEBUG]", "/ERROR]") + PROBE): + failures, diagnostics = log_findings(header + "\n" + DETAIL) + self.assertIn(DETAIL, failures) + self.assertEqual([], diagnostics) + + def test_gameplay_error_after_a_platform_probe_is_not_discarded(self): + error = "[12:44:33] [Server thread/ERROR] [minecraft/MinecraftServer]: Failed machine tick" + failures, _ = log_findings("\n".join((DEBUG + PROBE, DETAIL, FRAME, error))) + self.assertEqual([error], failures) diff --git a/test-rig/profiles/native118/dev-core.json b/test-rig/profiles/native118/dev-core.json new file mode 100644 index 00000000..c1bf3611 --- /dev/null +++ b/test-rig/profiles/native118/dev-core.json @@ -0,0 +1,43 @@ +{ + "name": "dev-core", + "mode": "development", + "runtimeBase": "build/works-native118", + "minecraftVersion": "1.18.2", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "sourceCommit": "bab9971cc2d593eee1f4a3de6dafc7ec268d25c2", + "sourceVersion": "2.4.10.118021", + "powerDeobfSha256": "C00F835A7A80BAF332355EF1F181F252B603A017A979D2F41EA3BBEAE314E280", + "levelType": "advantageworkstest:test_flat", + "levelSeed": "advantage-works-1.18.2-v1", + "startupTimeoutSeconds": 180, + "forge": { + "version": "1.18.2-40.3.0", + "installerPath": "../../../../BaseMetals/.runtime-cache/forge-1.18.2-40.3.0-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.18.2-40.3.0/forge-1.18.2-40.3.0-installer.jar", + "sha256": "9434C29790504DCD11CE97CB30DD8891B4B18848982357BC1B5ABFA79FB95103" + }, + "mods": [ + { + "id": "basesteam", + "version": "2.2.11.118021", + "file": "baseSteam-2.2.11.118021-deobf.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs-dev/baseSteam-2.2.11.118021-deobf.jar", + "sha256": "EAE2ED139593F831AC29791C9C4310988974ABB6B44C32377FC4740A452BD6EE" + }, + { + "id": "baseelectric", + "version": "2.2.11.118021", + "file": "baseElectric-2.2.11.118021-deobf.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs-dev/baseElectric-2.2.11.118021-deobf.jar", + "sha256": "217066658A128190379BE4518E862C7A099D85593A24B1878DECC10924FFE17A" + }, + { + "id": "advantageworkstest", + "version": "1.1.2.118021", + "file": "AdvantageWorksTestHarness-1.1.2.118021-deobf.jar", + "path": "build/gradle/libs-dev/AdvantageWorksTestHarness-1.1.2.118021-deobf.jar", + "sha256": "0FCE6394EE385B218F2C8E2B262790D7358DD05643EF204912AFE4896271CB8E" + } + ] +} diff --git a/test-rig/profiles/native118/packaged-current.json b/test-rig/profiles/native118/packaged-current.json new file mode 100644 index 00000000..cf78f94b --- /dev/null +++ b/test-rig/profiles/native118/packaged-current.json @@ -0,0 +1,64 @@ +{ + "name": "packaged-current", + "mode": "packaged", + "runtimeBase": "build/works-native118", + "minecraftVersion": "1.18.2", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "sourceCommit": "bab9971cc2d593eee1f4a3de6dafc7ec268d25c2", + "sourceVersion": "2.4.10.118021", + "powerDeobfSha256": "C00F835A7A80BAF332355EF1F181F252B603A017A979D2F41EA3BBEAE314E280", + "levelType": "advantageworkstest:test_flat", + "levelSeed": "advantage-works-1.18.2-v1", + "startupTimeoutSeconds": 180, + "forge": { + "version": "1.18.2-40.3.0", + "installerPath": "../../../../BaseMetals/.runtime-cache/forge-1.18.2-40.3.0-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.18.2-40.3.0/forge-1.18.2-40.3.0-installer.jar", + "sha256": "9434C29790504DCD11CE97CB30DD8891B4B18848982357BC1B5ABFA79FB95103" + }, + "mods": [ + { + "id": "basepower", + "version": "2.4.10.118021", + "file": "basePower-2.4.10.118021.jar", + "path": "../build/libs/basePower-2.4.10.118021.jar", + "sha256": "595991E8BB6B831C4F3A291251F283CD5FB1EC1F9BC48C1BB54BC5D88490ECB9" + }, + { + "id": "basesteam", + "version": "2.2.11.118021", + "file": "baseSteam-2.2.11.118021.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs/baseSteam-2.2.11.118021.jar", + "sha256": "2E7131FD22F9486905212F64323F5E11EEFBE0A538499E92477373D0C5D2A2CC" + }, + { + "id": "baseelectric", + "version": "2.2.11.118021", + "file": "baseElectric-2.2.11.118021.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs/baseElectric-2.2.11.118021.jar", + "sha256": "C5C1A6DEAE795F8AF741DBE35B0D8941F7FA4BBC708758BDFE056B4AEBADCBC1" + }, + { + "id": "basemetals", + "version": "3.1.2.118021", + "file": "BaseMetals-3.1.2.118021.jar", + "path": "../../../../BaseMetals/BaseMetals 118/BaseMetals/build/libs/BaseMetals-3.1.2.118021.jar", + "sha256": "9EA9DE0E5D548FD77BBAB05565D3C2004E1C2C985353C626F6610CDA07EE3BBF" + }, + { + "id": "orespawn", + "version": "4.1.0.118021", + "file": "OreSpawn-4.1.0.118021.jar", + "path": "../../../../MinecraftMineralogy/OS118/OreSpawn/build/libs/OreSpawn-4.1.0.118021.jar", + "sha256": "BD8D84A96DAFB7668E95D61B9D72C62AEF3440675473E41244C182C64653356F" + }, + { + "id": "advantageworkstest", + "version": "1.1.2.118021", + "file": "AdvantageWorksTestHarness-1.1.2.118021.jar", + "path": "build/gradle/libs/AdvantageWorksTestHarness-1.1.2.118021.jar", + "sha256": "CB3EE29B4BDD28ED005D1CA5231BFE8C7017A40714D3ED156FA7C2E045D0CA4D" + } + ] +} diff --git a/test-rig/profiles/native118/packaged-decoupled.json b/test-rig/profiles/native118/packaged-decoupled.json new file mode 100644 index 00000000..14f61905 --- /dev/null +++ b/test-rig/profiles/native118/packaged-decoupled.json @@ -0,0 +1,57 @@ +{ + "name": "packaged-decoupled", + "mode": "packaged", + "runtimeBase": "build/works-native118", + "minecraftVersion": "1.18.2", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "sourceCommit": "bab9971cc2d593eee1f4a3de6dafc7ec268d25c2", + "sourceVersion": "2.4.10.118021", + "powerDeobfSha256": "C00F835A7A80BAF332355EF1F181F252B603A017A979D2F41EA3BBEAE314E280", + "levelType": "advantageworkstest:test_flat", + "levelSeed": "advantage-works-1.18.2-v1", + "startupTimeoutSeconds": 180, + "forge": { + "version": "1.18.2-40.3.0", + "installerPath": "../../../../BaseMetals/.runtime-cache/forge-1.18.2-40.3.0-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.18.2-40.3.0/forge-1.18.2-40.3.0-installer.jar", + "sha256": "9434C29790504DCD11CE97CB30DD8891B4B18848982357BC1B5ABFA79FB95103" + }, + "mods": [ + { + "id": "basepower", + "version": "2.4.10.118021", + "file": "basePower-2.4.10.118021.jar", + "path": "../build/libs/basePower-2.4.10.118021.jar", + "sha256": "595991E8BB6B831C4F3A291251F283CD5FB1EC1F9BC48C1BB54BC5D88490ECB9" + }, + { + "id": "basesteam", + "version": "2.2.11.118021", + "file": "baseSteam-2.2.11.118021.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs/baseSteam-2.2.11.118021.jar", + "sha256": "2E7131FD22F9486905212F64323F5E11EEFBE0A538499E92477373D0C5D2A2CC" + }, + { + "id": "baseelectric", + "version": "2.2.11.118021", + "file": "baseElectric-2.2.11.118021.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs/baseElectric-2.2.11.118021.jar", + "sha256": "C5C1A6DEAE795F8AF741DBE35B0D8941F7FA4BBC708758BDFE056B4AEBADCBC1" + }, + { + "id": "orespawn", + "version": "4.1.0.118021", + "file": "OreSpawn-4.1.0.118021.jar", + "path": "../../../../MinecraftMineralogy/OS118/OreSpawn/build/libs/OreSpawn-4.1.0.118021.jar", + "sha256": "BD8D84A96DAFB7668E95D61B9D72C62AEF3440675473E41244C182C64653356F" + }, + { + "id": "advantageworkstest", + "version": "1.1.2.118021", + "file": "AdvantageWorksTestHarness-1.1.2.118021.jar", + "path": "build/gradle/libs/AdvantageWorksTestHarness-1.1.2.118021.jar", + "sha256": "CB3EE29B4BDD28ED005D1CA5231BFE8C7017A40714D3ED156FA7C2E045D0CA4D" + } + ] +} diff --git a/test-rig/profiles/native118/worldgen-advantage.json b/test-rig/profiles/native118/worldgen-advantage.json new file mode 100644 index 00000000..f4648686 --- /dev/null +++ b/test-rig/profiles/native118/worldgen-advantage.json @@ -0,0 +1,153 @@ +{ + "mode": "packaged", + "runtimeBase": "build/works-native118-worldgen", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "bab9971cc2d593eee1f4a3de6dafc7ec268d25c2", + "sourceVersion": "2.4.10.118021", + "forge": { + "version": "1.18.2-40.3.0", + "installerPath": "../../../../BaseMetals/.runtime-cache/forge-1.18.2-40.3.0-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.18.2-40.3.0/forge-1.18.2-40.3.0-installer.jar", + "sha256": "9434C29790504DCD11CE97CB30DD8891B4B18848982357BC1B5ABFA79FB95103" + }, + "mods": [ + { + "id": "basepower", + "version": "2.4.10.118021", + "file": "basePower-2.4.10.118021.jar", + "path": "../build/libs/basePower-2.4.10.118021.jar", + "sha256": "595991E8BB6B831C4F3A291251F283CD5FB1EC1F9BC48C1BB54BC5D88490ECB9" + }, + { + "id": "basesteam", + "version": "2.2.11.118021", + "file": "baseSteam-2.2.11.118021.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs/baseSteam-2.2.11.118021.jar", + "sha256": "2E7131FD22F9486905212F64323F5E11EEFBE0A538499E92477373D0C5D2A2CC" + }, + { + "id": "baseelectric", + "version": "2.2.11.118021", + "file": "baseElectric-2.2.11.118021.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs/baseElectric-2.2.11.118021.jar", + "sha256": "C5C1A6DEAE795F8AF741DBE35B0D8941F7FA4BBC708758BDFE056B4AEBADCBC1" + }, + { + "id": "orespawn", + "version": "4.1.0.118021", + "file": "OreSpawn-4.1.0.118021.jar", + "path": "../../../../MinecraftMineralogy/OS118/OreSpawn/build/libs/OreSpawn-4.1.0.118021.jar", + "sha256": "BD8D84A96DAFB7668E95D61B9D72C62AEF3440675473E41244C182C64653356F" + }, + { + "id": "advantageworkstest", + "version": "1.1.2.118021", + "file": "AdvantageWorksTestHarness-1.1.2.118021.jar", + "path": "build/gradle/libs/AdvantageWorksTestHarness-1.1.2.118021.jar", + "sha256": "CB3EE29B4BDD28ED005D1CA5231BFE8C7017A40714D3ED156FA7C2E045D0CA4D" + } + ], + "name": "worldgen-advantage", + "levelName": "AdvantageWorksWorldgen", + "levelSeed": "advantage-works-worldgen-1.15.2-v1", + "levelType": "default", + "generatorSettings": "", + "buildWorks": false, + "worldgenSampleRadius": 8, + "shutdownTimeoutSeconds": 300, + "lifecycleStations": [], + "firstProfileAssertions": [ + { + "id": "desert-oil-visible", + "section": "fluid_deposits", + "entry": "basepower:fluid_deposit/desert_crude_oil", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "desert-oil-owner", + "section": "fluid_deposits", + "entry": "basepower:fluid_deposit/desert_crude_oil", + "present": true, + "field": "source_provider", + "expectedString": "basepower" + }, + { + "id": "electric-sulfur-default", + "section": "ores", + "entry": "baseelectric:ore/sulfur", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "electric-lithium-default", + "section": "ores", + "entry": "baseelectric:ore/lithium", + "present": true, + "field": "enabled", + "expectedBoolean": true + } + ], + "profileMutations": [ + { + "section": "fluid_deposits", + "entry": "basepower:fluid_deposit/desert_crude_oil", + "field": "enabled", + "booleanValue": false + }, + { + "section": "ores", + "entry": "baseelectric:ore/sulfur", + "field": "enabled", + "booleanValue": false + } + ], + "restartProfileAssertions": [ + { + "id": "desert-oil-user-disable-persists", + "section": "fluid_deposits", + "entry": "basepower:fluid_deposit/desert_crude_oil", + "present": true, + "field": "enabled", + "expectedBoolean": false + }, + { + "id": "electric-sulfur-user-disable-persists", + "section": "ores", + "entry": "baseelectric:ore/sulfur", + "present": true, + "field": "enabled", + "expectedBoolean": false + }, + { + "id": "electric-lithium-remains-enabled", + "section": "ores", + "entry": "baseelectric:ore/lithium", + "present": true, + "field": "enabled", + "expectedBoolean": true + } + ], + "worldgenExpectations": { + "minimumCounts": { + "basepower:crude_oil": 1, + "baseelectric:li_ore": 1, + "baseelectric:sulfur_ore": 1 + }, + "maximumCounts": { + "mineralogy:crude_oil": 0, + "mineralogy:sulfur_ore": 0 + }, + "zeroBiomeViolations": true + }, + "minecraftVersion": "1.18.2", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "inputStatus": "ARTIFACTS_AND_SOURCES_PINNED_QUALIFICATION_PENDING", + "powerDeobfSha256": "C00F835A7A80BAF332355EF1F181F252B603A017A979D2F41EA3BBEAE314E280" +} diff --git a/test-rig/profiles/native118/worldgen-legacy-import.json b/test-rig/profiles/native118/worldgen-legacy-import.json new file mode 100644 index 00000000..3cb2df6d --- /dev/null +++ b/test-rig/profiles/native118/worldgen-legacy-import.json @@ -0,0 +1,220 @@ +{ + "mode": "packaged", + "runtimeBase": "build/works-native118-worldgen", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "bab9971cc2d593eee1f4a3de6dafc7ec268d25c2", + "sourceVersion": "2.4.10.118021", + "forge": { + "version": "1.18.2-40.3.0", + "installerPath": "../../../../BaseMetals/.runtime-cache/forge-1.18.2-40.3.0-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.18.2-40.3.0/forge-1.18.2-40.3.0-installer.jar", + "sha256": "9434C29790504DCD11CE97CB30DD8891B4B18848982357BC1B5ABFA79FB95103" + }, + "mods": [ + { + "id": "basepower", + "version": "2.4.10.118021", + "file": "basePower-2.4.10.118021.jar", + "path": "../build/libs/basePower-2.4.10.118021.jar", + "sha256": "595991E8BB6B831C4F3A291251F283CD5FB1EC1F9BC48C1BB54BC5D88490ECB9" + }, + { + "id": "basesteam", + "version": "2.2.11.118021", + "file": "baseSteam-2.2.11.118021.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs/baseSteam-2.2.11.118021.jar", + "sha256": "2E7131FD22F9486905212F64323F5E11EEFBE0A538499E92477373D0C5D2A2CC" + }, + { + "id": "baseelectric", + "version": "2.2.11.118021", + "file": "baseElectric-2.2.11.118021.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs/baseElectric-2.2.11.118021.jar", + "sha256": "C5C1A6DEAE795F8AF741DBE35B0D8941F7FA4BBC708758BDFE056B4AEBADCBC1" + }, + { + "id": "orespawn", + "version": "4.1.0.118021", + "file": "OreSpawn-4.1.0.118021.jar", + "path": "../../../../MinecraftMineralogy/OS118/OreSpawn/build/libs/OreSpawn-4.1.0.118021.jar", + "sha256": "BD8D84A96DAFB7668E95D61B9D72C62AEF3440675473E41244C182C64653356F" + }, + { + "id": "advantageworkstest", + "version": "1.1.2.118021", + "file": "AdvantageWorksTestHarness-1.1.2.118021.jar", + "path": "build/gradle/libs/AdvantageWorksTestHarness-1.1.2.118021.jar", + "sha256": "CB3EE29B4BDD28ED005D1CA5231BFE8C7017A40714D3ED156FA7C2E045D0CA4D" + } + ], + "name": "worldgen-legacy-import", + "levelName": "AdvantageWorksWorldgen", + "levelSeed": "advantage-works-worldgen-1.15.2-v1", + "levelType": "default", + "generatorSettings": "", + "buildWorks": false, + "worldgenSampleRadius": 8, + "stableWorldgenCountExclusions": [ + "basepower:crude_oil" + ], + "shutdownTimeoutSeconds": 300, + "lifecycleStations": [], + "configFixtures": [ + { + "source": "../../../PA113/PowerAdvantage/test-rig/../src/test/resources/orespawn-legacy/poweradvantage.json", + "destination": "config/orespawn/poweradvantage.json" + }, + { + "source": "../../../PA113/PowerAdvantage/test-rig/../../ElectricAdvantage/ElectricAdvantage/src/test/resources/orespawn-legacy/electricadvantage.json", + "destination": "config/orespawn/electricadvantage.json" + } + ], + "firstProfileAssertions": [ + { + "id": "legacy-oil-retained", + "section": "ores", + "entry": "basepower:legacy/basepower_crude_oil_0", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "legacy-lithium-retained", + "section": "ores", + "entry": "baseelectric:legacy/baseelectric_li_ore_0", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "legacy-sulfur-retained", + "section": "ores", + "entry": "baseelectric:legacy/baseelectric_sulfur_ore_1", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "modern-oil-not-duplicated", + "section": "fluid_deposits", + "entry": "basepower:fluid_deposit/desert_crude_oil", + "present": false + }, + { + "id": "modern-lithium-not-duplicated", + "section": "ores", + "entry": "baseelectric:ore/lithium", + "present": false + }, + { + "id": "modern-sulfur-not-duplicated", + "section": "ores", + "entry": "baseelectric:ore/sulfur", + "present": false + } + ], + "restartProfileAssertions": [ + { + "id": "legacy-oil-retained", + "section": "ores", + "entry": "basepower:legacy/basepower_crude_oil_0", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "legacy-lithium-retained", + "section": "ores", + "entry": "baseelectric:legacy/baseelectric_li_ore_0", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "legacy-sulfur-retained", + "section": "ores", + "entry": "baseelectric:legacy/baseelectric_sulfur_ore_1", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "modern-oil-not-duplicated", + "section": "fluid_deposits", + "entry": "basepower:fluid_deposit/desert_crude_oil", + "present": false + }, + { + "id": "modern-lithium-not-duplicated", + "section": "ores", + "entry": "baseelectric:ore/lithium", + "present": false + }, + { + "id": "modern-sulfur-not-duplicated", + "section": "ores", + "entry": "baseelectric:ore/sulfur", + "present": false + } + ], + "firstFileAssertions": [ + { + "id": "migration-report", + "path": "config/orespawn-os3-migration-report.json", + "contains": "os1_config_source", + "minimumOccurrences": 2 + }, + { + "id": "power-provider", + "path": "config/basepower-orespawn.json", + "contains": "basepower:legacy/basepower_crude_oil_0", + "minimumOccurrences": 1 + }, + { + "id": "electric-provider", + "path": "config/baseelectric-orespawn.json", + "contains": "baseelectric:legacy/baseelectric_li_ore_0", + "minimumOccurrences": 1 + } + ], + "restartFileAssertions": [ + { + "id": "migration-report", + "path": "config/orespawn-os3-migration-report.json", + "contains": "os1_config_source", + "minimumOccurrences": 2 + }, + { + "id": "power-provider", + "path": "config/basepower-orespawn.json", + "contains": "basepower:legacy/basepower_crude_oil_0", + "minimumOccurrences": 1 + }, + { + "id": "electric-provider", + "path": "config/baseelectric-orespawn.json", + "contains": "baseelectric:legacy/baseelectric_li_ore_0", + "minimumOccurrences": 1 + } + ], + "worldgenExpectations": { + "minimumCounts": { + "basepower:crude_oil": 1, + "baseelectric:li_ore": 1, + "baseelectric:sulfur_ore": 1 + }, + "maximumCounts": { + "mineralogy:crude_oil": 0, + "mineralogy:sulfur_ore": 0 + }, + "zeroBiomeViolations": false + }, + "minecraftVersion": "1.18.2", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "inputStatus": "ARTIFACTS_AND_SOURCES_PINNED_QUALIFICATION_PENDING", + "powerDeobfSha256": "C00F835A7A80BAF332355EF1F181F252B603A017A979D2F41EA3BBEAE314E280" +} diff --git a/test-rig/profiles/native118/worldgen-mineralogy.json b/test-rig/profiles/native118/worldgen-mineralogy.json new file mode 100644 index 00000000..a4816052 --- /dev/null +++ b/test-rig/profiles/native118/worldgen-mineralogy.json @@ -0,0 +1,163 @@ +{ + "mode": "packaged", + "runtimeBase": "build/works-native118-worldgen", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "bab9971cc2d593eee1f4a3de6dafc7ec268d25c2", + "sourceVersion": "2.4.10.118021", + "forge": { + "version": "1.18.2-40.3.0", + "installerPath": "../../../../BaseMetals/.runtime-cache/forge-1.18.2-40.3.0-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.18.2-40.3.0/forge-1.18.2-40.3.0-installer.jar", + "sha256": "9434C29790504DCD11CE97CB30DD8891B4B18848982357BC1B5ABFA79FB95103" + }, + "mods": [ + { + "id": "basepower", + "version": "2.4.10.118021", + "file": "basePower-2.4.10.118021.jar", + "path": "../build/libs/basePower-2.4.10.118021.jar", + "sha256": "595991E8BB6B831C4F3A291251F283CD5FB1EC1F9BC48C1BB54BC5D88490ECB9" + }, + { + "id": "basesteam", + "version": "2.2.11.118021", + "file": "baseSteam-2.2.11.118021.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs/baseSteam-2.2.11.118021.jar", + "sha256": "2E7131FD22F9486905212F64323F5E11EEFBE0A538499E92477373D0C5D2A2CC" + }, + { + "id": "baseelectric", + "version": "2.2.11.118021", + "file": "baseElectric-2.2.11.118021.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs/baseElectric-2.2.11.118021.jar", + "sha256": "C5C1A6DEAE795F8AF741DBE35B0D8941F7FA4BBC708758BDFE056B4AEBADCBC1" + }, + { + "id": "orespawn", + "version": "4.1.0.118021", + "file": "OreSpawn-4.1.0.118021.jar", + "path": "../../../../MinecraftMineralogy/OS118/OreSpawn/build/libs/OreSpawn-4.1.0.118021.jar", + "sha256": "BD8D84A96DAFB7668E95D61B9D72C62AEF3440675473E41244C182C64653356F" + }, + { + "id": "advantageworkstest", + "version": "1.1.2.118021", + "file": "AdvantageWorksTestHarness-1.1.2.118021.jar", + "path": "build/gradle/libs/AdvantageWorksTestHarness-1.1.2.118021.jar", + "sha256": "CB3EE29B4BDD28ED005D1CA5231BFE8C7017A40714D3ED156FA7C2E045D0CA4D" + }, + { + "id": "mineralogy", + "version": "6.1.4.118021", + "sourceRef": "a5f019592420d3f0de5e23585b7ee75cde2e84b3", + "file": "Mineralogy-6.1.4.118021.jar", + "path": "../../../../MinecraftMineralogy/MinecraftMineralogy 118/MinecraftMineralogy/build/libs/Mineralogy-6.1.4.118021.jar", + "sha256": "30BA2DECBA56F79EE137A1C825E2F7E4D9299DB109FAFD30A2F66D26C45F2E74" + } + ], + "name": "worldgen-mineralogy", + "levelName": "AdvantageWorksWorldgen", + "levelSeed": "advantage-works-worldgen-1.15.2-v1", + "levelType": "default", + "generatorSettings": "", + "buildWorks": false, + "worldgenSampleRadius": 16, + "shutdownTimeoutSeconds": 300, + "lifecycleStations": [], + "firstProfileAssertions": [ + { + "id": "desert-oil-enabled", + "section": "fluid_deposits", + "entry": "basepower:fluid_deposit/desert_crude_oil", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "ocean-oil-enabled", + "section": "fluid_deposits", + "entry": "mineralogy:fluid_deposit/crude_oil", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "mineralogy-sulfur-enabled", + "section": "ores", + "entry": "mineralogy:ore/mineralogy/sulfur_ore", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "electric-sulfur-disabled", + "section": "ores", + "entry": "baseelectric:ore/sulfur", + "present": true, + "field": "enabled", + "expectedBoolean": false + }, + { + "id": "electric-lithium-fallback-enabled", + "section": "ores", + "entry": "baseelectric:ore/lithium", + "present": true, + "field": "enabled", + "expectedBoolean": true + } + ], + "profileMutations": [ + { + "section": "ores", + "entry": "baseelectric:ore/sulfur", + "field": "enabled", + "booleanValue": true + } + ], + "restartProfileAssertions": [ + { + "id": "electric-sulfur-user-enable-persists", + "section": "ores", + "entry": "baseelectric:ore/sulfur", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "mineralogy-sulfur-remains-enabled", + "section": "ores", + "entry": "mineralogy:ore/mineralogy/sulfur_ore", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "ocean-oil-remains-enabled", + "section": "fluid_deposits", + "entry": "mineralogy:fluid_deposit/crude_oil", + "present": true, + "field": "enabled", + "expectedBoolean": true + } + ], + "worldgenExpectations": { + "minimumCounts": { + "mineralogy:crude_oil": 1, + "mineralogy:sulfur_ore": 1, + "basepower:crude_oil": 1, + "baseelectric:li_ore": 1 + }, + "maximumCounts": { + "baseelectric:sulfur_ore": 0 + }, + "zeroBiomeViolations": true + }, + "minecraftVersion": "1.18.2", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "inputStatus": "ARTIFACTS_AND_SOURCES_PINNED_QUALIFICATION_PENDING", + "powerDeobfSha256": "C00F835A7A80BAF332355EF1F181F252B603A017A979D2F41EA3BBEAE314E280" +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java index 2dfedc38..1b0b13c2 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java @@ -3,16 +3,17 @@ import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.common.world.ForgeWorldPreset; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; -import net.minecraftforge.common.world.ForgeWorldType; @Mod(AdvantageWorksMod.MODID) public final class AdvantageWorksMod { public static final String MODID = "advantageworkstest"; - public static final String VERSION = "1.1.0.117011"; + public static final String VERSION = "1.1.2.118021"; public AdvantageWorksMod() { - FMLJavaModLoadingContext.get().getModEventBus().addGenericListener(ForgeWorldType.class, Native117TestFlat::register); + FMLJavaModLoadingContext.get().getModEventBus() + .addGenericListener(ForgeWorldPreset.class, Native118TestFlat::register); MigrationProbe.register(); ConnectedProbe.register(); ActorProbe.register(); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ConnectedProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ConnectedProbe.java index cc6d0e84..d46fe3b5 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ConnectedProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ConnectedProbe.java @@ -107,7 +107,7 @@ private void execute(ServerLevel current, String operation, String label, int ti entry.add("properties", properties); BlockEntity tile = world.getBlockEntity(pos); if (tile != null) { - CompoundTag tag = tile.save(new CompoundTag()); + CompoundTag tag = tile.saveWithFullMetadata(); java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream(); net.minecraft.nbt.NbtIo.writeCompressed(tag, bytes); entry.addProperty("nbt", tag.toString()); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/DimensionEntityProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/DimensionEntityProbe.java index 9fd0fe64..8062597e 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/DimensionEntityProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/DimensionEntityProbe.java @@ -344,7 +344,7 @@ private JsonObject snapshot(MinecraftServer server, ServerLevel world, JsonObjec result.add("entities", entities); BlockPos pos = new BlockPos(176, 101, 8); JsonObject generator = new JsonObject(); generator.addProperty("actualName", world.getBlockState(pos).getBlock().getRegistryName().toString()); - BlockEntity tile = world.getBlockEntity(pos); if (tile != null) generator.add("tile", encode(tile.save(new CompoundTag()))); + BlockEntity tile = world.getBlockEntity(pos); if (tile != null) generator.add("tile", encode(tile.saveWithFullMetadata())); result.add("generator", generator); return result; } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleProbe.java index 7dffec97..3dc54b50 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleProbe.java @@ -326,7 +326,7 @@ private boolean roundTripPrerequisites(RoundTripRequest request) { ChunkPos chunk = new ChunkPos(key); BlockPos pos = new BlockPos(chunk.getMinBlockX(), 80, chunk.getMinBlockZ()); allChunks &= world.getChunkSource().getChunkNow(chunk.x, chunk.z) != null - && world.isPositionTickingWithEntitiesLoaded(pos) && world.isPositionEntityTicking(chunk); + && Native118ChunkTicking.withEntitiesLoaded(world, pos) && world.isPositionEntityTicking(pos); } request.bindings.clear(); request.prerequisites.clear(); boolean ready = true; @@ -335,16 +335,16 @@ private boolean roundTripPrerequisites(RoundTripRequest request) { BlockPos pos = position(fixture.to); LevelChunk chunk = world.getChunkSource().getChunkNow(pos.getX() >> 4, pos.getZ() >> 4); BlockEntity tile = existingTile(world, pos); - CompoundTag saved = tile == null ? new CompoundTag() : tile.save(new CompoundTag()); + CompoundTag saved = tile == null ? new CompoundTag() : tile.saveWithFullMetadata(); boolean hasTarget = saved.contains("targetX", 99) && saved.contains("targetY", 99) && saved.contains("targetZ", 99); BlockPos target = hasTarget ? new BlockPos(saved.getInt("targetX"), saved.getInt("targetY"), saved.getInt("targetZ")) : pos; Map flags = new LinkedHashMap<>(); flags.put("allFixtureChunksReady", allChunks); flags.put("chunkLoaded", chunk != null); - flags.put("blockAndEntitiesLoaded", world.isPositionTickingWithEntitiesLoaded(pos)); - flags.put("entityTicking", world.isPositionEntityTicking(new ChunkPos(pos))); + flags.put("blockAndEntitiesLoaded", Native118ChunkTicking.withEntitiesLoaded(world, pos)); + flags.put("entityTicking", world.isPositionEntityTicking(pos)); flags.put("targetChunkLoaded", world.getChunkSource().getChunkNow(target.getX() >> 4, target.getZ() >> 4) != null); - flags.put("targetTicking", world.isPositionTickingWithEntitiesLoaded(target)); + flags.put("targetTicking", Native118ChunkTicking.withEntitiesLoaded(world, target)); flags.put("worldAttached", tile != null && tile.getLevel() == world); flags.put("notRemoved", tile != null && !tile.isRemoved()); flags.put("blockTypeValid", tile instanceof PoweredEntity && chunk != null @@ -376,7 +376,7 @@ private void applyRoundTrip(RoundTripRequest request) { Map saved = new LinkedHashMap<>(); for (Fixture fixture : fixtures.values()) { if (fixture.to == null) continue; - CompoundTag tag = request.bindings.get(fixture.id).tile.save(new CompoundTag()); + CompoundTag tag = request.bindings.get(fixture.id).tile.saveWithFullMetadata(); if (!tag.contains("targetX", 99) || !tag.contains("targetY", 99) || !tag.contains("targetZ", 99) || tag.getShort("progress") <= 0) throw new IllegalStateException("Drill must be mid-block " + fixture.id); requireInside(fixture, new int[]{tag.getInt("targetX"), tag.getInt("targetY"), tag.getInt("targetZ")}); @@ -422,8 +422,8 @@ private Map callbackPrerequisites(RoundTripRequest request, Fix flags.put("notRemoved", !binding.tile.isRemoved()); flags.put("blockTypeValid", chunk != null && binding.tile.getType().isValid(chunk.getBlockState(pos))); flags.put("nativeBindingCurrent", binding.current(world)); - flags.put("blockAndEntitiesLoaded", world.isPositionTickingWithEntitiesLoaded(pos)); - flags.put("entityTicking", world.isPositionEntityTicking(new ChunkPos(pos))); + flags.put("blockAndEntitiesLoaded", Native118ChunkTicking.withEntitiesLoaded(world, pos)); + flags.put("entityTicking", world.isPositionEntityTicking(pos)); flags.put("targetChunkLoaded", world.getChunkSource().getChunkNow(target.getX() >> 4, target.getZ() >> 4) != null); flags.put("targetHasChunkAt", world.hasChunkAt(target)); return flags; @@ -545,7 +545,7 @@ private Map witness(ServerLevel world, int[] coordinates) { result.put("state", chunk.getBlockState(pos).toString()); BlockEntity tile = chunk.getBlockEntity(pos, LevelChunk.EntityCreationType.CHECK); if (tile != null) { - result.put("nbt", tile.save(new CompoundTag()).toString()); + result.put("nbt", tile.saveWithFullMetadata().toString()); result.put("tileIdentity", System.identityHashCode(tile)); if (tile instanceof PoweredEntity) { PoweredEntity powered = (PoweredEntity) tile; diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/MachineContext.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/MachineContext.java index 77392c18..42bf1547 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/MachineContext.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/MachineContext.java @@ -85,7 +85,7 @@ void requireTickingFoundation() { for (int x = origin.getX() >> 4; x <= (origin.getX() + 31) >> 4; x++) { for (int z = origin.getZ() >> 4; z <= (origin.getZ() + 31) >> 4; z++) { BlockPos position = new BlockPos(x * 16 + 8, origin.getY(), z * 16 + 8); - require(world.isPositionTickingWithEntitiesLoaded(position) && world.isPositionEntityTicking(position), + require(Native118ChunkTicking.withEntitiesLoaded(world, position) && world.isPositionEntityTicking(position), "Native block and entity sections are ticking before fixture construction: " + x + "," + z); } } @@ -195,12 +195,12 @@ int fluidAmount(int x, int y, int z) { return amount; } - CompoundTag nbt(int x, int y, int z) { return tile(x, y, z).save(new CompoundTag()); } + CompoundTag nbt(int x, int y, int z) { return tile(x, y, z).saveWithFullMetadata(); } void seed(int x, int y, int z, CompoundTag patch) { mutation("nbt", at(x, y, z), patch.toString()); BlockEntity tile = tile(x, y, z); - CompoundTag nbt = tile.save(new CompoundTag()); + CompoundTag nbt = tile.saveWithFullMetadata(); nbt.merge(patch.copy()); tile.load(nbt); tile.setChanged(); @@ -234,7 +234,7 @@ JsonArray snapshot() { row.addProperty("block", state.getBlock().getRegistryName().toString()); row.addProperty("state", state.toString()); BlockEntity tile = world.getBlockEntity(pos); - if (tile != null) row.addProperty("nbt", tile.save(new CompoundTag()).toString()); + if (tile != null) row.addProperty("nbt", tile.saveWithFullMetadata().toString()); rows.add(row); } return rows; diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/MachineOperationalProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/MachineOperationalProbe.java index 036be1d3..6e0490f2 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/MachineOperationalProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/MachineOperationalProbe.java @@ -281,7 +281,7 @@ private void inspectNext() { actual.addProperty("block", world.getBlockState(pos).getBlock().getRegistryName().toString()); actual.addProperty("state", world.getBlockState(pos).toString()); BlockEntity tile = world.getBlockEntity(pos); - if (tile != null) actual.addProperty("nbt", tile.save(new net.minecraft.nbt.CompoundTag()).toString()); + if (tile != null) actual.addProperty("nbt", tile.saveWithFullMetadata().toString()); positions.add(actual); } row.add("positions", positions); inspectionResults.add(row); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/MigrationProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/MigrationProbe.java index db462fd2..159316cc 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/MigrationProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/MigrationProbe.java @@ -84,7 +84,7 @@ private void snapshot(Level world, String label) throws Exception { BlockEntity tile = world.getBlockEntity(pos); if (tile != null) { tile.setChanged(); - CompoundTag nbt = tile.save(new CompoundTag()); + CompoundTag nbt = tile.saveWithFullMetadata(); entry.addProperty("nbt", nbt.toString()); java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream(); net.minecraft.nbt.NbtIo.writeCompressed(nbt, bytes); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117Readiness.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117Readiness.java index 59de9bb6..869f0378 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117Readiness.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117Readiness.java @@ -75,14 +75,14 @@ private void tick(TickEvent.WorldTickEvent event) { for (int x = observation.bounds.minX; x <= observation.bounds.maxX; x++) { for (int z = observation.bounds.minZ; z <= observation.bounds.maxZ; z++) { BlockPos pos = new BlockPos(x * 16, world.getMinBuildHeight(), z * 16); - states.add(new State(x, z, world.isPositionTickingWithEntitiesLoaded(pos), - world.isPositionEntityTicking(new ChunkPos(x, z)))); + states.add(new State(x, z, Native118ChunkTicking.withEntitiesLoaded(world, pos), + world.isPositionEntityTicking(pos))); } } if (!observation.observe(world.getGameTime(), System.nanoTime(), states)) return; JsonObject report = observation.report(); report.addProperty("label", label); - report.addProperty("minecraft", "1.17.1"); + report.addProperty("minecraft", "1.18.2"); report.addProperty("dimension", world.dimension().location().toString()); try { Files.createDirectories(output.toPath().getParent()); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshot.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshot.java index 03cd3d86..28f07ede 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshot.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshot.java @@ -32,8 +32,8 @@ import net.minecraft.world.level.storage.LevelResource; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.EventPriority; -import net.minecraftforge.fmlserverevents.FMLServerStartingEvent; -import net.minecraftforge.fmlserverevents.FMLServerStoppingEvent; +import net.minecraftforge.event.server.ServerStartingEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -68,13 +68,13 @@ private static JsonObject header(MinecraftServer server, String boot, String pha return result; } - private static void starting(FMLServerStartingEvent event) { + private static void starting(ServerStartingEvent event) { MinecraftServer server = event.getServer(); Path runtime = Paths.get("").toAbsolutePath().normalize(); if (!marked(runtime, server.getWorldData().getLevelName())) return; String boot = UUID.randomUUID().toString(); BOOTS.put(server, boot); - JsonObject pending = header(server, boot, "FMLServerStartingEvent"); + JsonObject pending = header(server, boot, "ServerStartingEvent"); pending.addProperty("status", "PENDING_STOP"); try { write(runtime, pending); @@ -83,12 +83,12 @@ private static void starting(FMLServerStartingEvent event) { } } - private static void stopping(FMLServerStoppingEvent event) { + private static void stopping(ServerStoppingEvent event) { MinecraftServer server = event.getServer(); Path runtime = Paths.get("").toAbsolutePath().normalize(); if (!marked(runtime, server.getWorldData().getLevelName())) return; String boot = BOOTS.remove(server); - JsonObject result = header(server, boot, "FMLServerStoppingEvent"); + JsonObject result = header(server, boot, "ServerStoppingEvent"); try { if (boot == null) throw new IllegalStateException("No marked startup was observed for this server"); JsonObject dimensions = new JsonObject(); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2.java index a6941ea0..5daa55fc 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2.java @@ -29,8 +29,8 @@ import net.minecraft.world.level.storage.LevelResource; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.EventPriority; -import net.minecraftforge.fmlserverevents.FMLServerStartingEvent; -import net.minecraftforge.fmlserverevents.FMLServerStoppingEvent; +import net.minecraftforge.event.server.ServerStartingEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; import org.apache.logging.log4j.LogManager; /** Versioned stop-boundary observation. Native no-save helpers are never saved-state witnesses. */ @@ -74,7 +74,7 @@ private static JsonObject header(MinecraftServer server, String boot, String pha return result; } - private static void starting(FMLServerStartingEvent event) { + private static void starting(ServerStartingEvent event) { MinecraftServer server = event.getServer(); Path runtime = Path.of("").toAbsolutePath().normalize(); if (!Native117StoppedEntitiesSnapshot.marked(runtime, server.getWorldData().getLevelName())) return; @@ -82,7 +82,7 @@ private static void starting(FMLServerStartingEvent event) { BOOTS.put(server, boot); try { Files.write(runtime.resolve(CONTRACT), contract()); - JsonObject pending = header(server, boot, "FMLServerStartingEvent"); + JsonObject pending = header(server, boot, "ServerStartingEvent"); pending.addProperty("status", "PENDING_STOP"); Native117StoppedEntitiesSnapshot.write(runtime, pending); } catch (Exception error) { @@ -90,14 +90,14 @@ private static void starting(FMLServerStartingEvent event) { } } - private static void stopping(FMLServerStoppingEvent event) { + private static void stopping(ServerStoppingEvent event) { MinecraftServer server = event.getServer(); Path runtime = Path.of("").toAbsolutePath().normalize(); if (!Native117StoppedEntitiesSnapshot.marked(runtime, server.getWorldData().getLevelName())) return; JsonObject result = new JsonObject(); try { String boot = BOOTS.remove(server); - result = header(server, boot, "FMLServerStoppingEvent"); + result = header(server, boot, "ServerStoppingEvent"); if (boot == null) throw new IllegalStateException("No marked startup for this server"); JsonObject dimensions = new JsonObject(); Set seen = new HashSet<>(); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117TestFlat.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117TestFlat.java deleted file mode 100644 index 381674fc..00000000 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117TestFlat.java +++ /dev/null @@ -1,35 +0,0 @@ -package zone.moddev.mc.advantageworks; - -import java.util.Collections; -import java.util.Optional; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.level.levelgen.FlatLevelSource; -import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; -import net.minecraft.world.level.levelgen.flat.FlatLayerInfo; -import net.minecraft.world.level.levelgen.StructureSettings; -import net.minecraftforge.common.world.ForgeWorldType; -import net.minecraftforge.event.RegistryEvent; - -final class Native117TestFlat { - static final String ID = "advantageworkstest:test_flat"; - - private Native117TestFlat() {} - - static ForgeWorldType createType() { - return new ForgeWorldType((biomes, dimensions, seed, settings) -> { - if (!settings.isEmpty()) throw new IllegalArgumentException("Test flat creation settings must be empty"); - FlatLevelGeneratorSettings flat = new FlatLevelGeneratorSettings( - new StructureSettings(Optional.empty(), Collections.emptyMap()), biomes); - flat.getLayersInfo().add(new FlatLayerInfo(1, Blocks.BEDROCK)); - flat.getLayersInfo().add(new FlatLayerInfo(2, Blocks.DIRT)); - flat.getLayersInfo().add(new FlatLayerInfo(1, Blocks.GRASS_BLOCK)); - flat.updateLayers(); - return new FlatLevelSource(flat); - }).setRegistryName(new ResourceLocation(ID)); - } - - static void register(RegistryEvent.Register event) { - event.getRegistry().register(createType()); - } -} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117WorldgenAccess.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117WorldgenAccess.java index 59d9d414..aadc43a6 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117WorldgenAccess.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117WorldgenAccess.java @@ -32,7 +32,7 @@ final class Native117WorldgenAccess implements WorldgenSamplingJob.WorldAccess { } @Override public CompletableFuture request(int x, int z) { - // Forge 37's public API managedBlocks on the server-thread branch, but not here. + // The public API managedBlocks on the server-thread branch, but not here. return source.getChunkFuture(x, z, ChunkStatus.FULL, true).thenApply(result -> { net.minecraft.world.level.chunk.ChunkAccess access = result.left().orElseThrow(() -> new IllegalStateException("Native FULL chunk unavailable: " + result.right())); @@ -55,8 +55,10 @@ final class Native117WorldgenAccess implements WorldgenSamplingJob.WorldAccess { })); } + @Override public int minBuildHeight() { return world.getMinBuildHeight(); } + @Override public boolean matchesBiome(String expected, int x, int y, int z) { - return WorksController.matchesExpectedBiome(world, expected, world.getBiome(cursor.set(x, y, z))); + return WorksController.matchesExpectedBiome(world, expected, world.getBiome(cursor.set(x, y, z)).value()); } @Override public CompletableFuture requestChunk(int x, int z, boolean counting) { @@ -64,7 +66,7 @@ final class Native117WorldgenAccess implements WorldgenSamplingJob.WorldAccess { private Biome center; @Override public boolean centerMatches(String expected) { - if (center == null) center = world.getBiome(new BlockPos((x << 4) + 8, world.getSeaLevel(), (z << 4) + 8)); + if (center == null) center = world.getBiome(new BlockPos((x << 4) + 8, world.getSeaLevel(), (z << 4) + 8)).value(); return WorksController.matchesExpectedBiome(world, expected, center); } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native118ChunkTicking.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native118ChunkTicking.java new file mode 100644 index 00000000..a1f4b30e --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native118ChunkTicking.java @@ -0,0 +1,15 @@ +package zone.moddev.mc.advantageworks; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; + +final class Native118ChunkTicking { + private Native118ChunkTicking() {} + + static boolean withEntitiesLoaded(ServerLevel world, BlockPos pos) { + // The native combined check became private in 1.18. + long key = ChunkPos.asLong(pos); + return world.areEntitiesLoaded(key) && world.getChunkSource().isPositionTicking(key); + } +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native118TestFlat.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native118TestFlat.java new file mode 100644 index 00000000..0eed8e6f --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native118TestFlat.java @@ -0,0 +1,40 @@ +package zone.moddev.mc.advantageworks; + +import java.util.List; +import java.util.Optional; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.levelgen.FlatLevelSource; +import net.minecraft.world.level.levelgen.flat.FlatLayerInfo; +import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraftforge.common.world.ForgeWorldPreset; +import net.minecraftforge.event.RegistryEvent; + +final class Native118TestFlat { + static final String ID = "advantageworkstest:test_flat"; + + private Native118TestFlat() {} + + static ForgeWorldPreset createType() { + return new ForgeWorldPreset((registries, seed, settings) -> { + // Forge 40 passes the level-type identifier rather than generator JSON. + if (!settings.isEmpty() && !settings.equals(ID)) { + throw new IllegalArgumentException("Unexpected settings for the test flat preset"); + } + Optional> structures = Optional.of(HolderSet.direct(List.of())); + var flat = new FlatLevelGeneratorSettings(structures, registries.registryOrThrow(Registry.BIOME_REGISTRY)); + flat.getLayersInfo().add(new FlatLayerInfo(1, Blocks.BEDROCK)); + flat.getLayersInfo().add(new FlatLayerInfo(2, Blocks.DIRT)); + flat.getLayersInfo().add(new FlatLayerInfo(1, Blocks.GRASS_BLOCK)); + flat.updateLayers(); + return new FlatLevelSource(registries.registryOrThrow(Registry.STRUCTURE_SET_REGISTRY), flat); + }).setRegistryName(new ResourceLocation(ID)); + } + + static void register(RegistryEvent.Register event) { + event.getRegistry().register(createType()); + } +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeHarvestProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeHarvestProbe.java index 10f978da..1cffdbf3 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeHarvestProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeHarvestProbe.java @@ -218,7 +218,7 @@ private static void run(ServerLevel world, String label) throws Exception { harvestDrops(world, new AABB(POS).inflate(2)).forEach(ItemEntity::discard); JsonObject report = new JsonObject(); report.addProperty("label", label); - report.addProperty("minecraft", "1.17.1"); + report.addProperty("minecraft", "1.18.2"); report.addProperty("worldTime", world.getGameTime()); report.addProperty("fixtureIsolation", "native117-live-drops-v1"); report.addProperty("status", java.util.stream.StreamSupport.stream(rows.spliterator(), false) diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeInteropProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeInteropProbe.java index 9be1b0bb..ff9e942c 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeInteropProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeInteropProbe.java @@ -203,7 +203,7 @@ private static void locks(ServerLevel world, JsonArray checks) { input.setItem(0, new ItemStack(net.minecraft.world.item.Items.DIRT, 16)); TileEntityConveyor belt = (TileEntityConveyor) world.getBlockEntity(BELT); BlockEntity chest = world.getBlockEntity(BELT.west()); - CompoundTag state = chest.save(new CompoundTag()); + CompoundTag state = chest.saveWithFullMetadata(); state.putString("Lock", "AdvantageInteropKey"); chest.load(state); check(checks, "locked-chest/conveyor-refuses-extraction", () -> { diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProcessProgression.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProcessProgression.java index ba31f88c..05f2af2f 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProcessProgression.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProcessProgression.java @@ -12,8 +12,11 @@ import java.util.Map; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeType; import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.core.Direction; @@ -50,7 +53,7 @@ static void smelt(ServerLevel world, JsonObject fixture, String recipe, ItemStac && !output.isEmpty() && id(output).equals(id(expected)) && output.getCount() == expected.getCount(), "Live furnace failed " + recipe + ": input=" + furnace.getItem(0) + ", fuel=" + furnace.getItem(1) + ", output=" + output - + ", expected=" + expected + ", saved=" + furnace.save(new net.minecraft.nbt.CompoundTag())); + + ", expected=" + expected + ", saved=" + furnace.saveWithFullMetadata()); record("live_furnace:" + recipe, input.getCount(), output.getCount()); } @@ -93,12 +96,12 @@ static void hammer(ServerLevel world, ServerPlayer player, Map hammerClass = hammer.getItem().getClass(); if (!hammerClass.getName().equals("zone.moddev.mc.basemetals.content.CrackhammerItem")) throw new IllegalStateException("Unexpected hammer class " + hammerClass.getName()); - java.lang.reflect.Method selector = net.minecraftforge.fml.util.ObfuscationReflectionHelper.findMethod( - hammerClass, "findRecipe", net.minecraft.world.level.Level.class, ItemStack.class); - Object selected = selector.invoke(null, world, input.copy()); + RecipeType type = net.minecraft.core.Registry.RECIPE_TYPE + .getOptional(new ResourceLocation("basemetals", "crushing")).orElseThrow(); + Recipe selected = world.getRecipeManager().getRecipeFor( + (RecipeType>) type, new SimpleContainer(input.copy()), world).orElse(null); selection.addProperty("status", selected == null ? "NO_MATCH" : "MATCH"); selection.addProperty("matched", selected != null); if (selected != null) { - net.minecraft.world.item.crafting.Recipe actual = - (net.minecraft.world.item.crafting.Recipe) selected; - selection.addProperty("id", actual.getId().toString()); - selection.addProperty("class", actual.getClass().getName()); - selection.addProperty("resultNbt", actual.getResultItem().save(new net.minecraft.nbt.CompoundTag()).toString()); + selection.addProperty("id", selected.getId().toString()); + selection.addProperty("class", selected.getClass().getName()); + selection.addProperty("resultNbt", selected.getResultItem().save(new net.minecraft.nbt.CompoundTag()).toString()); } - } catch (ReflectiveOperationException | RuntimeException | LinkageError error) { + } catch (RuntimeException | LinkageError error) { selection.addProperty("status", "OBSERVER_ERROR"); selection.addProperty("errorType", error.getClass().getName()); selection.addProperty("error", error.toString()); @@ -158,8 +161,8 @@ private static JsonObject hammerSnapshot(ServerLevel world, ServerPlayer player, sample.addProperty("sourceNbt", source.getItem().save(new net.minecraft.nbt.CompoundTag()).toString()); sample.addProperty("uuidLookupIsSource", world.getEntity(source.getUUID()) == source); sample.addProperty("entitiesLoaded", world.areEntitiesLoaded(net.minecraft.world.level.ChunkPos.asLong(pos))); - sample.addProperty("blockAndEntitiesTicking", world.isPositionTickingWithEntitiesLoaded(pos)); - sample.addProperty("entityTicking", world.isPositionEntityTicking(new net.minecraft.world.level.ChunkPos(pos))); + sample.addProperty("blockAndEntitiesTicking", Native118ChunkTicking.withEntitiesLoaded(world, pos)); + sample.addProperty("entityTicking", world.isPositionEntityTicking(pos)); sample.addProperty("clientSide", world.isClientSide); sample.addProperty("creative", player.isCreative()); sample.addProperty("shift", player.isShiftKeyDown()); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProgressionProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProgressionProbe.java index 4676cfdf..475704b6 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProgressionProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProgressionProbe.java @@ -99,11 +99,17 @@ private void audit(MinecraftServer server, ServerLevel world, String label) thro if (item != null) acquire(known, player, new ItemStack(item), "natural_resource", Collections.emptyList()); } JsonArray loot = new JsonArray(); + var overriddenTable = server.getLootTables().get(new ResourceLocation("minecraft:chests/simple_dungeon")); for (int seed = 0; seed < 8; seed++) { - List overridden = server.getLootTables().get( - new ResourceLocation("minecraft:chests/simple_dungeon")).getRandomItems(chestContext(world, seed)); - require(overridden.size() == 1 && id(overridden.get(0)).equals("minecraft:dirt") - && overridden.get(0).getCount() == 1, "Player loot data-pack override was changed"); + List raw = new ArrayList<>(); + overriddenTable.getRandomItemsRaw(chestContext(world, seed), raw::add); + require(raw.size() == 1 && id(raw.get(0)).equals("minecraft:dirt") + && raw.get(0).getCount() == 1, "Player loot data-pack override was changed"); + // Base Metals has its own Forge global modifier. It may add loot after the table runs. + List gameplay = overriddenTable.getRandomItems(chestContext(world, seed)); + require(gameplay.stream().filter(stack -> id(stack).equals("minecraft:dirt")).mapToInt(ItemStack::getCount).sum() == 1 + && gameplay.stream().noneMatch(ProgressionProbe::advantage), + "The live loot pipeline changed the override's contents or added Advantage parts"); } if (PowerAdvantage.recipeMode == RecipeMode.APOCALYPTIC) { for (int i = 0; i < 4096; i++) { @@ -217,12 +223,13 @@ private void audit(MinecraftServer server, ServerLevel world, String label) thro report.add("blockedRecipes", missingRecipes); report.add("lootSeeds", loot); report.addProperty("lootDataPackOverrideVerified", true); + report.addProperty("lootOverrideWithModifiersVerified", true); report.add("processWitnesses", ProcessProgression.witnesses()); JsonObject tags = new JsonObject(); for (String name : new String[] {"wires", "dusts/carbon", "rubber", "plastic", "circuit_boards", "solder"}) { List members = new ArrayList<>(); - for (Item item : ItemTags.getAllTags().getTagOrEmpty(new ResourceLocation("forge", name)).getValues()) - members.add(item.getRegistryName().toString()); + for (var item : net.minecraft.core.Registry.ITEM.getTagOrEmpty(ItemTags.create(new ResourceLocation("forge", name)))) + members.add(item.value().getRegistryName().toString()); Collections.sort(members); tags.add(name, JSON.toJsonTree(members)); } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeUnlockProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeUnlockProbe.java index fb6029ba..fb95eb9e 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeUnlockProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeUnlockProbe.java @@ -60,7 +60,7 @@ static Connection nativeTransport() { return connection; } - private void stopped(net.minecraftforge.fmlserverevents.FMLServerStoppedEvent event) { + private void stopped(net.minecraftforge.event.server.ServerStoppedEvent event) { if (transport != null) ((io.netty.channel.embedded.EmbeddedChannel) transport.channel()).finishAndReleaseAll(); } @@ -191,7 +191,7 @@ private void run(MinecraftServer server, ServerLevel world, String operation, St } else if (operation.equals("restore")) { require(server.getPlayerList().load(player) != null, "No saved test player"); require(transport.channel().isActive(), "Native recipe-login channel is not active"); - require(net.minecraftforge.fmllegacy.network.NetworkHooks.getConnectionData(transport) == null, + require(net.minecraftforge.network.NetworkHooks.getConnectionData(transport) == null, "Server-only probe must not impersonate a remote modded-client handshake"); // Dispatch the real Forge event to exercise all three namespaces' restoration. MinecraftForge.EVENT_BUS.post(new PlayerEvent.PlayerLoggedInEvent(player)); @@ -257,8 +257,9 @@ private static boolean matchesInventory(InventoryChangeTrigger.TriggerInstance c private static List choices(JsonObject predicate) { if (predicate.has("item")) return Collections.singletonList(item(predicate.get("item").getAsString())); - List items = new ArrayList<>(ItemTags.getAllTags().getTagOrEmpty( - new ResourceLocation(predicate.get("tag").getAsString())).getValues()); + List items = new ArrayList<>(); + for (var holder : net.minecraft.core.Registry.ITEM.getTagOrEmpty( + ItemTags.create(new ResourceLocation(predicate.get("tag").getAsString())))) items.add(holder.value()); items.sort(Comparator.comparing(candidate -> candidate.getRegistryName().toString())); return items; } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/RedstoneReloadProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/RedstoneReloadProbe.java index 78a56311..3d8d7c2a 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/RedstoneReloadProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/RedstoneReloadProbe.java @@ -137,7 +137,7 @@ private void tick(TickEvent.WorldTickEvent event) { seedTrace.add(observation); boolean ready = true; for (Witness witness : witnesses) { - CompoundTag tag = tile(witness).save(new CompoundTag()); + CompoundTag tag = tile(witness).saveWithFullMetadata(); ready &= tag.getShort("progress") > 0 && tag.contains("targetX", 99) && ((PoweredEntity) tile(witness)).getEnergy(new ConduitType("electricity")) > 24; } @@ -186,8 +186,8 @@ private boolean ready(String stage) { BlockEntity tile = tile(witness); boolean ticking = world.getChunkSource().isPositionTicking(new net.minecraft.world.level.ChunkPos(witness.pos).toLong()); boolean listed = NativeBlockEntityTicks.contains(world, tile); - boolean blockAndEntitiesLoaded = world.isPositionTickingWithEntitiesLoaded(witness.pos); - boolean entityTicking = world.isPositionEntityTicking(new net.minecraft.world.level.ChunkPos(witness.pos)); + boolean blockAndEntitiesLoaded = Native118ChunkTicking.withEntitiesLoaded(world, witness.pos); + boolean entityTicking = world.isPositionEntityTicking(witness.pos); Map row = new LinkedHashMap<>(); row.put("id", witness.id); row.put("pos", coordinates(witness.pos)); @@ -273,7 +273,7 @@ private List> snapshots() { List> result = new ArrayList<>(); for (Witness witness : witnesses) { BlockEntity tile = tile(witness); - CompoundTag nbt = tile.save(new CompoundTag()); + CompoundTag nbt = tile.saveWithFullMetadata(); BlockState state = world.getBlockState(witness.pos); Map row = new LinkedHashMap<>(); row.put("id", witness.id); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksController.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksController.java index 081a129e..d050d9fb 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksController.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksController.java @@ -39,7 +39,7 @@ import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.TickEvent; import net.minecraftforge.fml.ModList; -import net.minecraftforge.fmlserverevents.FMLServerStoppingEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; import net.minecraftforge.registries.ForgeRegistries; import net.minecraft.world.level.material.Fluid; import cyano.poweradvantage.api.fluid.FluidStack; @@ -80,7 +80,7 @@ static WorksController getInstance() { synchronized void loadDefinition() { if (definition != null) return; - definition = WorksDefinitionLoader.loadNative117(); + definition = WorksDefinitionLoader.loadNative118(); for (WorksDefinition.Station station : definition.stations) { stations.put(station.id.toUpperCase(Locale.ROOT), station); statuses.put(station.id, "NOT_BUILT"); @@ -252,7 +252,7 @@ private synchronized void worldgenTick(TickEvent.ServerTickEvent event) { } } - private synchronized void worldgenStopping(FMLServerStoppingEvent event) { + private synchronized void worldgenStopping(ServerStoppingEvent event) { worldgenJob.release(); } @@ -355,7 +355,7 @@ private List> snapshot(ServerLevel world, WorksDefinition.St block.put("block", name == null ? "unknown" : name.toString()); block.put("state", state.toString()); BlockEntity tile = world.getBlockEntity(pos); - if (tile != null) block.put("nbt", tile.save(new CompoundTag()).toString()); + if (tile != null) block.put("nbt", tile.saveWithFullMetadata().toString()); snapshot.add(block); } return snapshot; @@ -394,7 +394,7 @@ private boolean evaluateAssertion(ServerLevel world, WorksDefinition.Station sta for (int[] position : assertion.positions) { BlockEntity source = world.getBlockEntity(absolute(station, position)); if (source == null) return false; - total += AssertionChecks.fluidAmount(source.save(new CompoundTag()), assertion.nbtPath, assertion.fluid); + total += AssertionChecks.fluidAmount(source.saveWithFullMetadata(), assertion.nbtPath, assertion.fluid); } return compare(total, assertion.operator, assertion.value); } @@ -442,18 +442,18 @@ && compare(source.getAnalogOutputSignal(state, world, pos), } if ("nbtNumber".equals(assertion.type)) { if (tile == null) return false; - CompoundTag nbt = tile.save(new CompoundTag()); + CompoundTag nbt = tile.saveWithFullMetadata(); Number number = numberAt(nbt, assertion.nbtPath); return number != null && compare(number.doubleValue(), assertion.operator, assertion.value); } if ("nbtString".equals(assertion.type)) { if (tile == null) return false; - String text = stringAt(tile.save(new CompoundTag()), assertion.nbtPath); + String text = stringAt(tile.saveWithFullMetadata(), assertion.nbtPath); return assertion.expectedText != null && assertion.expectedText.equals(text); } if ("energy".equals(assertion.type)) { if (tile == null) return false; - int[] energy = tile.save(new CompoundTag()).getIntArray("Energy"); + int[] energy = tile.saveWithFullMetadata().getIntArray("Energy"); int index = assertion.energyIndex == null ? 0 : assertion.energyIndex; return index >= 0 && index < energy.length && compare(Float.intBitsToFloat(energy[index]), assertion.operator, assertion.value); @@ -749,7 +749,7 @@ private void mergeTileNbt(ServerLevel world, BlockPos pos, JsonObject supplied) BlockEntity tile = world.getBlockEntity(pos); if (tile == null) throw new IllegalStateException("No tile entity at " + pos); try { - CompoundTag base = tile.save(new CompoundTag()); + CompoundTag base = tile.saveWithFullMetadata(); CompoundTag additions = JsonNbtConverter.toCompound(supplied); base.merge(additions); base.putInt("x", pos.getX()); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksDefinitionLoader.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksDefinitionLoader.java index 8ef7378a..442246b6 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksDefinitionLoader.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksDefinitionLoader.java @@ -38,6 +38,10 @@ static WorksDefinition loadNative117() { return load("/assets/advantageworkstest/works/works-1.17.1.json", "1.17.1"); } + static WorksDefinition loadNative118() { + return load("/assets/advantageworkstest/works/works-1.18.2.json", "1.18.2"); + } + static WorksDefinition load(String resource, String minecraft) { JsonObject target = readResource(resource); if (target.get("format").getAsInt() != 1 || !minecraft.equals(target.get("minecraft").getAsString()) diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksResultWriter.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksResultWriter.java index f230b151..5e037290 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksResultWriter.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksResultWriter.java @@ -31,7 +31,7 @@ File write(String stationId, String kind, Map payload) { Map document = new LinkedHashMap<>(); document.put("format", 1); document.put("recordedAtUtc", utcNow()); - document.put("minecraft", "1.17.1"); + document.put("minecraft", "1.18.2"); document.put("forge", net.minecraftforge.versions.forge.ForgeVersion.getVersion()); document.put("mods", loadedMods()); document.put("station", stationId); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorldgenSamplingJob.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorldgenSamplingJob.java index 3f586b0b..151adccf 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorldgenSamplingJob.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorldgenSamplingJob.java @@ -18,6 +18,7 @@ final class WorldgenSamplingJob { private static final int SEARCH_SIDE = 129; interface WorldAccess { + default int minBuildHeight() { return 0; } boolean matchesBiome(String expected, int x, int y, int z); CompletableFuture requestChunk(int chunkX, int chunkZ, boolean counting); void releaseChunk(int chunkX, int chunkZ); @@ -46,7 +47,7 @@ void release() { } private final WorldAccess world; - private final int spawnX, spawnY, spawnZ, seaLevel, radius; + private final int spawnX, spawnY, spawnZ, seaLevel, radius, minBuildHeight; private final Set chunks = new LinkedHashSet<>(); private final Set populationChunks = new LinkedHashSet<>(); private final Map expectedBiomes = new LinkedHashMap<>(); @@ -72,6 +73,7 @@ void release() { this.spawnZ = spawnZ; this.seaLevel = seaLevel; this.radius = radius; + minBuildHeight = world.minBuildHeight(); squareX = spawnX >> 4; squareZ = spawnZ >> 4; centers.put("spawn", squareX + "," + squareZ); @@ -245,7 +247,7 @@ private void countChunk(SampleChunk chunk, int chunkX, int chunkZ) { int minX = chunkX << 4, minZ = chunkZ << 4; for (int localX = 0; localX < 16; localX++) { for (int localZ = 0; localZ < 16; localZ++) { - for (int y = 0; y <= 96; y++) { + for (int y = minBuildHeight; y <= 96; y++) { int x = minX + localX, z = minZ + localZ; String name = chunk.blockName(x, y, z); if (name == null || !counts.containsKey(name)) continue; diff --git a/test-rig/src/main/resources/META-INF/mods.toml b/test-rig/src/main/resources/META-INF/mods.toml index 34f720ba..dc07cc3d 100644 --- a/test-rig/src/main/resources/META-INF/mods.toml +++ b/test-rig/src/main/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[37,)" +loaderVersion="[40,)" license="LGPL-3.0-only" [[mods]] @@ -12,20 +12,20 @@ description='''Server-side commissioning harness for the Advantage family. Not f [[dependencies.advantageworkstest]] modId="basepower" mandatory=true -versionRange="[2.4.9.117011,)" +versionRange="[2.4.10.118021,)" ordering="AFTER" side="SERVER" [[dependencies.advantageworkstest]] modId="forge" mandatory=true -versionRange="[37.1.1,)" +versionRange="[40.3.0,41)" ordering="NONE" side="SERVER" [[dependencies.advantageworkstest]] modId="minecraft" mandatory=true -versionRange="[1.17.1]" +versionRange="[1.18.2]" ordering="NONE" side="SERVER" diff --git a/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.18.2.json b/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.18.2.json new file mode 100644 index 00000000..17123ff9 --- /dev/null +++ b/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.18.2.json @@ -0,0 +1,6 @@ +{ + "format": 1, + "minecraft": "1.18.2", + "title": "Advantage Works 1.18.2 Commissioning Ground", + "stationSource": "/assets/advantageworkstest/works/works-1.13.2.json" +} diff --git a/test-rig/src/main/resources/pack.mcmeta b/test-rig/src/main/resources/pack.mcmeta index 420e95ee..86489f25 100644 --- a/test-rig/src/main/resources/pack.mcmeta +++ b/test-rig/src/main/resources/pack.mcmeta @@ -1,6 +1,8 @@ { "pack": { - "description": "Advantage Works 1.17.1 test resources", - "pack_format": 7 + "description": "Advantage Works 1.18.2 test resources", + "pack_format": 9, + "forge:resource_pack_format": 8, + "forge:data_pack_format": 9 } } diff --git a/test-rig/src/runner/java/zone/moddev/mc/advantageworks/runner/WorksRunner.java b/test-rig/src/runner/java/zone/moddev/mc/advantageworks/runner/WorksRunner.java index 9e24ff31..06680f15 100644 --- a/test-rig/src/runner/java/zone/moddev/mc/advantageworks/runner/WorksRunner.java +++ b/test-rig/src/runner/java/zone/moddev/mc/advantageworks/runner/WorksRunner.java @@ -67,7 +67,7 @@ private WorksRunner() { public static void main(String[] args) throws Exception { Arguments arguments = Arguments.parse(args); - Profile profile = readNative117Profile(arguments.profile); + Profile profile = readNative118Profile(arguments.profile); if (arguments.serverPort != null) profile.serverPort = arguments.serverPort; File testRig = findTestRig(arguments.profile); File powerRoot = testRig.getParentFile().getCanonicalFile(); @@ -80,7 +80,7 @@ public static void main(String[] args) throws Exception { runtimeJavaHome(profile); if (profile.preparedSeed != null) packagedCommand(profile, profile.preparedSeed.root.toFile()); } - System.out.println("Native 1.17.1 profile preflight only (no world opened): " + GSON.toJson(inputs)); + System.out.println("Native " + profile.minecraftVersion + " profile preflight only (no world opened): " + GSON.toJson(inputs)); return; } String stamp = timestamp(); @@ -247,7 +247,7 @@ private static Process launch(Profile profile, File powerRoot, File runtime, Fil command.add("--no-daemon"); workingDirectory = powerRoot; } else { - if ("1.17.1".equals(profile.minecraftVersion)) { + if (modularForge(profile)) { command.addAll(packagedCommand(profile, runtime)); } else if (profile.launchCommand != null && !profile.launchCommand.isEmpty()) { for (String token : profile.launchCommand) { @@ -293,7 +293,7 @@ private static Process launch(Profile profile, File powerRoot, File runtime, Fil static List packagedCommand(Profile profile, File runtime) throws Exception { if (profile.launchCommand != null && !profile.launchCommand.isEmpty()) { - throw new IllegalArgumentException("Native117 packaged launches cannot override the pinned command"); + throw new IllegalArgumentException("Packaged launches cannot override the pinned command"); } File java = javaExecutable(runtimeJavaHome(profile)); String launcher = serverLauncherPath(profile); @@ -315,13 +315,20 @@ static void requireModularLauncher(File runtime, String launcher) throws Excepti PreparedServerSeed.requirePlain(root); Path args = root.resolve(launcher); PreparedServerSeed.requirePlain(args); + boolean native118 = launcher.startsWith("libraries/net/minecraftforge/forge/1.18.2-40.3.0/"); Set expected = new LinkedHashSet<>(Arrays.asList( "--add-modules ALL-MODULE-PATH", "--add-opens java.base/java.util.jar=cpw.mods.securejarhandler", "--add-exports java.base/sun.security.util=cpw.mods.securejarhandler", "-DlibraryDirectory=libraries", "cpw.mods.bootstraplauncher.BootstrapLauncher", - "--launchTarget forgeserver", "--fml.forgeVersion 37.1.1", "--fml.mcVersion 1.17.1", - "--fml.forgeGroup net.minecraftforge", "--fml.mcpVersion 20210706.113038")); + "--launchTarget forgeserver", native118 ? "--fml.forgeVersion 40.3.0" : "--fml.forgeVersion 37.1.1", + native118 ? "--fml.mcVersion 1.18.2" : "--fml.mcVersion 1.17.1", + "--fml.forgeGroup net.minecraftforge", + native118 ? "--fml.mcpVersion 20220404.173914" : "--fml.mcpVersion 20210706.113038")); + if (native118) expected.addAll(Arrays.asList( + "--add-opens java.base/java.lang.invoke=cpw.mods.securejarhandler", + "--add-exports jdk.naming.dns/com.sun.jndi.dns=java.naming", + "-Djava.net.preferIPv6Addresses=system")); Set seen = new LinkedHashSet<>(); for (String line : Files.readAllLines(args, StandardCharsets.UTF_8)) { if (line.isEmpty()) continue; @@ -381,6 +388,16 @@ static final class Boot { /** Captures each process separately; no earlier boot can supply a later boot's evidence. */ static final class BootLogs { + private static final String NETTY_DEBUG = "[io.netty.util.internal.PlatformDependent0/]: "; + private static final Map NETTY_PROBES = Map.of( + "direct buffer constructor: unavailable", + "java.lang.UnsupportedOperationException: Reflective setAccessible(true) disabled", + "jdk.internal.misc.Unsafe.allocateUninitializedArray(int): unavailable", + "java.lang.IllegalAccessException: class io.netty.util.internal.PlatformDependent0$6 (in module io.netty.all) cannot access class jdk.internal.misc.Unsafe (in module java.base) because module java.base does not export jdk.internal.misc to module io.netty.all"); + private static final Pattern NETTY_HEADER = Pattern.compile( + "^\\[[^\\]]+\\] \\[[^\\]]+/DEBUG\\] " + Pattern.quote(NETTY_DEBUG) + "(.*)$"); + private static final Pattern NETTY_FRAME = Pattern.compile( + "^\\tat [\\w.$/<>]+\\([^()]*\\)(?: ~?\\[[^\\]]*\\])?$"); static final Pattern ERROR = Pattern.compile("/(?:ERROR|FATAL)\\]|\\[(?:ERROR|FATAL)\\]|^(?:ERROR|FATAL) StatusLogger|" + "\\b(?:[\\w$]+\\.)*[\\w$]*(?:Exception|Error)\\b|Exception in thread|Caused by:|" + "Failed to reload data packs|Encountered an unexpected exception|Advantage Works command failed|" @@ -443,9 +460,27 @@ void archive() throws Exception { List> nonFatalDiagnostics = new ArrayList<>(); try (BufferedReader reader = Files.newBufferedReader(target.toPath(), StandardCharsets.UTF_8)) { String line; + String nettyProbe = null; + boolean nettyFrames = false; while ((line = reader.readLine()) != null) { ready |= line.contains("Done ("); stopped |= line.contains("Stopping server"); + java.util.regex.Matcher header = NETTY_HEADER.matcher(line); + if (header.matches() && NETTY_PROBES.containsKey(header.group(1))) { + nettyProbe = header.group(1); + nettyFrames = false; + continue; + } + if (nettyProbe != null && line.equals(NETTY_PROBES.get(nettyProbe))) { + nonFatalDiagnostics.add(Map.of("line", line, "reason", + "Netty DEBUG optional capability probe: " + nettyProbe)); + nettyProbe = null; + nettyFrames = true; + continue; + } + if (nettyFrames && NETTY_FRAME.matcher(line).matches()) continue; + nettyProbe = null; + nettyFrames = false; if (MIXIN_DEBUG_CLEANUP.matcher(line).matches()) { Map diagnostic = new LinkedHashMap<>(); diagnostic.put("line", line); @@ -747,7 +782,9 @@ static PreparedServerSeed preparedServerSeed(File testRig, Profile profile, Argu } return new PreparedServerSeed(preparedPath(testRig, arguments.preparedServerSeed), preparedPath(testRig, arguments.preparedServerSeedInventory), arguments.preparedServerSeedSha256, - serverLauncherPath(profile), "1.17.1".equals(profile.minecraftVersion) + serverLauncherPath(profile), "1.18.2".equals(profile.minecraftVersion) + ? "libraries/net/minecraft/server/1.18.2-20220404.173914/server-1.18.2-20220404.173914-extra.jar" + : "1.17.1".equals(profile.minecraftVersion) ? "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-extra.jar" : "minecraft_server." + profile.minecraftVersion + ".jar"); } @@ -1053,16 +1090,22 @@ private static void download(String address, File destination) throws Exception } static String serverLauncherPath(Profile profile) { - if (!"1.17.1".equals(profile.minecraftVersion)) return "forge-" + profile.forge.version + ".jar"; + if (!modularForge(profile)) return "forge-" + profile.forge.version + ".jar"; boolean windows = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win"); return "libraries/net/minecraftforge/forge/" + profile.forge.version + "/" + (windows ? "win_args.txt" : "unix_args.txt"); } + private static boolean modularForge(Profile profile) { + return "1.17.1".equals(profile.minecraftVersion) || "1.18.2".equals(profile.minecraftVersion); + } + private static File runtimeJavaHome(Profile profile) throws Exception { - if (!"1.17.1".equals(profile.minecraftVersion)) return java8Home(profile); - String configured = profile.java16Home; - if (configured == null || configured.trim().isEmpty()) configured = System.getenv("ADVANTAGE_WORKS_JAVA16_HOME"); + if (!modularForge(profile)) return java8Home(profile); + boolean native118 = "1.18.2".equals(profile.minecraftVersion); + String variable = native118 ? "ADVANTAGE_WORKS_JAVA17_HOME" : "ADVANTAGE_WORKS_JAVA16_HOME"; + String configured = native118 ? profile.java17Home : profile.java16Home; + if (configured == null || configured.trim().isEmpty()) configured = System.getenv(variable); File home = configured == null || configured.trim().isEmpty() ? new File(System.getProperty("java.home")) : new File(expand(configured)); File release = new File(home, "release"); @@ -1070,9 +1113,11 @@ private static File runtimeJavaHome(Profile profile) throws Exception { if (release.isFile()) { try (InputStream input = Files.newInputStream(release.toPath())) { metadata.load(input); } } - if (!javaExecutable(home).isFile() || !"\"16.0.2\"".equals(metadata.getProperty("JAVA_VERSION")) - || !"\"Temurin-16.0.2+7\"".equals(metadata.getProperty("IMPLEMENTOR_VERSION"))) { - throw new IllegalStateException("Set ADVANTAGE_WORKS_JAVA16_HOME to the pinned Temurin 16.0.2+7 runtime: " + home); + String javaVersion = native118 ? "17.0.20.1" : "16.0.2"; + String temurin = native118 ? "17.0.20.1+1" : "16.0.2+7"; + if (!javaExecutable(home).isFile() || !("\"" + javaVersion + "\"").equals(metadata.getProperty("JAVA_VERSION")) + || !("\"Temurin-" + temurin + "\"").equals(metadata.getProperty("IMPLEMENTOR_VERSION"))) { + throw new IllegalStateException("Set " + variable + " to the pinned Temurin " + temurin + " runtime: " + home); } return home.getCanonicalFile(); } @@ -1218,6 +1263,19 @@ static Profile readNative117Profile(File file) throws Exception { return profile; } + static Profile readNative118Profile(File file) throws Exception { + Profile profile = readProfile(file, "1.18.2", "1.18.2-40.3.0"); + if (!"UNQUALIFIED".equals(profile.qualificationStatus)) { + throw new IllegalArgumentException("The 1.18.2 destination must start UNQUALIFIED"); + } + for (ModInput input : profile.mods) { + if (input.version == null || !input.version.endsWith(".118021")) { + throw new IllegalArgumentException("Non-1.18.2 destination mod version: " + input.id); + } + } + return profile; + } + static Profile readNative116Profile(File file) throws Exception { Profile profile = readProfile(file, "1.16.5", "1.16.5-36.2.34"); if (!"UNQUALIFIED".equals(profile.qualificationStatus)) { @@ -1255,9 +1313,9 @@ static Profile readProfile(File file, String minecraft, String forge) throws Exc } static void requireInputPins(Profile profile) { - if ("1.17.1".equals(profile.minecraftVersion) && "packaged".equals(profile.mode) + if (modularForge(profile) && "packaged".equals(profile.mode) && profile.launchCommand != null && !profile.launchCommand.isEmpty()) { - throw new IllegalArgumentException("Native117 packaged launches cannot override the pinned command"); + throw new IllegalArgumentException("Packaged launches cannot override the pinned command"); } for (ModInput input : profile.mods) { if (input.sha256 == null || !input.sha256.matches("[A-Fa-f0-9]{64}")) { @@ -1747,6 +1805,7 @@ static final class Profile { List stableWorldgenCountExclusions = new ArrayList<>(); String java8Home; String java16Home; + String java17Home; String gradleJavaHome; String recipeMode = "NORMAL"; String sourceCommit; diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/ConnectedProbeObservationTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/ConnectedProbeObservationTest.java index b09458cf..48eebe07 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/ConnectedProbeObservationTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/ConnectedProbeObservationTest.java @@ -47,7 +47,7 @@ private static void observe(CompoundTag data) throws Exception { Field persistent = BlockEntity.class.getDeclaredField("customTileData"); persistent.setAccessible(true); persistent.set(tile, data); - CompoundTag before = tile.save(new CompoundTag()).copy(); + CompoundTag before = tile.saveWithFullMetadata().copy(); ObservationWorld world = allocate(ObservationWorld.class); world.tile = tile; ConnectedProbe probe = new ConnectedProbe(); @@ -65,7 +65,7 @@ private static void observe(CompoundTag data) throws Exception { JsonObject row = probe.snapshot(definition).get(0).getAsJsonObject(); assertSame("Observation must not allocate or replace live persistent data", data, persistent.get(tile)); assertEquals("Observation must not call the allocating getter", 0, tile.persistentReads); - assertEquals(before, tile.save(new CompoundTag())); + assertEquals(before, tile.saveWithFullMetadata()); assertEquals(before.toString(), row.get("nbt").getAsString()); assertEquals(before, NbtIo.readCompressed(new ByteArrayInputStream( Base64.getDecoder().decode(row.get("nbtBase64").getAsString())))); diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117AsyncWorldgenSourceTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117AsyncWorldgenSourceTest.java index 6a6a253e..e774e4c3 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117AsyncWorldgenSourceTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117AsyncWorldgenSourceTest.java @@ -75,7 +75,7 @@ public class Native117AsyncWorldgenSourceTest { @Test public void serverStopClosesTheOwnedJobAndDispatchExecutor() throws Exception { String controller = source("WorksController.java"); assertTrue(controller.contains("addListener(this::worldgenStopping)")); - int start = controller.indexOf("void worldgenStopping(FMLServerStoppingEvent"); + int start = controller.indexOf("void worldgenStopping(ServerStoppingEvent"); int end = controller.indexOf("static boolean matchesExpectedBiome", start); assertTrue(start >= 0 && end > start); assertTrue(controller.substring(start, end).contains("worldgenJob.release()")); diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117CommandRegistrationTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117CommandRegistrationTest.java index 9ba8bdc8..7914632d 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117CommandRegistrationTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117CommandRegistrationTest.java @@ -16,7 +16,7 @@ public class Native117CommandRegistrationTest { assertTrue(name, source.contains("import net.minecraftforge.event.RegisterCommandsEvent;")); assertTrue(name, source.contains("(RegisterCommandsEvent event)")); assertTrue(name, source.contains("event.getDispatcher()")); - assertFalse(name, source.contains("FMLServerStartingEvent")); + assertFalse(name, source.contains("ServerStartingEvent")); } } } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117HammerQueryGateTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117HammerQueryGateTest.java index edb78088..eff7763b 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117HammerQueryGateTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117HammerQueryGateTest.java @@ -91,7 +91,7 @@ private static JsonObject visible() { assertTrue(body.indexOf("ADVHAMMER BEFORE") < body.indexOf("HammerQueryGate.ready(")); assertTrue(body.indexOf("HammerQueryGate.ready(") < body.indexOf(".useOn(")); assertTrue(body.indexOf(".useOn(") < body.indexOf("ADVHAMMER AFTER")); - assertTrue(body.contains("action == InteractionResult.SUCCESS && source.getItem().isEmpty()")); + assertTrue(body.contains("action == InteractionResult.CONSUME && source.getItem().isEmpty()")); assertTrue(body.contains("player.getMainHandItem().getDamageValue() == 1, \"Live hammer failed \"")); assertTrue(body.contains("id(entity.getItem()).equals(id(expected))")); assertTrue(body.contains("require(produced == expected.getCount()")); diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117RedstoneReadinessTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117RedstoneReadinessTest.java index 16a325c7..84af062d 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117RedstoneReadinessTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117RedstoneReadinessTest.java @@ -31,8 +31,8 @@ public class Native117RedstoneReadinessTest { int end = source.indexOf("private void construct(Witness witness)", start); assertTrue(start >= 0 && end > start); String readiness = source.substring(start, end); - assertTrue(readiness.contains("world.isPositionTickingWithEntitiesLoaded(witness.pos)")); - assertTrue(readiness.contains("world.isPositionEntityTicking(new net.minecraft.world.level.ChunkPos(witness.pos))")); + assertTrue(readiness.contains("Native118ChunkTicking.withEntitiesLoaded(world, witness.pos)")); + assertTrue(readiness.contains("world.isPositionEntityTicking(witness.pos)")); assertTrue(readiness.contains("NativeBlockEntityTicks.contains(world, tile)")); assertTrue(readiness.contains("all &= nativeReady(ticking, listed, blockAndEntitiesLoaded, entityTicking);")); for (String field : new String[]{"blockTicking", "tickableListed", "blockAndEntitiesLoaded", "entityTicking"}) { diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotTest.java index e726586e..58133b4d 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotTest.java @@ -25,7 +25,7 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.level.Level; -import net.minecraftforge.fmlserverevents.FMLServerStoppingEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; @@ -140,7 +140,7 @@ public class Native117StoppedEntitiesSnapshotTest { } @Test public void entrypointRegistersNativeStoppingHandlerAndRealEnumeration() throws Exception { - assertNotNull(Native117StoppedEntitiesSnapshot.class.getDeclaredMethod("stopping", FMLServerStoppingEvent.class)); + assertNotNull(Native117StoppedEntitiesSnapshot.class.getDeclaredMethod("stopping", ServerStoppingEvent.class)); Path sources = Path.of("src/main/java/zone/moddev/mc/advantageworks"); assertTrue(Files.readString(sources.resolve("AdvantageWorksMod.java")).contains("Native117StoppedEntitiesSnapshot.register();")); String source = Files.readString(sources.resolve("Native117StoppedEntitiesSnapshot.java")); diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117DefinitionTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118DefinitionTest.java similarity index 90% rename from test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117DefinitionTest.java rename to test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118DefinitionTest.java index be2e3ffe..96ee7fc2 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117DefinitionTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118DefinitionTest.java @@ -6,16 +6,16 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; -public class Native117DefinitionTest { +public class Native118DefinitionTest { @Test public void nativeTargetPreservesEverySourceStationAndDistrict() throws Exception { Gson gson = new Gson(); try (InputStreamReader reader = new InputStreamReader(getClass().getResourceAsStream( "/assets/advantageworkstest/works/works-1.13.2.json"), StandardCharsets.UTF_8)) { WorksDefinition source = gson.fromJson(reader, WorksDefinition.class); - WorksDefinition target = WorksDefinitionLoader.loadNative117(); + WorksDefinition target = WorksDefinitionLoader.loadNative118(); assertEquals("1.13.2", source.minecraft); - assertEquals("1.17.1", target.minecraft); + assertEquals("1.18.2", target.minecraft); assertEquals(21, target.stations.size()); assertEquals(gson.toJsonTree(source.districts), gson.toJsonTree(target.districts)); assertEquals(gson.toJsonTree(source.stations), gson.toJsonTree(target.stations)); diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117EngineSignaturesTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118EngineSignaturesTest.java similarity index 77% rename from test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117EngineSignaturesTest.java rename to test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118EngineSignaturesTest.java index 96b14068..90c588eb 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117EngineSignaturesTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118EngineSignaturesTest.java @@ -15,12 +15,13 @@ import org.junit.Test; import static org.junit.Assert.*; -public class Native117EngineSignaturesTest { +public class Native118EngineSignaturesTest { @Test public void nativeChunkRegistryAndTileSignaturesAreExact() throws Exception { assertEquals(LevelChunk.class, ServerChunkCache.class.getMethod("getChunkNow", int.class, int.class).getReturnType()); assertEquals(ServerLevel.class, MinecraftServer.class.getMethod("getLevel", ResourceKey.class).getReturnType()); assertEquals(void.class, BlockEntity.class.getMethod("load", CompoundTag.class).getReturnType()); - assertEquals(CompoundTag.class, BlockEntity.class.getMethod("save", CompoundTag.class).getReturnType()); + assertEquals(CompoundTag.class, BlockEntity.class.getMethod("saveWithFullMetadata").getReturnType()); + assertTrue(Modifier.isProtected(BlockEntity.class.getDeclaredMethod("saveAdditional", CompoundTag.class).getModifiers())); } @Test public void nativePlayerSaveRemainsProtectedAndRespawnUsesRegistryKeys() throws Exception { @@ -32,17 +33,16 @@ public class Native117EngineSignaturesTest { } @Test public void realNativeBitStoragePadsFiveSevenAndThirteenBitValues() throws Exception { - Class storage = Class.forName("net.minecraft.util.BitStorage"); for (int bits : new int[] {5, 7, 13}) { - Object instance = storage.getConstructor(int.class, int.class).newInstance(bits, 4096); + var instance = new net.minecraft.util.SimpleBitStorage(bits, 4096); int perLong = 64 / bits; - storage.getMethod("set", int.class, int.class).invoke(instance, perLong - 1, 3); - storage.getMethod("set", int.class, int.class).invoke(instance, perLong, 17); - long[] raw = (long[]) storage.getMethod("getRaw").invoke(instance); + instance.set(perLong - 1, 3); + instance.set(perLong, 17); + long[] raw = instance.getRaw(); assertEquals((4096 + perLong - 1) / perLong, raw.length); assertEquals(3L << ((perLong - 1) * bits), raw[0]); assertEquals(17L, raw[1]); - assertEquals(17, storage.getMethod("get", int.class).invoke(instance, perLong)); + assertEquals(17, instance.get(perLong)); } } } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117PackMetadataTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118PackMetadataTest.java similarity index 65% rename from test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117PackMetadataTest.java rename to test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118PackMetadataTest.java index 9df3b685..e9ec3db8 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117PackMetadataTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118PackMetadataTest.java @@ -8,14 +8,16 @@ import org.junit.Test; import static org.junit.Assert.*; -public class Native117PackMetadataTest { - @Test public void harnessResourcesDeclareTheNative117PackFormat() throws Exception { +public class Native118PackMetadataTest { + @Test public void harnessResourcesDeclareTheNative118PackFormats() throws Exception { try (InputStream input = getClass().getResourceAsStream("/pack.mcmeta")) { assertNotNull(input); JsonObject pack = new JsonParser().parse(new InputStreamReader(input, StandardCharsets.UTF_8)) .getAsJsonObject().getAsJsonObject("pack"); assertTrue(pack.getAsJsonPrimitive("pack_format").isNumber()); - assertEquals(7, pack.get("pack_format").getAsInt()); + assertEquals(9, pack.get("pack_format").getAsInt()); + assertEquals(8, pack.get("forge:resource_pack_format").getAsInt()); + assertEquals(9, pack.get("forge:data_pack_format").getAsInt()); } } } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117TestFlatTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118TestFlatTest.java similarity index 64% rename from test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117TestFlatTest.java rename to test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118TestFlatTest.java index 1a0c66bc..1b6d8d6f 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117TestFlatTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118TestFlatTest.java @@ -6,8 +6,7 @@ import net.minecraft.world.level.block.Blocks; import net.minecraft.server.Bootstrap; import net.minecraft.core.RegistryAccess; -import net.minecraft.core.Registry; -import net.minecraft.resources.RegistryWriteOps; +import net.minecraft.resources.RegistryOps; import net.minecraft.world.level.levelgen.FlatLevelSource; import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; import net.minecraft.world.level.levelgen.WorldGenSettings; @@ -15,14 +14,13 @@ import org.junit.Test; import static org.junit.Assert.*; -public class Native117TestFlatTest { +public class Native118TestFlatTest { @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); } @Test public void constructorHasExactLayersAndNoStructures() { - RegistryAccess registries = RegistryAccess.builtin(); - FlatLevelSource generator = (FlatLevelSource) Native117TestFlat.createType().createChunkGenerator( - registries.registryOrThrow(Registry.BIOME_REGISTRY), - registries.registryOrThrow(Registry.NOISE_GENERATOR_SETTINGS_REGISTRY), 42L, ""); + RegistryAccess registries = RegistryAccess.builtinCopy().freeze(); + FlatLevelSource generator = (FlatLevelSource) Native118TestFlat.createType() + .createChunkGenerator(registries, 42L, ""); FlatLevelGeneratorSettings flat = generator.settings(); assertEquals(3, flat.getLayersInfo().size()); java.util.List layers = flat.getLayers(); @@ -31,14 +29,13 @@ public class Native117TestFlatTest { assertEquals(Blocks.DIRT.defaultBlockState(), layers.get(1)); assertEquals(Blocks.DIRT.defaultBlockState(), layers.get(2)); assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), layers.get(3)); - assertTrue(flat.structureSettings().structureConfig().isEmpty()); - assertNull(flat.structureSettings().stronghold()); + assertEquals(0, flat.structureOverrides().orElseThrow().size()); } @Test public void fullSaveCodecRetainsVanillaFlatIdentityAndAllDimensions() { - RegistryAccess registries = RegistryAccess.builtin(); - WorldGenSettings settings = Native117TestFlat.createType().createSettings(registries, 42L, false, false, ""); - JsonObject saved = WorldGenSettings.CODEC.encodeStart(RegistryWriteOps.create(JsonOps.INSTANCE, registries), settings) + RegistryAccess registries = RegistryAccess.builtinCopy().freeze(); + WorldGenSettings settings = Native118TestFlat.createType().createSettings(registries, 42L, false, false, ""); + JsonObject saved = WorldGenSettings.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, registries), settings) .result().orElseThrow(() -> new AssertionError("Native settings codec did not encode")).getAsJsonObject(); assertEquals(42L, saved.get("seed").getAsLong()); assertFalse(saved.get("generate_features").getAsBoolean()); @@ -52,19 +49,19 @@ public class Native117TestFlatTest { JsonObject flat = generator.getAsJsonObject("settings"); assertEquals("minecraft:plains", flat.get("biome").getAsString()); assertEquals(3, flat.getAsJsonArray("layers").size()); - assertEquals(0, flat.getAsJsonObject("structures").getAsJsonObject("structures").entrySet().size()); - assertFalse(flat.getAsJsonObject("structures").has("stronghold")); - assertFalse(saved.toString().contains(Native117TestFlat.ID)); + assertEquals(0, flat.getAsJsonArray("structure_overrides").size()); + assertFalse(saved.toString().contains(Native118TestFlat.ID)); } - @Test public void flatJsonCreationSettingsAreRefused() { - RegistryAccess registries = RegistryAccess.builtin(); + @Test public void dedicatedServerPresetIdIsAllowedButOverridesAreRefused() { + RegistryAccess registries = RegistryAccess.builtinCopy().freeze(); + assertTrue(Native118TestFlat.createType().createChunkGenerator( + registries, 42L, Native118TestFlat.ID) instanceof FlatLevelSource); try { - Native117TestFlat.createType().createChunkGenerator(registries.registryOrThrow(Registry.BIOME_REGISTRY), - registries.registryOrThrow(Registry.NOISE_GENERATOR_SETTINGS_REGISTRY), 42L, "{}"); + Native118TestFlat.createType().createChunkGenerator(registries, 42L, "{\"biome\":\"minecraft:desert\"}"); fail("Flat JSON must not enter the vanilla JsonOps path"); } catch (IllegalArgumentException expected) { - assertTrue(expected.getMessage().contains("must be empty")); + assertTrue(expected.getMessage().contains("Unexpected settings")); } } } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118WorldgenHeightTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118WorldgenHeightTest.java new file mode 100644 index 00000000..f8e04ca7 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118WorldgenHeightTest.java @@ -0,0 +1,32 @@ +package zone.moddev.mc.advantageworks; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import org.junit.Test; +import static org.junit.Assert.*; + +public class Native118WorldgenHeightTest { + @Test public void countsFromTheNativeNegativeBuildHeight() { + WorldgenSamplingJob.WorldAccess world = new WorldgenSamplingJob.WorldAccess() { + @Override public int minBuildHeight() { return -64; } + @Override public boolean matchesBiome(String expected, int x, int y, int z) { return false; } + @Override public void releaseChunk(int x, int z) { } + @Override public void close() { } + @Override public CompletableFuture requestChunk(int x, int z, boolean count) { + return CompletableFuture.completedFuture(new WorldgenSamplingJob.SampleChunk() { + @Override public boolean centerMatches(String expected) { return false; } + @Override public String blockName(int bx, int y, int bz) { + assertTrue(count); + assertTrue(y >= -64 && y <= 96); + return y == -64 && bx == x * 16 && bz == z * 16 ? "baseelectric:sulfur_ore" : null; + } + }); + } + }; + WorldgenSamplingJob job = new WorldgenSamplingJob(world, 0, 70, 0, 64, 1); + for (int tick = 0; tick < 500 && !job.finished(); tick++) job.tick(); + assertTrue(job.finished()); + assertNull(job.failure()); + assertEquals(9L, ((Map) job.result().get("counts")).get("baseelectric:sulfur_ore")); + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native117RunnerLogTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native117RunnerLogTest.java index 95427e19..44f79221 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native117RunnerLogTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native117RunnerLogTest.java @@ -164,6 +164,29 @@ private static Exception rejected(Action action) throws Exception { private static final String MIXIN_CLEANUP = "[31Oct2026 06:47:24.115] [main/DEBUG] [mixin/]: " + "Error cleaning class output directory: .mixin.out"; + private static final String NETTY_HEADER = "[09Nov2026 12:38:11.123] [main/DEBUG] " + + "[io.netty.util.internal.PlatformDependent0/]: direct buffer constructor: unavailable"; + private static final String NETTY_CAUSE = "java.lang.UnsupportedOperationException: Reflective setAccessible(true) disabled"; + + @Test public void exactNettyCapabilityProbeRemainsVisibleWithoutFailingTheBoot() throws Exception { + WorksRunner.BootLogs boot = boot("first-run"); + fill(boot); + String raw = CLEAN + NETTY_HEADER + "\n" + NETTY_CAUSE + "\n" + + "\tat io.netty.util.internal.PlatformDependent0.run(PlatformDependent0.java:11) ~[?:?]\n"; + write(log(boot, "debug"), raw); + boot.archive(); + assertTrue(boot.clean); + assertTrue(receipt(boot).contains("Netty DEBUG optional capability probe")); + assertEquals(raw, Files.readString(new File(boot.results, "first-run-debug.log").toPath())); + } + + @Test public void nettyDoesNotHideAnUnrelatedExceptionOrAnErrorLevelProbe() throws Exception { + for (String event : Arrays.asList(NETTY_CAUSE, NETTY_HEADER.replace("DEBUG", "ERROR") + "\n" + NETTY_CAUSE, + NETTY_HEADER + "\n" + NETTY_CAUSE + " other cause", + NETTY_HEADER + "\n" + NETTY_CAUSE + "\nCaused by: unrelated crash")) { + rejectEventInEveryLog(event); + } + } private void rejectEventInEveryLog(String event) throws Exception { for (String kind : Arrays.asList("console", "latest", "debug")) { diff --git a/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java b/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java index c9780d80..5c736fb3 100644 --- a/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java +++ b/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java @@ -28,7 +28,7 @@ public static void premain(String options, Instrumentation instrumentation) { "cpw.mods.modlauncher.serviceapi.ILaunchPluginService$Phase", true, loader); Object phase = phaseType.getField("BEFORE").get(null); Class pluginType = Class.forName( - "net.minecraftforge.common.asm.CapabilityTokenSubclass", true, loader); + "net.minecraftforge.fml.common.asm.CapabilityTokenSubclass", true, loader); pluginType.getMethod("processClassWithFlags", phaseType, nodeType, asmType, String.class) .invoke(pluginType.getConstructor().newInstance(), phase, node, asmName, "junit"); Class writerType = Class.forName("org.objectweb.asm.ClassWriter", true, loader);