diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 053a08da..815e0059 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,16 @@ -name: Power Advantage 1.16 CI +name: basePower 1.17 CI on: push: - branches: [master-1.16.5, 'feature/**'] + branches: [master-1.17.1, 'feature/**'] pull_request: - branches: [master-1.16.5] + branches: [master-1.17.1] permissions: contents: read concurrency: - group: poweradvantage-1.16-${{ github.workflow }}-${{ github.ref }} + group: poweradvantage-1.17-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/OreSpawn - ref: a113e5d767169189f0e35ade1e173a12d550b00e + ref: b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf path: .ci-dependencies/OreSpawn - name: Use canonical LF dependency inputs # Use the same source bytes on hosted and local dependency builds. @@ -34,14 +34,19 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/BaseMetals - ref: 995f6cc4dd2d5eecc628a42e5c43fa5390c24bca + ref: 0deeeeecdbbc1fbcdecf910c59477de8efe164d7 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 8 toolchain + - 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 @@ -55,7 +60,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_8_X64,$JAVA_HOME_25_X64 + echo "MMD_GRADLE_TOOLCHAINS=-Porg.gradle.java.installations.paths=$JAVA_HOME,$JAVA_HOME_16_X64,$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 @@ -70,8 +75,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.116051-deobf.jar" - bmjar="$basemetals/build/libs-dev/BaseMetals-3.1.1.116051-deobf.jar" + osjar="$orespawn/build/libs-dev/OreSpawn-4.1.0.117011-deobf.jar" + bmjar="$basemetals/build/libs-dev/BaseMetals-3.1.1.117011-deobf.jar" test -f "$osjar" && test -f "$bmjar" echo "orespawn=$osjar" >> "$GITHUB_OUTPUT" echo "basemetals=$bmjar" >> "$GITHUB_OUTPUT" @@ -85,20 +90,20 @@ jobs: - name: Upload audited release candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: PowerAdvantage-1.16.5-${{ github.sha }} + name: PowerAdvantage-1.17.1-${{ github.sha }} if-no-files-found: error retention-days: 30 path: | - build/libs/PowerAdvantage-2.4.7.116051.jar - build/libs/PowerAdvantage-2.4.7.116051-sources.jar - build/libs/PowerAdvantage-2.4.7.116051-javadoc.jar + 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/release/SHA256SUMS CHANGELOG.txt - name: Upload diagnostics on failure if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: PowerAdvantage-1.16.5-diagnostics-${{ github.sha }} + name: PowerAdvantage-1.17.1-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 8baf3fa0..b8afd74d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,9 +2,9 @@ name: CodeQL on: push: - branches: [master-1.16.5, 'feature/**'] + branches: [master-1.17.1, 'feature/**'] pull_request: - branches: [master-1.16.5] + branches: [master-1.17.1] schedule: - cron: '47 7 * * 4' @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/OreSpawn - ref: a113e5d767169189f0e35ade1e173a12d550b00e + ref: b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf path: .ci-dependencies/OreSpawn - name: Use canonical LF dependency inputs # Use the same source bytes on hosted and local dependency builds. @@ -34,14 +34,19 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/BaseMetals - ref: 995f6cc4dd2d5eecc628a42e5c43fa5390c24bca + ref: 0deeeeecdbbc1fbcdecf910c59477de8efe164d7 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 8 toolchain + - 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 @@ -55,7 +60,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_8_X64,$JAVA_HOME_25_X64 + echo "MMD_GRADLE_TOOLCHAINS=-Porg.gradle.java.installations.paths=$JAVA_HOME,$JAVA_HOME_16_X64,$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 @@ -70,8 +75,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.116051-deobf.jar" >> "$GITHUB_OUTPUT" - echo "basemetals=$basemetals/build/libs-dev/BaseMetals-3.1.1.116051-deobf.jar" >> "$GITHUB_OUTPUT" + 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" - name: Initialize CodeQL uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 with: diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index af9cd1ab..31febbbd 100644 --- a/.github/workflows/release-on-tag.yml +++ b/.github/workflows/release-on-tag.yml @@ -1,4 +1,4 @@ -name: Validate Power Advantage release tag +name: Validate basePower release tag on: push: @@ -16,7 +16,7 @@ concurrency: jobs: validate-release-tag: name: Validate tag for manual release confirmation - if: github.repository == 'MinecraftModDevelopmentMods/PowerAdvantage' + if: github.repository == 'MinecraftModDevelopmentMods/basePower' runs-on: ubuntu-latest timeout-minutes: 10 diff --git a/.github/workflows/validate-gradle-build.yml b/.github/workflows/validate-gradle-build.yml index fc66575a..0bf04b84 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.16.5 + - master-1.17.1 - 'feature/**' pull_request: branches: - - master-1.16.5 + - master-1.17.1 permissions: contents: read diff --git a/CHANGELOG.txt b/CHANGELOG.txt index cdbac601..463bdc95 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,21 @@ +basePower 2.4.9.117011 (Minecraft 1.17.1) + +- Bring basePower to Minecraft 1.17.1 with one shared tab for the three mods. +- Keep existing base* machines and map legacy Advantage blocks, items, fluids and saved state. +- Carry old Advantage mod and data pack names forward when opening an existing world with the replacement mods installed. +- Fix Eclipse refreshes when ForgeGradle reuses an existing launch. + +Power Advantage 2.4.8.117011 (Minecraft 1.17.1) + +- Move to Forge 37.1.1 and Java 16, keeping the existing block, item, fluid and saved-state names. +- Use native block-entity factories and tickers for machines and conveyors, retaining their saved position and facing. +- Supply configured rubber and solder aliases through an additive server data pack instead of replacing vanilla tag readers. +- Preserve legacy entity state in both terrain chunks and the separate 1.17 entity storage. +- Use the 1.17 ingredient-predicate format so unrelated items do not unlock recipes. +- Save machine timers, progress and externally supplied energy after an autosave, without sending extra comparator or client updates. +- Remap old grass-path blocks/items and the berry-picking sound when loading older Forge registry snapshots. +- Carry forward the finite-input, cold-restart, migration and rendered GUI checks; automated and manual acceptance remain separate. + Power Advantage 2.4.7.116051 (Minecraft 1.16.5) - Charge energy sources only for what consumers accept, retaining rejected energy for the next machine. diff --git a/README.md b/README.md index 6d1db450..6f030dd5 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,13 @@ -# PowerAdvantage +# basePower -**Minecraft 1.16.5, version 2.4.7.116051. Source port candidate.** +**Minecraft 1.17.1, version 2.4.9.117011. Local test candidate.** -See the [1.16 port record](docs/1.16/PORT-STATUS.md) for current checks and remaining -work. The accepted [1.15 test record](docs/1.15/CURRENT-QUALIFICATION.md) is retained -as historical evidence, not a pass for this version. Manual 1.16 acceptance is -still required. +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. -The [test-suite review](docs/1.16/TEST-SUITE-AUDIT.md) separates operating checks -from saved-state and presence checks. It records the remaining machine coverage -gaps; a green build does not qualify every machine setup. - -The follow-up [machine operating checks](docs/1.16/OPERATING-TESTS.md) use finite -inputs, real server ticks, blocked and starved controls, and full-process -restarts. They check useful output and retained resources, not just animation -or a rising progress counter. - -Power Advantage provides shared power, fluids, item transport and compatibility APIs for Steam Advantage and Electric Advantage. +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 @@ -29,21 +20,20 @@ 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.16.5 +## Building Minecraft 1.17.1 -The `master-1.16.5` build uses Forge 36.2.34, official `1.16.5` mappings, -MCP `20210115.111550`, ForgeGradle 7.0.34 and Gradle 9.6.1. -Run Gradle with Java 17 and compile production code with Java 8. Mavenizer also -needs Java 25. CI pins all three JDKs. Pass the installed homes using -`-Porg.gradle.java.installations.paths=,,` and disable +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 +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 automatic discovery and downloads for a reproducibility check. -Build the pinned Base Metals `3.1.1.116051` and OreSpawn `4.1.0.116051` deobf jars first. -The exact Base Metals source is -`995f6cc4dd2d5eecc628a42e5c43fa5390c24bca`. CI builds that dependency first; -the checksums and complete source pins are in `gradle.properties`. +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`. -Then run from `D:\PowerAdvantage\PA116\PowerAdvantage`: +Then run from the repository root: ```text gradlew.bat clean check build javadoc verifyReleaseDependencies verifyReleaseArtifacts writeReleaseChecksums verifyEclipseProductionClasspath @@ -52,8 +42,8 @@ gradlew.bat clean check build javadoc verifyReleaseDependencies verifyReleaseArt The default dependency jars are: ```text -D:\BaseMetals\BaseMetals 116\BaseMetals\build\libs-dev\BaseMetals-3.1.1.116051-deobf.jar -D:\MinecraftMineralogy\OS116\OreSpawn\build\libs-dev\OreSpawn-4.1.0.116051-deobf.jar +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 ``` Other layouts can pass `-PbaseMetalsDeobfJar=` and `-PoreSpawnDeobfJar=`. @@ -75,40 +65,35 @@ 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 Steam/Electric `2.2.8.116051` deobf jars, +To prepare a combined client, build the baseSteam and baseElectric `2.2.10.117011` deobf jars, run `gradlew.bat prepareCombinedEclipseLaunch`, refresh Eclipse, then launch `runClientAllAdvantage.launch`. Inputs must match -`test-rig/profiles/native116/dev-core.json`; alternative layouts can pass +`test-rig/profiles/native117/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. -The local GUI corrections have separate input hashes, keeping the earlier machine -test profiles unchanged. To prepare that combined preview, run -`gradlew.bat prepareCombinedEclipseLaunch -PadvantageDevelopmentProfile=docs/1.16/gui-development-inputs.json`. -See the [GUI checks](docs/1.16/GUI-CHECKS.md) for coverage and the repeatable command. - `check` and Eclipse preparation also run `verifyDevelopmentRuntimeArchives`, 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.16 candidate. Manual -visual and interaction acceptance remains separate. - -The migration test fixtures do not import restricted JDK internals. A plain ECJ -batch compile does not apply Eclipse's JRE access rules automatically. After -`eclipseClasspath` and `test`, run `tools/verification/Verify-EclipseCompilation.ps1` -with `-Compiler `, `-Java17Home `, `-Java8Home ` and -`-Evidence ` to compile fresh main/test classes with restricted -references treated as errors, then run the tests on Java 8. Keep Eclipse's -access-restriction checks enabled. +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. ## Compatibility -The implementation namespace remains `zone.moddev.mc.poweradvantage`, with the -historical `cyano.poweradvantage.api` package retained. The port keeps registry, -fluid, tile, NBT and provider identities. The current test record distinguishes -machine preservation, actual operation and vanilla migration differences. +The implementation namespace is `zone.moddev.mc.basepower`. The historical +`cyano.poweradvantage.api` package remains available. The port keeps basePower's +registry names and saved machine data and maps the older Power Advantage names. +The test record distinguishes state preservation, real operation and changes +made by Minecraft itself. The upgrade path prepares stored items before vanilla flattening and converts entities individually, so a mod entity cannot prevent a nearby pet's UUID and @@ -118,13 +103,13 @@ silently discarded. Legacy Forge swimming speed, name-tag distance, gravity and player reach use their native registry names without discarding saved values or modifiers. -The 1.16 port uses native dimension registry keys, tile save/load methods and -matrix-based menus. Config-dependent material aliases are resolved during the -server's native tag load and synchronized to clients. The shared pre-flattening -migration hook remains necessary; its presence is not proof that an upgrade is safe. +The port uses native dimension registry keys, block-entity factories and tickers, +capabilities and menus. Config-dependent material aliases are additive server +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) and -[Advantage Works rig](test-rig/README.md) describe the earlier protocols. -The [1.16 status](docs/1.16/PORT-STATUS.md) tracks this port's qualification. +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 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 8447ced1..7633a2d9 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ plugins { group = project.mod_group version = project.mod_version -base.archivesName = 'PowerAdvantage' +base.archivesName = 'basePower' def versionParts = project.mod_version.toString().tokenize('.') if (versionParts.size() != 4 || !versionParts.every { it ==~ /\d+/ }) { @@ -45,7 +45,7 @@ ext.release_tag = project.mod_version java { toolchain { - languageVersion = JavaLanguageVersion.of(8) + languageVersion = JavaLanguageVersion.of(16) vendor = JvmVendorSpec.ADOPTIUM } withSourcesJar() @@ -54,11 +54,11 @@ java { tasks.withType(JavaCompile).configureEach { javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(8) + languageVersion = JavaLanguageVersion.of(16) vendor = JvmVendorSpec.ADOPTIUM } - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_16 + targetCompatibility = JavaVersion.VERSION_16 options.encoding = 'UTF-8' options.compilerArgs.addAll(['-Xmaxerrs', '1000']) } @@ -96,15 +96,15 @@ minecraft { def forgeRunModClassesDirectory = layout.buildDirectory.dir('forge-run-mod-classes/main') def forgeMainOutputPath = forgeRunModClassesDirectory.get().asFile.absolutePath def mainModClasses = "${mod_id}%%${forgeMainOutputPath}${File.pathSeparator}${mod_id}%%${forgeMainOutputPath}" - def forge36RunTargets = [client: 'fmluserdevclient', server: 'fmluserdevserver', data: 'fmluserdevdata'] + def forge37RunTargets = [client: 'fmluserdevclient', server: 'fmluserdevserver', data: 'fmluserdevdata'] runs { configureEach { - mainClass = 'net.minecraftforge.userdev.LaunchTesting' + jvmArgs '--add-opens', 'java.base/java.lang.invoke=ALL-UNNAMED' workingDir.convention layout.projectDirectory.dir(configuredRunDirectory) systemProperty 'forge.logging.markers', 'REGISTRIES' systemProperty 'forge.logging.console.level', 'info' - mods { poweradvantage { source sourceSets.main } } - environment 'target', forge36RunTargets[name] + mods { basepower { source sourceSets.main } } + environment 'target', forge37RunTargets[name] environment 'MCP_MAPPINGS', "${mapping_channel}_${mapping_version}" environment 'MCP_VERSION', mcp_version environment 'FORGE_VERSION', forge_version @@ -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.16 launches.' + description = 'Merges classes and processed resources for Forge 1.17 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 116/BaseMetals/build/libs-dev/' - + 'BaseMetals-3.1.1.116051-deobf.jar')) + .getOrElse('../../../BaseMetals/BaseMetals 117/BaseMetals/build/libs-dev/' + + "BaseMetals-${project.basemetals_version}-deobf.jar")) def oreSpawnDeobfJar = file(providers.gradleProperty('oreSpawnDeobfJar') - .getOrElse('../../../MinecraftMineralogy/OS116/OreSpawn/build/libs-dev/' - + 'OreSpawn-4.1.0.116051-deobf.jar')) + .getOrElse('../../../MinecraftMineralogy/OS117/OreSpawn/build/libs-dev/' + + 'OreSpawn-4.1.0.117011-deobf.jar')) dependencies { implementation minecraft.dependency( @@ -182,6 +182,23 @@ dependencies { testImplementation 'junit:junit:4.13.2' } +def compileUnitBootstrap = tasks.register('compileUnitBootstrap', JavaCompile) { + source fileTree('test-rig/unit-bootstrap') + classpath = files() + destinationDirectory = layout.buildDirectory.dir('unit-bootstrap/classes') +} +def unitBootstrapJar = tasks.register('unitBootstrapJar', Jar) { + dependsOn compileUnitBootstrap + archiveFileName = 'forge-native-unit-bootstrap.jar' + destinationDirectory = layout.buildDirectory.dir('unit-bootstrap') + from compileUnitBootstrap.flatMap { it.destinationDirectory } + manifest.attributes('Premain-Class': 'zone.moddev.mc.advantageworks.unit.ForgeCapabilityBootstrap') +} +tasks.withType(Test).configureEach { + dependsOn unitBootstrapJar + doFirst { jvmArgs "-javaagent:${unitBootstrapJar.get().archiveFile.get().asFile}" } +} + tasks.named('processResources', ProcessResources) { filteringCharset = 'UTF-8' inputs.properties([ @@ -316,46 +333,46 @@ 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 != 6) { - throw new GradleException('Minecraft 1.16.5 resources must use pack format 6') + 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') } - if (project.mod_version != '2.4.7.116051') { + if (project.mod_version != '2.4.9.117011') { throw new GradleException("Unexpected PowerAdvantage release version: ${project.mod_version}") } - if (project.minecraft_version != '1.16.5' - || project.forge_version != '36.2.34' + if (project.minecraft_version != '1.17.1' + || project.forge_version != '37.1.1' || project.mapping_channel != 'official' - || project.mapping_version != '1.16.5' - || project.mcp_version != '20210115.111550') { + || project.mapping_version != '1.17.1' + || project.mcp_version != '20210706.113038') { throw new GradleException('Unexpected Minecraft, Forge, or mappings target') } if (project.loader_name != 'forge' || project.loader_code != '1' - || project.java_version != '8' - || project.java_toolchain_version != '8.0.502+7' + || project.java_version != '16' + || project.java_toolchain_version != '16.0.2+7' || project.gradle_java_version != '17') { throw new GradleException('Unexpected release dispatcher target metadata') } - if (project.mod_id != 'poweradvantage' - || project.group.toString() != 'zone.moddev.mc.poweradvantage' - || base.archivesName.get() != 'PowerAdvantage' - || project.curseforge_project_id != '240982') { + if (project.mod_id != 'basepower' + || project.group.toString() != 'zone.moddev.mc.basepower' + || base.archivesName.get() != 'basePower' + || project.curseforge_project_id != '1780140') { throw new GradleException('Unexpected Forge, Maven, archive, or CurseForge identity') } if (project.cf_requirements != 'base-metals,mmd-orespawn' || project.cf_optionals != 'additional-loot-tables' - || project.cf_versions != '1.16.5') { + || project.cf_versions != '1.17.1') { throw new GradleException('Unexpected CurseForge dependency or game-version metadata') } - if (project.basemetals_version != '3.1.1.116051' - || project.orespawn_version != '4.1.0.116051' - || project.basemetals_source_commit != '995f6cc4dd2d5eecc628a42e5c43fa5390c24bca' - || project.orespawn_source_commit != 'a113e5d767169189f0e35ade1e173a12d550b00e') { + 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') { throw new GradleException('Unexpected Base Metals or OreSpawn dependency identity') } String mainSource = file( - 'src/main/java/zone/moddev/mc/poweradvantage/PowerAdvantage.java').getText('UTF-8') - if (!mainSource.contains('MODID = "poweradvantage"') - || !mainSource.contains('VERSION = "2.4.7.116051"') + '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('@Mod(PowerAdvantage.MODID)')) { throw new GradleException('PowerAdvantage source identity changed') } @@ -363,7 +380,7 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Legacy cyano.poweradvantage.api compatibility surface is missing') } String entitySource = file( - 'src/main/java/zone/moddev/mc/poweradvantage/init/Entities.java').getText('UTF-8') + 'src/main/java/zone/moddev/mc/basepower/init/Entities.java').getText('UTF-8') [ 'tileentity.pipe_terminal', 'tileentity.fluid_drain', 'tileentity.fluid_discharge', 'tileentity.fluid_storage_tank', @@ -377,7 +394,7 @@ tasks.register('verifyReleaseConfiguration') { } } String modsToml = file('src/main/resources/META-INF/mods.toml').getText('UTF-8').replace('\r\n', '\n') - ['modId="poweradvantage"', 'modId="basemetals"', 'modId="orespawn"', + ['modId="basepower"', 'modId="basemetals"', 'modId="orespawn"', 'mandatory=true'].each { required -> if (!modsToml.contains(required)) { throw new GradleException("Source mods.toml is missing ${required}") @@ -407,9 +424,9 @@ tasks.register('verifyReleaseConfiguration') { } } Map provider = new JsonSlurper().parse(file( - 'src/main/resources/data/poweradvantage/orespawn/provider.json')) as Map + 'src/main/resources/data/basepower/orespawn/provider.json')) as Map Map desertOil = provider.fluid_deposits[ - 'poweradvantage:fluid_deposit/desert_crude_oil'].dimensions['minecraft:overworld'] as Map + 'basepower:fluid_deposit/desert_crude_oil'].dimensions['minecraft:overworld'] as Map if (desertOil.biome_dictionary != ['SANDY'] || (desertOil.excluded_biome_dictionary as Set) != (['BEACH', 'MESA'] as Set)) { throw new GradleException( @@ -544,12 +561,12 @@ tasks.register('verifyReleaseArtifacts') { List names = zip.entries().collect { it.name } ['META-INF/mods.toml', 'META-INF/accesstransformer.cfg', 'pack.mcmeta', 'coremods/poweradvantage_116_migration.js', - 'zone/moddev/mc/poweradvantage/PowerAdvantage.class', + 'zone/moddev/mc/basepower/PowerAdvantage.class', 'cyano/poweradvantage/api/PoweredEntity.class', 'cyano/poweradvantage/api/fluid/FluidNetworkApi.class', - 'assets/poweradvantage/lang/en_us.json', - 'data/poweradvantage/recipes/fluid_drain.json', - 'data/poweradvantage/advancements/recipes/sprocket_rod_normal.json'].each { required -> + 'assets/basepower/lang/en_us.json', + 'data/basepower/recipes/fluid_drain.json', + 'data/basepower/advancements/recipes/sprocket_rod_normal.json'].each { required -> if (!names.contains(required)) { throw new GradleException("Release jar is missing ${required}") } @@ -566,14 +583,12 @@ tasks.register('verifyReleaseArtifacts') { List accessRules = zip.getInputStream(zip.getEntry('META-INF/accesstransformer.cfg')) .getText('UTF-8').readLines().collect { it.trim() } .findAll { it && !it.startsWith('#') } - if (accessRules != [ - 'public net.minecraft.resources.DataPackRegistries field_240955_e_', - 'public net.minecraft.tags.NetworkTagManager field_199720_b']) { - throw new GradleException('Packaged native tag-reader access rules were not reobfuscated correctly') + if (accessRules != ['public-f net.minecraft.util.datafix.fixes.BlockStateData f_14934_']) { + throw new GradleException('Packaged pre-flattening table access rule is incorrect') } String modsToml = zip.getInputStream(zip.getEntry('META-INF/mods.toml')) .getText(StandardCharsets.UTF_8.name()) - if (!modsToml.contains('modId="poweradvantage"') + if (!modsToml.contains('modId="basepower"') || !modsToml.contains("version=\"${project.mod_version}\"") || !modsToml.contains('modId="basemetals"') || !modsToml.contains('modId="orespawn"')) { @@ -594,14 +609,14 @@ tasks.register('verifyReleaseArtifacts') { } } int major = ((header[6] & 0xff) << 8) | (header[7] & 0xff) - if (major != 52) { - throw new GradleException("${entry.name} uses Java class major ${major}, expected 52") + if (major != 60) { + throw new GradleException("${entry.name} uses Java class major ${major}, expected 60") } } } new ZipFile(new File(libs, expectedReleaseFiles.get()[1])).withCloseable { sourceZip -> - if (sourceZip.getEntry('zone/moddev/mc/poweradvantage/PowerAdvantage.java') == null + if (sourceZip.getEntry('zone/moddev/mc/basepower/PowerAdvantage.java') == null || sourceZip.getEntry('cyano/poweradvantage/api/PoweredEntity.java') == null) { throw new GradleException('Sources jar is missing implementation or legacy API sources') } @@ -699,9 +714,9 @@ publishing { artifact(tasks.named('javadocJar')) } pom { - name = 'Power Advantage' + name = 'basePower' description = project.mod_description - url = 'https://github.com/MinecraftModDevelopmentMods/PowerAdvantage' + url = 'https://github.com/MinecraftModDevelopmentMods/basePower' licenses { license { name = 'GNU Lesser General Public License, Version 3' @@ -709,9 +724,9 @@ publishing { } } scm { - connection = 'scm:git:https://github.com/MinecraftModDevelopmentMods/PowerAdvantage.git' - developerConnection = 'scm:git:ssh://git@github.com/MinecraftModDevelopmentMods/PowerAdvantage.git' - url = 'https://github.com/MinecraftModDevelopmentMods/PowerAdvantage' + connection = 'scm:git:https://github.com/MinecraftModDevelopmentMods/basePower.git' + developerConnection = 'scm:git:ssh://git@github.com/MinecraftModDevelopmentMods/basePower.git' + url = 'https://github.com/MinecraftModDevelopmentMods/basePower' } } } @@ -839,10 +854,10 @@ tasks.register('isolateEclipseProductionRuns') { "key=\"MC_VERSION\" value=\"${project.minecraft_version}\"") if (launch.name in ['runClient.launch', 'runServer.launch', 'runData.launch']) { // Eclipse compiles into bin/main, not Gradle's merged run output. - def launchXml = new groovy.xml.XmlSlurper().parseText(contents) + def launchXml = new groovy.xml.XmlParser().parseText(contents) def modClasses = launchXml.mapAttribute.find { - it.@key.text() == 'org.eclipse.debug.core.environmentVariables' - }.mapEntry.findAll { it.@key.text() == 'MOD_CLASSES' } + it.@key == 'org.eclipse.debug.core.environmentVariables' + }.mapEntry.findAll { it.@key == 'MOD_CLASSES' } if (modClasses.size() != 1) { throw new GradleException("${launch.name} must define one MOD_CLASSES entry") } @@ -879,26 +894,32 @@ tasks.register('isolateEclipseProductionRuns') { }.findAll { File runtimeFile -> !mainOutputs.contains(runtimeFile) } - List launchLines = contents.readLines() - int buildshipContainerLine = launchLines.findIndexOf { - it.contains('org.eclipse.buildship.core.gradleclasspathcontainer') + def parser = new groovy.xml.XmlParser() + def launchXml = parser.parseText(contents) + def classpath = launchXml.listAttribute.find { it.@key == 'org.eclipse.jdt.launching.CLASSPATH' } + if (classpath == null) { + throw new GradleException("${name} is missing its explicit classpath") + } + // A refresh can reuse an already isolated launch. Replace its old runtime + // archives too, but keep ForgeGradle's separate Slime Launcher entry. + def previousRuntime = classpath.listEntry.findAll { entry -> + def runtime = parser.parseText(entry.@value) + String archive = runtime.@externalArchive + runtime.@containerPath == 'org.eclipse.buildship.core.gradleclasspathcontainer' + || (archive && !file(archive).name.startsWith('slime-launcher-')) } - if (buildshipContainerLine < 0) { - throw new GradleException("${name} is missing the Buildship classpath container") + if (previousRuntime.isEmpty()) { + throw new GradleException("${name} is missing its production runtime entries") } - List runtimeEntries = productionRuntime.collect { File runtimeFile -> - String escapedPath = runtimeFile.canonicalPath - .replace('&', '&amp;') - .replace('<', '&lt;') - .replace('>', '&gt;') - (' ') + int runtimeIndex = classpath.children().indexOf(previousRuntime.first()) + previousRuntime.each { classpath.remove(it) } + def runtimeEntries = productionRuntime.collect { File runtimeFile -> + def archive = new Node(null, 'runtimeClasspathEntry', + [externalArchive: runtimeFile.path, type: '2', path: '5']) + new Node(null, 'listEntry', [value: groovy.xml.XmlUtil.serialize(archive)]) } - launchLines.remove(buildshipContainerLine) - launchLines.addAll(buildshipContainerLine, runtimeEntries) - contents = launchLines.join('\n') + '\n' + classpath.children().addAll(runtimeIndex, runtimeEntries) + contents = groovy.xml.XmlUtil.serialize(launchXml) ['--cache', '--metadata', '--to-srg', '--to-obf'].each { option -> String prefix = option + ' ' @@ -1135,9 +1156,9 @@ tasks.register('verifyRecipeTagGuards') { throw new GradleException("${recipeFile.path} contains a non-object recipe condition") } Set guards = conditions - .findAll { it instanceof Map && it.type == 'poweradvantage:tag_not_empty' } + .findAll { it instanceof Map && it.type == 'basepower:tag_not_empty' } .collect { it.tag as String } as Set - Set emptyGuards = conditions.findAll { it.type == 'poweradvantage:tag_empty' } + Set emptyGuards = conditions.findAll { it.type == 'basepower:tag_empty' } .collect { it.tag as String } as Set if (!(emptyGuards.intersect(guards)).isEmpty()) { throw new GradleException("${recipeFile.path} requires the same tag to be empty and nonempty") @@ -1179,10 +1200,10 @@ tasks.register('clientResourceProbeJar', Jar) { // Optional coordinated play-test launch. The ordinary launch stays Power-only; // these deobf jars are runtime inputs, never release contents or compile dependencies. def advantageExtensionJars = [ - steamadvantage: file(providers.gradleProperty('steamAdvantageDeobfJar').getOrElse( - '../SteamAdvantage/SteamAdvantage/build/libs-dev/SteamAdvantage-2.2.8.116051-deobf.jar')), - electricadvantage: file(providers.gradleProperty('electricAdvantageDeobfJar').getOrElse( - '../ElectricAdvantage/ElectricAdvantage/build/libs-dev/ElectricAdvantage-2.2.8.116051-deobf.jar')) + basesteam: file(providers.gradleProperty('steamAdvantageDeobfJar').getOrElse( + '../SteamAdvantage/SteamAdvantage/build/libs-dev/baseSteam-2.2.10.117011-deobf.jar')), + baseelectric: file(providers.gradleProperty('electricAdvantageDeobfJar').getOrElse( + '../ElectricAdvantage/ElectricAdvantage/build/libs-dev/baseElectric-2.2.10.117011-deobf.jar')) ] tasks.register('prepareCombinedEclipseLaunch') { @@ -1192,7 +1213,7 @@ tasks.register('prepareCombinedEclipseLaunch') { dependsOn tasks.named('deobfJar') doLast { File inputProfile = file(providers.gradleProperty('advantageDevelopmentProfile').getOrElse( - 'test-rig/profiles/native116/dev-core.json')) + 'test-rig/profiles/native117/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.17/BASE-CANDIDATE.md b/docs/1.17/BASE-CANDIDATE.md new file mode 100644 index 00000000..a5628d6c --- /dev/null +++ b/docs/1.17/BASE-CANDIDATE.md @@ -0,0 +1,140 @@ +# Minecraft 1.17.1 Base Mods + +These are local builds, not releases. The gameplay tests below use the exact +player jars listed here. The combined Eclipse client tests use the corresponding +development build. Back up the entire world and only open copies when testing +an upgrade. + +| Mod | Version | Player jar SHA-256 | +| --- | --- | --- | +| basePower | 2.4.9.117011 | `A788C08163CECE1C6D33FBDC295BE6EC272DE9AF7911BE74E707D0EE9121DF73` | +| baseSteam | 2.2.10.117011 | `8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B` | +| baseElectric | 2.2.10.117011 | `9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F` | +| Base Metals (local) | 3.1.1.117011 | `F3053F23D2415476FC52426A24E3B0B828198A678FBD5DD789923DB2063E0FEB` | + +The local Base Metals patch moves common setup work to Forge's queued setup +thread. It fixes a startup deadlock seen when both projects initialized the +Forge handshake together. The packaged checks covered ten client/server login +and reload scenarios, including settings mismatches. OreSpawn is the public +4.1.0.117011 release. + +## November 8 Baseline + +These results used the previous Power jar, SHA-256 +`C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F`. + +- The three reproducible release builds, including clean caches and checkout + paths containing spaces. Dependency, archive and Eclipse classpath audits + passed. The combined client launch is `runClientAllAdvantage.launch`. +- Independent Eclipse compiler checks. basePower passed all 201 tests against + freshly compiled Java 16 output with access restrictions still enabled. +- All 21 walkable works stations with and without Base Metals, before and after + a full restart, including an independent audit of the assertions. +- 132 finite machine jobs and their cold-restart checks. A second campaign + imported the 132 existing setups from 1.16.5 without reconstructing them. +- Original Advantage 1.10.2 and released base* 1.10.2 and 1.12.2 saves kept + their machine state and turbine UUIDs, and resumed conveying, crushing and + smelting. Chained base* fixtures from 1.13.2 through 1.16.5 kept their + machine state across both boots. The direct 1.15.2 check compared 14 blocks, + 12 tile entities and 21 item stacks after conversion and restart. + The original 1.10.2 and legacy 1.12.2 player books kept their enchantments + and player UUIDs in the saved files after both boots. +- Both live Base Metals/basePower transformer orders on the original 1.10.2 + fixture. Bytecode inspection confirmed both hooks ran in the requested order; + both resulting saves agreed after restart. +- Both 30-minute load runs (fresh and upgraded), the full server restart, and + processing after restart. The independent audit checked raw checkpoints, + inventory acknowledgements and unchanged inputs. +- Recipe unlocks and natural progression in all three recipe modes. The + combined client checked recipe-book grouping, creative entries, models, + GUI layout and required labels, including intentionally broken controls. +- Mining and drops, Forge capabilities, native tank reload, both energy + converter directions, and all six crude/refined oil lanes including Mineralogy. +- Partial networks with the supply, machine or bridge genuinely unloaded. + Extended drill runs covered four moves per drill, blocked output across a + restart, then resumed work and redstone controls in fresh and upgraded worlds. + The separate saved-progress run checked 18 drill routes per world, including + 72 observed moves and the first natural tick after each saved-state read. +- Worldgen and restart checks for the standalone, Mineralogy and legacy OreSpawn + profiles, including preservation of user changes to generation rules. + +The November 8 rerun passed all 2,186 offline checks and 826 Java tests across +the three release builds and test harness. The three rebuilt player jars and the +harness matched their pinned SHA-256 values. The combined client passed 351 +caption checks and rejected all 355 deliberately broken caption controls. The +two direct respawn routes passed 24 server-side cases; no real client death +screen was part of that check. + +## November 9 Metadata Fix + +When a save still lists an old Advantage mod, the world reader now translates +its Forge mod-list entry and data pack selection to the installed replacement. +It does not rewrite registry snapshots, hide other missing mods, or override a +pack choice already saved under the new name. The remap runs in memory; the +next normal save records the current IDs. The artifact diff is one new metadata +class and the world-reader transformer. + +The original 1.13-chained regression fixture now boots and restarts without +the old missing-mod or missing-pack messages. Its saved pack list contains +the three current IDs and retains the unrelated entries. The original 1.10.2 +fixture also passes with both Base Metals/basePower transformer orders. + +The new exact target manifest is `test-rig/build/base117-20261109-a-inputs/inputs.json`, +SHA-256 `F1C023D8F4784D60C2416818BE43A2A8349A422E7E90AFF5D35CA68D1DB3DD05`. +Two clean builds reproduced the Power package and development jar hashes. +Steam, Electric and the harness rebuilt to their unchanged hashes. All 2,187 +offline checks and 830 Java tests passed. + +- Both 30-minute endurance worlds passed 30 load batches, a full restart and + resumed processing. The fresh world measured 37,489 ticks and the upgraded + world 36,048. The independent replay checked 33 checkpoints per world. +- The original 1.10.2 and released base* 1.10.2 and 1.12.2 worlds converted, + restarted and resumed machine operation. Two newly serialized Overworld + fixtures passed strict player and entity checks. The 16 Nether and End + routes passed the independent platform assessment with only the vanilla wolf + difference below; all six 1.13-derived saves passed raw metadata checks. + All 36 respawn cases passed, including the previously blocked chained route. +- Both 21-station Works profiles, the 132 fresh machine cases, and the 132 + imported machine states passed their restart checks. The drill and redstone + reloads, harvest, Forge interoperability, six oil lanes and all three + world-generation profiles passed. +- Recipe unlocks and all 18 progression cases passed, with 36 independently + replayed hammer invocations. The combined client passed 351 GUI caption + checks and rejected 355 deliberately broken controls. + +To regenerate the combined Eclipse launch for this local rebuild without +changing the earlier pinned profiles: + +```powershell +.\gradlew.bat prepareCombinedEclipseLaunch -PadvantageDevelopmentProfile=test-rig/build/base117-20261109-a-inputs/dev-core.json +``` + +## Still Open + +The earlier full Python run had 120 failures and 44 errors. The offline checks +now read the original Git blobs for historical oracles and check the active +base* adapters separately. The archived checksums and failure reports are +unchanged. The checks require the pinned Git history and the three sibling 1.13 +repositories for the historical recipe inventory. + +The older 30-route upgrade plan and the eight original Overworld actor routes +still lack their exact source worlds. A search of 1,374 accessible saves did +not find them. All 16 Nether and End source fixtures matched their original +file inventories. Newly generated Overworld fixtures are separate coverage, +not a replacement for the missing originals. + +Forge/Minecraft 1.17.1 restores a legacy tamed wolf from 8 to 20 health even +without Advantage installed. The strict comparisons retain that difference; +the separate platform assessment checks every other field without an exception. +The old failing campaign and its logs are kept. The manual walkthrough is +separate, including live turbine orientation, sounds and player controls. + +Raw reports and the frozen manifest are in the ignored `test-rig/build/` +directories for the `base117-20261107` and `base117-20261108-qual` campaigns. +The November 8 reruns used the manifest with SHA-256 +`1CCC81CAAA597EE2EC2AD7CC134730F1E4A582DB2A79817A07FC6E5005F7C1AB` +unless a profile explicitly adds Mineralogy or omits Base Metals. +The two 30-minute reruns measured 38,346 fresh and 36,038 upgraded game ticks. +Both passed 30 input batches, full restarts and an independent replay of +33 checkpoints each. The earlier Advantage runs and failed attempts are +retained separately. None of those files are release content. diff --git a/docs/1.17/CURRENT-QUALIFICATION.md b/docs/1.17/CURRENT-QUALIFICATION.md new file mode 100644 index 00000000..8bdcd17c --- /dev/null +++ b/docs/1.17/CURRENT-QUALIFICATION.md @@ -0,0 +1,158 @@ +# Minecraft 1.17.1 Pre-Rename Test Record + +This record is for the earlier Advantage-named jars. See the +[base* candidate record](BASE-CANDIDATE.md) for the current jars. + +This is a local candidate, not a published release. **Qualification is incomplete +and it is not yet ready for manual acceptance.** A passing older jar, an isolated +successful rerun or a completed subset does not close a failed campaign. + +## Candidate + +| Mod | Version | Release SHA-256 | +| --- | --- | --- | +| Power Advantage | 2.4.8.117011 | `336D9A11F5488EBD63F208554A039A53A398ABC58F632880B8420AF10F5AFBF0` | +| Steam Advantage | 2.2.9.117011 | `BA3488817313101329AC6FE6D371514E8697FFFBD490B7E2C98B4CA705290569` | +| Electric Advantage | 2.2.9.117011 | `E1B4A7C65E09B6EE218F52328F0CD569A61B37E8646A46AC6D17EB3948D60EE4` | + +Production source checkpoints are Power `5b9abe5`, Steam `3348ebf` and Electric +`9e6be35`. Packaged tests use published Base Metals `3.1.0.117011` and OreSpawn +`4.1.0.117011`; oil tests also use published Mineralogy `6.1.4.117011`. +Minecraft runs on Java 16; Gradle runs on Java 17. + +Evidence is retained locally under +`D:/PowerAdvantage/_verification/advantage-117-port-20261031/` and +`test-rig/build/`. Generated worlds, dependency jars and reports are not release +content or intended for Git. Frozen manifests and failed reports are never +overwritten by a later test-harness revision. + +## Completed Scope + +These results use the production jars above. Most use the frozen O harness; +the separately frozen P harness changes test observation, not production jars. + +| Check | Result and boundary | +| --- | --- | +| Release audits | Power 201, Steam 199 and Electric 96 tests pass, with dependency, archive, bytecode, resource and Eclipse checks | +| Independent compiler and reproducibility | ECJ checks and two independent source exports per mod; all four artifacts match, including fresh-cache and spaced-path builds | +| O machine operation | 132 measured setups pass, with exact cold inspection and independent raw-save replay | +| Imported 1.16 operating setup | 132 layouts preserved across conversion and full restart without reconstruction; final stopped terrain also matches independent raw replay | +| O connected upgrades | 20 operating routes pass at a fixed 1,024-tick interval; 40 raw-save windows have no differences | +| Fluid interoperability | 22 operating and two restored cases pass | +| Oil | Six lanes, three boots and two full restarts pass; correct identities and amounts for Advantage and Mineralogy oils | +| Harvesting | 271 actual tool/tier/drop cases pass on each boot | +| Actors and player storage | Eight actor routes and six player-layout routes pass, with independent saved-data checks | +| P respawn | All 72 Nether/End server-handler cases and independent audits pass using immutable per-case requests; startup reliability and recipe qualification remain separate | +| P world generation | Six profiles, twelve boots and 20,004 sampled chunks pass independent owner, biome, dimension, duplicate and ticket-cleanup audits; 36 clean logs retained | +| Q instrumented recipes | Five discovery boots and eighteen manufacturing checks pass, with 36 single hammer invocations independently replayed from exact native query, selector, action, debit and output telemetry | +| Transformer order | Six worlds and 12 launches pass semantics; individual automatic backups depend on callback order, so an independent whole-world backup is required | +| Partial-network unloading | Wide bridge and extended-drill layouts pass in both worlds, with actual unload events, both load orders, finite work, restart and independent raw replay | +| P drill lifecycle | Seven groups of twelve cases pass in each world; independent audits corroborate 48 actual native callbacks across seven boots, without manually ticking machines | +| Endurance | Two worlds complete thirty one-minute batches, full process restart and resumed processing; independently replayed raw checkpoints | +| Redstone-held drills | Nine cases pass; eight held drills do no work or energy debit and one unheld control advances for sixteen native ticks; both stopped terrain boundaries match | +| Combined client | Current jars pass 94 items, 905 block states, 351 caption cases and 355 broken controls; independent comparison binds all 542 loose Power entries and four runtime dependencies to their pins | +| Walkable Works | All 21 stations exercised in current and no-Base-Metals profiles before and after restart; 21 manual checklists per phase remain open | +| Harness controls | The P checkpoint passes 315 Java and 871 native-adapter Python controls; the instrumented Q source passes 327 Java and 876 native-adapter Python controls. Fourteen shared controls also pass. Native scope is recorded separately | + +The no-Base-Metals profile still includes required OreSpawn. It is not an +absent-OreSpawn startup test. Endurance uses fresh equipment and rebuilt Works +inside a disposable migrated world, not thirty minutes of every original legacy +machine. Imported-layout preservation is not credited as another operating test. + +## Open Failures + +- **Startup deadlock:** the O End-dimension run captured two thread dumps showing + Base Metals' `ContentModeNetwork.register` initializing `FMLHandshakeHandler` + while Forge initializes `FMLNetworkConstants` on another loading thread. Each + waits for the other's class initialization. That route remains failed. + Three other O loading stalls have no thread dumps and are not automatically + assigned the same cause. Forty-eight startup-control boots and a later + instrumented import passed, but do not repair or explain those failures. +- **Frozen migration coverage:** O completed 14 of 16 routes. Two routes failed + during mod loading. Available raw windows match, but missing windows are not + passes. +- **Dimension coverage:** O completed all eight Nether routes. The End campaign + stopped during its first route's restart; only 25 of the required 48 boots + completed across both dimensions. Earlier complete matrices used a different + Steam jar and do not close this gap. +- **Recipes:** five discovery boots passed, but the operating recipe campaign + failed its live hammer check after resource reload in APOCALYPTIC mode. + Thirteen of eighteen manufacturing checks completed. Native item-query + visibility is a hypothesis, not an established gameplay diagnosis. A finite + P control subsequently passed all five discovery boots and eighteen + manufacturing checks without changing the hammer protocol. Q then added + exact native query/selector observation and fail-closed setup gates. All + original action and output assertions remain; its 36 invocations pass + independent replay. This proves the new run's conditions and bounded + behaviour, not the cause of the earlier uninstrumented failure. The historical + investigation remains open; no production repair is claimed. + +The earlier world-generation sampler blocked on synchronous FULL-chunk loading +and hit the native watchdog. P uses asynchronous native requests with the same +sample counts, biome checks and deadlines. All six profiles now pass, including +Mineralogy, legacy import and the combined Base Metals installation. All 47,016 +requests completed and their tickets were released. The original failure remains. + +O's changed-target guard failed on seven Steam drill rows without proving the +first eligible callback. P now records that actual native callback and passes +both worlds with the original state, quantity and work assertions unchanged. +Its general raw-evidence audit and separate callback audit pass; all 1,043 +guarded files remained unchanged. The original O failure is retained. This is +an observer repair, not a new Steam production fix or a startup-deadlock fix. + +O's End respawn attempt failed while replacing a live fixture file. P avoids +that replacement with immutable requests; all 72 cases and independent audits +now pass. The original failed attempt remains retained. Server adapters do not +prove real-client death screens or resolve the separate startup deadlock. + +The O dimension handoff is +`dimensions-native117-o-failed-handoff.json` in the local evidence root. +Machine, recipe, harvest and interoperability receipts are indexed by +`final-o/handoff.json`; its overall recipe failure must not be overlooked. +The frozen P input is `test-rig/build/native117-target-p.json`. A frozen input +is not a qualification certificate. + +The Q hammer input and its independent invocation replay are retained under +`hammer-observer-q/` in the local evidence root. The same instrumented Java +source rebuilt in the main checkout produces byte-identical harness jars. +The source profiles use the new pins, with no machine-specific provenance paths; +they remain explicitly unqualified. + +Resolve the dependency startup deadlock and complete the missing frozen/End +routes before manual acceptance. A successful boot of another fixture does not +fill a missing route or prove startup reliability. + +## Repairs And Evidence Rules + +Production repairs during this port cover native ingredient predicates, dirty +machine state after autosave, exact vanilla path/sound aliases and nine Steam +recycling recipes whose external serializer needs a Base Metals presence guard. +No recipe ratio or gameplay balance change is intended. + +Test repairs remain separate. The connected observation window was fixed at +1,024 ticks because some drills require 576 powered steps. Non-mutating entity +observers avoid allocating empty `ForgeData`. Empty NBT is not broadly ignored. +The natural-tick observer transparently wraps the native delegate, forwards it +once and records its first callback; it never manually ticks a machine. + +Strict tamed-wolf health comparison reports 8 becoming 20. A separate native +control proves that exact platform difference; no other entity change is allowed. +Non-persistent leash helpers receive lifecycle evidence, not raw-storage UUID +persistence credit. Incomplete or invalid short bridge layouts remain failed. + +## Manual Boundary + +The current combined-client evidence is +`run-client-resource-runs/20261031-101716-9e75e592/`, with the independent +`client-final-production-o-binding.json` receipt in the local evidence root. +This is a worldless resource, caption and geometry check, not live-world or +manual visual acceptance. + +Manual checks still include normal Eclipse startup, visual appearance, recipe +book interaction, live turbine/drill animation, elevator rides, turret display +and multiplayer interaction. All three mods share Power Advantage's creative +tab. See the [manual checklist](MANUAL-TESTING.md). + +Use disposable copies. These fixtures do not qualify the entire Sylvester +modpack or unrelated mods without target-version releases. No 1.17 port code +has been pushed, merged, tagged or published. diff --git a/docs/1.17/MANUAL-TESTING.md b/docs/1.17/MANUAL-TESTING.md new file mode 100644 index 00000000..dfa0f2f2 --- /dev/null +++ b/docs/1.17/MANUAL-TESTING.md @@ -0,0 +1,70 @@ +# Minecraft 1.17.1 Manual Check + +Use the [test record](CURRENT-QUALIFICATION.md) for the exact candidate and the +limits of the automated checks. Manual acceptance is still open. Do not use the +only copy of an existing world. + +## Open The Projects + +Import these directories as existing Gradle projects in Eclipse: + +```text +D:\PowerAdvantage\PA117\PowerAdvantage +D:\PowerAdvantage\PA117\SteamAdvantage\SteamAdvantage +D:\PowerAdvantage\PA117\ElectricAdvantage\ElectricAdvantage +``` + +Use a separate Eclipse workspace if an older port already uses these project +names. These are project directories, not pre-existing Eclipse workspaces. + +Use Java 17 for Gradle and Java 16 for the project JRE and Minecraft launches. +After refreshing the Gradle projects, clean the Eclipse projects so their Java +and processed-resource output is current. + +Power's `runClient.launch` loads Power alone. Its +`runClientAllAdvantage.launch` also loads the pinned Steam and Electric builds. +Prepare the latter with `gradlew.bat prepareCombinedEclipseLaunch` if needed. +Both use Power's `run` directory. Neither launch includes Advantage Works or +automatically opens a test world. + +## Look And Interact + +- Check the shared Power Advantage creative tab, names, tooltips and recipe-book + pages for all three mods. Unlock + recipes by collecting ingredients, not by granting every recipe at once. +- Open the machine menus at the GUI scales you normally use. Check titles, + inventory labels, slot alignment, gauges, hover text and filled-fluid colours. + Put items and fluids in the machine; an empty drawing is not a useful test of + every gauge state. +- Place horizontal and vertical drills. Watch their moving parts and output, + then save, close Minecraft completely and reopen the copied world. Check + facing, pressure or power, inventory and continued work together. +- Inspect live turbines, elevator travel, lighting and turret presentation. + Automated state and renderer checks do not replace watching these in a world. +- Try ordinary multiplayer interaction if that is part of the intended pack. + Server-side player adapters do not prove a real client's death screen, + reconnect experience or network presentation. + +## Try An Existing World Copy + +Keep an independent backup of the entire world, including player and dimension +data. Per-mod migration backups can contain intermediate data from another mod's +hook; they are not a substitute for that backup. + +Only load a copy whose other mods have compatible target versions. Missing +unrelated mods are not an acceptable reason to lose their blocks or inventories. +This port's tests do not qualify the complete Sylvester modpack. + +Before opening the copy, note a few working machines, their contents, tank +levels, battery charge and drill directions. Check them immediately after the +upgrade, let them work, then save and close the entire game before checking +again. Do not repair pipes or replace machines before recording a fault: that +would hide a restoration problem. + +The strict dimension fixtures record one independently reproduced native change: +tamed-wolf health can become 20 instead of 8. That does not excuse any other +entity, inventory or machine-state change. + +For a problem report, keep the log, screenshot, candidate versions and untouched +source copy. Note whether it happened on first import, during operation or only +after restarting. Do not save over the original world while investigating. diff --git a/docs/1.17/PORT-STATUS.md b/docs/1.17/PORT-STATUS.md new file mode 100644 index 00000000..0c154f85 --- /dev/null +++ b/docs/1.17/PORT-STATUS.md @@ -0,0 +1,28 @@ +# Minecraft 1.17.1 Port + +This is a local port candidate. It has not been approved for normal worlds or +published. Earlier-version results do not qualify this version. + +basePower targets Forge 37.1.1 with Java 16 production bytecode. Gradle +runs on Java 17; Mavenizer needs Java 25 and the legacy renamer uses Java 8. +The dependency source commits and development-jar checksums are pinned in +`gradle.properties`. Packaged tests use the local Base Metals 3.1.1.117011 +startup fix and the published OreSpawn 4.1.0.117011 jar. + +The port keeps registry and saved-state identities. Native block-entity factories +now receive their real position and block state, and native tickers dispatch +the existing machine update logic. Configured material aliases are additive +server data-pack tags. Legacy migration also covers separate entity chunks. + +Qualification is in progress; see the [base* test record](BASE-CANDIDATE.md). +The [pre-rename test record](CURRENT-QUALIFICATION.md) is historical evidence. +The handover will list the tested artifact hashes, +completed checks, failures and remaining manual checks. Compilation, saved-state +presence, animation or a green result from an older port is not evidence that a +machine has done useful work in 1.17. + +Use disposable copies for every upgrade test. Never open the only copy of a +valuable world with a port candidate. + +The [manual checklist](MANUAL-TESTING.md) gives the Eclipse project paths and +the remaining visual, interaction and copied-world checks. diff --git a/gradle.properties b/gradle.properties index 72de79b5..98fc9cc1 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.16.5 -minecraft_version_range=[1.16.5] -forge_version=36.2.34 -forge_installer_sha256=53B73DBBCC486F38726E76CE923B13381044984C83C762847C7193340F97130F -forge_version_range=[36.2.34,37) -loader_version_range=[36,) +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,) mapping_channel=official -mapping_version=1.16.5 -mcp_version=20210115.111550 +mapping_version=1.17.1 +mcp_version=20210706.113038 loader_name=forge loader_code=1 -java_version=8 -java_toolchain_version=8.0.502+7 -java_setup_version=8.0.502+7 +java_version=16 +java_toolchain_version=16.0.2+7 +java_setup_version=16.0.2+7 gradle_java_version=17 -curseforge_project_id=240982 +curseforge_project_id=1780140 -mod_id=poweradvantage -mod_name=Power Advantage +mod_id=basepower +mod_name=basePower mod_license=LGPL-3.0-only -mod_version=2.4.7.116051 -mod_group=zone.moddev.mc.poweradvantage +mod_version=2.4.9.117011 +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.16.5 +cf_versions=1.17.1 -basemetals_version=3.1.1.116051 -basemetals_source_commit=995f6cc4dd2d5eecc628a42e5c43fa5390c24bca +basemetals_version=3.1.1.117011 +basemetals_source_commit=0deeeeecdbbc1fbcdecf910c59477de8efe164d7 basemetals_curse_project_id=240967 -basemetals_deobf_sha256=A7B0C4F394DFAF4E11E0275B1A86CB1D47C7AF8E175759D9BB84F965C684466D +basemetals_deobf_sha256=83A8C32C633B56BB7E4405E8B3B24A4DEA32B9119216E5097B884056D679CAD1 -orespawn_version=4.1.0.116051 -orespawn_source_commit=a113e5d767169189f0e35ade1e173a12d550b00e +orespawn_version=4.1.0.117011 +orespawn_source_commit=b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf orespawn_curse_project_id=245586 -orespawn_deobf_sha256=DCEE75480C9DD70EE58B218AAE799C5FAB0525DAF9A05455270F1570C24F7F8F +orespawn_deobf_sha256=CB7A736314842B110B2E4788FBD5E21D4AE8803C5ED34A49199B034BE572DA39 diff --git a/src/main/java/cyano/poweradvantage/api/ConduitBlock.java b/src/main/java/cyano/poweradvantage/api/ConduitBlock.java index 156f84bf..f06b6433 100644 --- a/src/main/java/cyano/poweradvantage/api/ConduitBlock.java +++ b/src/main/java/cyano/poweradvantage/api/ConduitBlock.java @@ -1,11 +1,11 @@ package cyano.poweradvantage.api; -import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry; -import net.minecraft.block.material.Material; -import net.minecraft.block.BlockState; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.Explosion; -import net.minecraft.world.World; +import zone.moddev.mc.basepower.conduitnetwork.ConduitRegistry; +import net.minecraft.world.level.material.Material; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.Level; /** * @@ -23,7 +23,7 @@ * @author DrCyano * */ -public abstract class ConduitBlock extends net.minecraft.block.Block implements ITypedConduit { +public abstract class ConduitBlock extends net.minecraft.world.level.block.Block implements ITypedConduit { /** * Block constructor * @@ -45,7 +45,7 @@ protected ConduitBlock(Properties properties) { * This method is called whenever the block is placed into the world */ @Override - public void onPlace(BlockState state, World w, BlockPos coord, BlockState oldState, boolean isMoving) { + public void onPlace(BlockState state, Level w, BlockPos coord, BlockState oldState, boolean isMoving) { super.onPlace(state, w, coord, oldState, isMoving); ConduitRegistry.getInstance().conduitBlockPlacedEvent(w, coord, getTypes()); } @@ -54,7 +54,7 @@ public void onPlace(BlockState state, World w, BlockPos coord, BlockState oldSta * This method is called when the block is removed from the world by an entity. */ @Override - public void onRemove(BlockState state, World w, BlockPos coord, BlockState newState, boolean isMoving) { + public void onRemove(BlockState state, Level w, BlockPos coord, BlockState newState, boolean isMoving) { if (state.getBlock() != newState.getBlock()) { ConduitRegistry.getInstance().conduitBlockRemovedEvent(w, coord, getTypes()); } @@ -65,7 +65,7 @@ public void onRemove(BlockState state, World w, BlockPos coord, BlockState newSt * This method is called when the block is destroyed by an explosion. */ @Override - public void wasExploded(World w, BlockPos coord, Explosion boom) { + public void wasExploded(Level w, BlockPos coord, Explosion boom) { super.wasExploded(w, coord, boom); ConduitRegistry.getInstance().conduitBlockRemovedEvent(w, coord, getTypes()); } diff --git a/src/main/java/cyano/poweradvantage/api/ConduitType.java b/src/main/java/cyano/poweradvantage/api/ConduitType.java index f782afc5..8ed905a9 100644 --- a/src/main/java/cyano/poweradvantage/api/ConduitType.java +++ b/src/main/java/cyano/poweradvantage/api/ConduitType.java @@ -1,6 +1,6 @@ package cyano.poweradvantage.api; -import zone.moddev.mc.poweradvantage.util.HashCodeHelper; +import zone.moddev.mc.basepower.util.HashCodeHelper; import java.util.Locale; diff --git a/src/main/java/cyano/poweradvantage/api/FluidCategoryRegistry.java b/src/main/java/cyano/poweradvantage/api/FluidCategoryRegistry.java index 12c3a2c6..333fb963 100644 --- a/src/main/java/cyano/poweradvantage/api/FluidCategoryRegistry.java +++ b/src/main/java/cyano/poweradvantage/api/FluidCategoryRegistry.java @@ -7,9 +7,9 @@ import java.util.Map; import java.util.Set; -import net.minecraft.fluid.Fluid; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.Registry; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.Registry; /** * Registry of semantic fluid categories shared by Advantage mods. @@ -26,6 +26,7 @@ public final class FluidCategoryRegistry { static { registerAlias(CRUDE_OIL, "crude_oil"); + registerAlias(CRUDE_OIL, "basepower:crude_oil"); registerAlias(CRUDE_OIL, "poweradvantage:crude_oil"); registerAlias(CRUDE_OIL, "mineralogy_crude_oil"); registerAlias(CRUDE_OIL, "mineralogy:crude_oil"); diff --git a/src/main/java/cyano/poweradvantage/api/GUIBlock.java b/src/main/java/cyano/poweradvantage/api/GUIBlock.java index 653a0239..b7f828db 100644 --- a/src/main/java/cyano/poweradvantage/api/GUIBlock.java +++ b/src/main/java/cyano/poweradvantage/api/GUIBlock.java @@ -1,24 +1,26 @@ package cyano.poweradvantage.api; -import net.minecraft.block.material.Material; -import net.minecraft.block.Block; -import net.minecraft.block.SoundType; -import net.minecraft.block.BlockState; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.InventoryHelper; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.block.BlockRenderType; -import net.minecraft.util.Direction; -import net.minecraft.util.Hand; -import net.minecraft.util.ActionResultType; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; -import net.minecraft.world.IBlockReader; -import zone.moddev.mc.poweradvantage.registry.MachineGUIRegistry; +import net.minecraft.world.level.material.Material; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.entity.player.Player; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.Containers; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.BlockGetter; +import zone.moddev.mc.basepower.registry.MachineGUIRegistry; import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; -import zone.moddev.mc.poweradvantage.util.FluidContainerHelper; +import zone.moddev.mc.basepower.util.FluidContainerHelper; /** *

@@ -33,7 +35,7 @@ * @author DrCyano * */ -public abstract class GUIBlock extends net.minecraft.block.ContainerBlock{ +public abstract class GUIBlock extends net.minecraft.world.level.block.BaseEntityBlock{ /** * Constructor for GUI block @@ -52,6 +54,20 @@ protected GUIBlock(Block.Properties properties) { super(properties); } + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, + BlockEntityType type) { + if (!type.isValid(state)) return null; + return (world, pos, currentState, tile) -> { + if (tile.isRemoved() || tile.getType() != type || tile.getLevel() != world) return; + if (tile instanceof PoweredEntity) { + ((PoweredEntity) tile).tick(); + } else if (tile instanceof zone.moddev.mc.basepower.machines.conveyors.TileEntityConveyor) { + ((zone.moddev.mc.basepower.machines.conveyors.TileEntityConveyor) tile).tick(); + } + }; + } + @Deprecated protected void setPowerAdvantageHardness(float hardness) { // Block properties are immutable after construction in 1.13. @@ -114,8 +130,8 @@ public boolean isOpaqueCube(BlockState bs) { * -1 = special renderer */ @Override - public BlockRenderType getRenderShape(BlockState bs) { - return BlockRenderType.MODEL; + public RenderShape getRenderShape(BlockState bs) { + return RenderShape.MODEL; } /** @@ -127,43 +143,43 @@ public BlockRenderType getRenderShape(BlockState bs) { */ @SuppressWarnings("deprecation") @Override - public ActionResultType use(BlockState bs, World w, BlockPos coord, - PlayerEntity player, Hand hand, net.minecraft.util.math.BlockRayTraceResult hit) { + public InteractionResult use(BlockState bs, Level w, BlockPos coord, + Player player, InteractionHand hand, net.minecraft.world.phys.BlockHitResult hit) { Direction facing = hit.getDirection(); if (w.isClientSide) { - return ActionResultType.SUCCESS; + return InteractionResult.SUCCESS; } - final TileEntity tileEntity = w.getBlockEntity(coord); + final BlockEntity tileEntity = w.getBlockEntity(coord); if (tileEntity == null || player.isShiftKeyDown()) { - return ActionResultType.PASS; + return InteractionResult.PASS; } // handle buckets and fluid containers - net.minecraft.item.ItemStack item = player.getItemInHand(hand); + net.minecraft.world.item.ItemStack item = player.getItemInHand(hand); if(item != null && !item.isEmpty() && tileEntity instanceof IAdvantageFluidHandler && FluidContainerHelper.interact(player, hand, (IAdvantageFluidHandler) tileEntity, facing)) { - return ActionResultType.SUCCESS; + return InteractionResult.SUCCESS; } // open GUI - if(this.getGuiOwner() == null) return ActionResultType.PASS; - if(tileEntity instanceof PoweredEntity && player instanceof ServerPlayerEntity){ - ((PoweredEntity)tileEntity).syncPowerAdvantageDataTo((ServerPlayerEntity)player); + if(this.getGuiOwner() == null) return InteractionResult.PASS; + if(tileEntity instanceof PoweredEntity && player instanceof ServerPlayer){ + ((PoweredEntity)tileEntity).syncPowerAdvantageDataTo((ServerPlayer)player); } - if (!(player instanceof ServerPlayerEntity)) return ActionResultType.PASS; - MachineGUIRegistry.open((ServerPlayerEntity) player, w, coord, this.getGuiID()); - return ActionResultType.SUCCESS; + if (!(player instanceof ServerPlayer)) return InteractionResult.PASS; + MachineGUIRegistry.open((ServerPlayer) player, w, coord, this.getGuiID()); + return InteractionResult.SUCCESS; } @Override - public void onRemove(BlockState state, World world, BlockPos pos, BlockState newState, + public void onRemove(BlockState state, Level world, BlockPos pos, BlockState newState, boolean isMoving) { if (state.getBlock() != newState.getBlock()) { - TileEntity te = world.getBlockEntity(pos); - if (te instanceof IInventory) { - InventoryHelper.dropContents(world, pos, (IInventory) te); - ((IInventory) te).clearContent(); + BlockEntity te = world.getBlockEntity(pos); + if (te instanceof Container) { + Containers.dropContents(world, pos, (Container) te); + ((Container) te).clearContent(); world .updateNeighbourForOutputSignal(pos, this); } super.onRemove(state, world, pos, newState, isMoving); diff --git a/src/main/java/cyano/poweradvantage/api/PowerConnectorContext.java b/src/main/java/cyano/poweradvantage/api/PowerConnectorContext.java index ab97553e..d3d989e1 100644 --- a/src/main/java/cyano/poweradvantage/api/PowerConnectorContext.java +++ b/src/main/java/cyano/poweradvantage/api/PowerConnectorContext.java @@ -1,9 +1,9 @@ package cyano.poweradvantage.api; -import net.minecraft.block.BlockState; -import net.minecraft.util.Direction; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.IBlockReader; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; /** * This data structure class is used to reduce the parameter list of the ITypedConduit methods to a single pointer. @@ -14,7 +14,7 @@ public class PowerConnectorContext { /** * World instance */ - public final IBlockReader world; + public final BlockGetter world; /** * Type of power */ @@ -58,7 +58,7 @@ public class PowerConnectorContext { */ public PowerConnectorContext( final ConduitType powerType, - final IBlockReader world, + final BlockGetter world, final BlockState connectorBlock, final BlockPos connectorPosition, final Direction connectorSide, @@ -78,7 +78,7 @@ public PowerConnectorContext( public PowerConnectorContext( final ConduitType powerType, - final IBlockReader world, + final BlockGetter world, final BlockState connectorBlock, final BlockPos connectorPosition, final Direction connectorSide diff --git a/src/main/java/cyano/poweradvantage/api/PoweredEntity.java b/src/main/java/cyano/poweradvantage/api/PoweredEntity.java index f3680fb3..d886d570 100644 --- a/src/main/java/cyano/poweradvantage/api/PoweredEntity.java +++ b/src/main/java/cyano/poweradvantage/api/PoweredEntity.java @@ -1,15 +1,14 @@ package cyano.poweradvantage.api; -import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry; -import net.minecraft.block.BlockState; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.network.IPacket; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.tileentity.TileEntityType; -import net.minecraft.tileentity.ITickableTileEntity; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; +import zone.moddev.mc.basepower.conduitnetwork.ConduitRegistry; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.protocol.Packet; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; import java.util.List; @@ -25,28 +24,28 @@ * @author DrCyano * */ -public abstract class PoweredEntity extends TileEntity implements ITickableTileEntity, IPowerMachine { +public abstract class PoweredEntity extends BlockEntity implements IPowerMachine { private final int powerUpdateInterval = 8; private int conduitRefreshDelay = 2; - private final zone.moddev.mc.poweradvantage.util.MachineCapabilities machineCapabilities = - new zone.moddev.mc.poweradvantage.util.MachineCapabilities(this); + private final zone.moddev.mc.basepower.util.MachineCapabilities machineCapabilities = + new zone.moddev.mc.basepower.util.MachineCapabilities(this); @Override public net.minecraftforge.common.util.LazyOptional getCapability( - net.minecraftforge.common.capabilities.Capability capability, net.minecraft.util.Direction face) { + net.minecraftforge.common.capabilities.Capability capability, net.minecraft.core.Direction face) { if (!areCapabilitiesValid()) return net.minecraftforge.common.util.LazyOptional.empty(); if (machineCapabilities == null) return super.getCapability(capability, face); net.minecraftforge.common.util.LazyOptional local = machineCapabilities.get(capability, face); return local.isPresent() ? local : super.getCapability(capability, face); } - @Override protected void invalidateCaps() { + @Override public void invalidateCaps() { if (machineCapabilities != null) machineCapabilities.invalidate(); super.invalidateCaps(); } - @Override protected void reviveCaps() { + @Override public void reviveCaps() { super.reviveCaps(); if (machineCapabilities != null) machineCapabilities.revive(); } @@ -60,8 +59,8 @@ protected boolean areCapabilitiesValid() { reviveCaps(); } - protected PoweredEntity(TileEntityType type) { - super(type); + protected PoweredEntity(BlockEntityType type, BlockPos pos, BlockState state) { + super(type, pos, state); } /** @@ -110,13 +109,16 @@ public float addEnergy(float energy, ConduitType type) { if (newValue < 0) { float delta = -1 * this.getEnergy(type); this.setEnergy(0, type); + if (delta != 0) markMachineStateUnsaved(); return delta; } else if (newValue > this.getEnergyCapacity(type)) { float delta = this.getEnergyCapacity(type) - this.getEnergy(type); this.setEnergy(this.getEnergyCapacity(type), type); + if (delta != 0) markMachineStateUnsaved(); return delta; } this.setEnergy(newValue, type); + if (energy != 0) markMachineStateUnsaved(); return energy; } @@ -154,11 +156,11 @@ protected final void sync() { * @param newSate State after change * @return true if this TileEntity should be invalidated (deleted and recreated), false otherwise */ - public boolean shouldRefresh(World world, BlockPos pos, BlockState oldState, BlockState newSate) { + public boolean shouldRefresh(Level world, BlockPos pos, BlockState oldState, BlockState newSate) { return oldState.getBlock() != newSate.getBlock(); } - protected final World powerAdvantageWorld() { + protected final Level powerAdvantageWorld() { return super.getLevel(); } @@ -182,9 +184,9 @@ public void onLoad() { * * @param player player receiving the current machine state */ - public final void syncPowerAdvantageDataTo(ServerPlayerEntity player) { + public final void syncPowerAdvantageDataTo(ServerPlayer player) { this.setChanged(); - IPacket packet = this.getUpdatePacket(); + Packet packet = this.getUpdatePacket(); if (packet != null) { player.connection .send(packet); } @@ -194,7 +196,6 @@ public final void syncPowerAdvantageDataTo(ServerPlayerEntity player) { * Method net.minecraft.server.gui.IUpdatePlayerListBox.update() is invoked * to do tick updates */ - @Override public final void tick() { // this method was moved from TileEntity to IUpdatePlayerListBox if (this.isServer()) { @@ -204,6 +205,17 @@ public final void tick() { if (this.isServer() && ((this.getLevel().getGameTime() + this.getTickOffset()) % powerUpdateInterval == 0)) { this.powerUpdate(); } + markMachineStateUnsaved(); + } + + private void markMachineStateUnsaved() { + if (!(this.level instanceof net.minecraft.server.level.ServerLevel) || isRemoved()) return; + BlockPos pos = getBlockPos(); + net.minecraft.world.level.chunk.LevelChunk chunk = ((net.minecraft.server.level.ServerLevel) this.level) + .getChunkSource().getChunkNow(pos.getX() >> 4, pos.getZ() >> 4); + // Timers and externally supplied energy can change without a GUI sync. + // Mark the loaded chunk only; do not load neighbours or notify comparators. + if (chunk != null) chunk.setUnsaved(true); } private void refreshConduitNetworkAfterLoad() { @@ -267,8 +279,8 @@ private final int getTickOffset() { * packet. */ @Override - public void load(net.minecraft.block.BlockState state, final CompoundNBT tagRoot) { - super.load(state, tagRoot); + public void load(final CompoundTag tagRoot) { + super.load(tagRoot); this.conduitRefreshDelay = 2; if (tagRoot.contains("Energy", 11)) { int[] data = tagRoot.getIntArray("Energy"); @@ -284,7 +296,7 @@ public void load(net.minecraft.block.BlockState state, final CompoundNBT tagRoot * the network. */ @Override - public CompoundNBT save(final CompoundNBT tagRoot) { + public CompoundTag save(final CompoundTag tagRoot) { super.save(tagRoot); int[] data = new int[this.getTypes().length]; for (int i = 0; i < data.length; i++) { @@ -294,11 +306,11 @@ public CompoundNBT save(final CompoundNBT tagRoot) { return tagRoot; } - /** @deprecated Minecraft now calls {@link #load(BlockState, CompoundNBT)}. */ - @Deprecated public void read(CompoundNBT tag) { load(getBlockState(), tag); } + /** @deprecated Minecraft now calls {@link #load(CompoundTag)}. */ + @Deprecated public void read(CompoundTag tag) { load(tag); } - /** @deprecated Minecraft now calls {@link #save(CompoundNBT)}. */ - @Deprecated public CompoundNBT write(CompoundNBT tag) { return save(tag); } + /** @deprecated Minecraft now calls {@link #save(CompoundTag)}. */ + @Deprecated public CompoundTag write(CompoundTag tag) { return save(tag); } /** diff --git a/src/main/java/cyano/poweradvantage/api/fluid/ColoredFluid.java b/src/main/java/cyano/poweradvantage/api/fluid/ColoredFluid.java index 292d7e68..70f16cf7 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/ColoredFluid.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/ColoredFluid.java @@ -1,6 +1,6 @@ package cyano.poweradvantage.api.fluid; -import net.minecraft.util.ResourceLocation; +import net.minecraft.resources.ResourceLocation; /** * Legacy description of a colored fluid. diff --git a/src/main/java/cyano/poweradvantage/api/fluid/FluidConduitBlock.java b/src/main/java/cyano/poweradvantage/api/fluid/FluidConduitBlock.java index bbec92ef..9e02292a 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/FluidConduitBlock.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/FluidConduitBlock.java @@ -3,8 +3,8 @@ import cyano.poweradvantage.api.ConduitBlock; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PowerConnectorContext; -import zone.moddev.mc.poweradvantage.init.Fluids; -import net.minecraft.block.material.Material; +import zone.moddev.mc.basepower.init.Fluids; +import net.minecraft.world.level.material.Material; /** *

diff --git a/src/main/java/cyano/poweradvantage/api/fluid/FluidNetworkApi.java b/src/main/java/cyano/poweradvantage/api/fluid/FluidNetworkApi.java index cebc5d6f..14eb54bd 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/FluidNetworkApi.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/FluidNetworkApi.java @@ -6,11 +6,11 @@ import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PowerRequest; -import net.minecraft.block.Block; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; -import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry; -import zone.moddev.mc.poweradvantage.init.Fluids; +import net.minecraft.world.level.block.Block; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import zone.moddev.mc.basepower.conduitnetwork.ConduitRegistry; +import zone.moddev.mc.basepower.init.Fluids; /** * Stable entry point for offering fluid from another mod to a PowerAdvantage @@ -27,7 +27,7 @@ private FluidNetworkApi() { * * @return the exact number of millibuckets accepted by the network */ - public static int offerFluid(World world, BlockPos conduitPosition, FluidStack offered, int maxTransfer) { + public static int offerFluid(Level world, BlockPos conduitPosition, FluidStack offered, int maxTransfer) { return offerFluid(world, conduitPosition, offered, maxTransfer, PowerRequest.LAST_PRIORITY, null); } @@ -39,7 +39,7 @@ public static int offerFluid(World world, BlockPos conduitPosition, FluidStack o * @param source optional source object to exclude from receiving its own offer * @return the exact number of millibuckets accepted by consumers */ - public static int offerFluid(World world, BlockPos conduitPosition, FluidStack offered, int maxTransfer, + public static int offerFluid(Level world, BlockPos conduitPosition, FluidStack offered, int maxTransfer, byte minimumPriority, @Nullable Object source) { if (world == null || world.isClientSide || conduitPosition == null || offered == null || offered.getFluid() == null || offered.amount <= 0 || maxTransfer <= 0 diff --git a/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java b/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java index 6185d00f..600c0574 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java @@ -3,12 +3,14 @@ import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PowerRequest; import cyano.poweradvantage.api.PoweredEntity; -import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry; -import zone.moddev.mc.poweradvantage.init.Fluids; -import net.minecraft.fluid.Fluid; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.tileentity.TileEntityType; -import net.minecraft.util.Direction; +import zone.moddev.mc.basepower.conduitnetwork.ConduitRegistry; +import zone.moddev.mc.basepower.init.Fluids; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.state.BlockState; import java.util.List; @@ -30,8 +32,8 @@ */ public abstract class FluidPoweredEntity extends PoweredEntity implements IAdvantageFluidHandler { - protected FluidPoweredEntity(TileEntityType type) { - super(type); + protected FluidPoweredEntity(BlockEntityType type, BlockPos pos, BlockState state) { + super(type, pos, state); } @@ -79,9 +81,9 @@ protected List getRequestsForFluid(ConduitType powerType) { * @param tagRoot An NBT tag */ @Override - public CompoundNBT save(final CompoundNBT tagRoot) { + public CompoundTag save(final CompoundTag tagRoot) { super.save(tagRoot); - CompoundNBT tankTag = new CompoundNBT(); + CompoundTag tankTag = new CompoundTag(); this.getTank().writeToNBT(tankTag); tagRoot.put("Tank", tankTag); return tagRoot; @@ -93,10 +95,10 @@ public CompoundNBT save(final CompoundNBT tagRoot) { * @param tagRoot An NBT tag */ @Override - public void load(net.minecraft.block.BlockState state, final CompoundNBT tagRoot) { - super.load(state, tagRoot); + public void load(final CompoundTag tagRoot) { + super.load(tagRoot); if (tagRoot.contains("Tank", 10)) { - CompoundNBT tankTag = tagRoot .getCompound("Tank"); + CompoundTag tankTag = tagRoot .getCompound("Tank"); getTank().readFromNBT(tankTag); if (tankTag.contains("Empty", 1)) { // empty the tank if NBT says its empty (not default behavior of Tank.readFromNBT(...) ) diff --git a/src/main/java/cyano/poweradvantage/api/fluid/FluidStack.java b/src/main/java/cyano/poweradvantage/api/fluid/FluidStack.java index 83b78291..f24dc411 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/FluidStack.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/FluidStack.java @@ -2,11 +2,11 @@ import javax.annotation.Nullable; -import net.minecraft.fluid.Fluid; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.Registry; -import zone.moddev.mc.poweradvantage.init.Fluids; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.Registry; +import zone.moddev.mc.basepower.init.Fluids; /** * Stable Advantage millibucket value object. Native Forge fluid capabilities @@ -15,7 +15,7 @@ public final class FluidStack { private final Fluid fluid; public int amount; - @Nullable public CompoundNBT tag; + @Nullable public CompoundTag tag; public FluidStack(Fluid fluid, int amount) { if (fluid == null) throw new IllegalArgumentException("fluid"); @@ -36,7 +36,7 @@ public boolean isFluidEqual(@Nullable FluidStack other) { && (tag == null ? other.tag == null : tag.equals(other.tag)); } - public CompoundNBT writeToNBT(CompoundNBT nbt) { + public CompoundTag writeToNBT(CompoundTag nbt) { ResourceLocation id = net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(fluid); if (id != null) nbt.putString("FluidName", id.toString()); nbt.putInt("Amount", amount); @@ -45,7 +45,7 @@ public CompoundNBT writeToNBT(CompoundNBT nbt) { } @Nullable - public static FluidStack loadFluidStackFromNBT(CompoundNBT nbt) { + public static FluidStack loadFluidStackFromNBT(CompoundTag nbt) { if (nbt == null || !nbt.contains("FluidName", 8)) return null; Fluid fluid = Fluids.byName(nbt.getString("FluidName")); if (fluid == null) return null; diff --git a/src/main/java/cyano/poweradvantage/api/fluid/FluidTank.java b/src/main/java/cyano/poweradvantage/api/fluid/FluidTank.java index 8d18bf42..69d45666 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/FluidTank.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/FluidTank.java @@ -2,8 +2,8 @@ import javax.annotation.Nullable; -import net.minecraft.fluid.Fluid; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.nbt.CompoundTag; /** Millibucket tank retaining the Advantage fluid API and saved NBT format. */ public class FluidTank { @@ -71,13 +71,13 @@ protected void onContentsChanged() {} public FluidTankInfo getInfo() { return new FluidTankInfo(fluid, capacity); } - public CompoundNBT writeToNBT(CompoundNBT nbt) { + public CompoundTag writeToNBT(CompoundTag nbt) { if (fluid == null) nbt.putBoolean("Empty", true); else fluid.writeToNBT(nbt); return nbt; } - public FluidTank readFromNBT(CompoundNBT nbt) { + public FluidTank readFromNBT(CompoundTag nbt) { setFluid(nbt.contains("Empty", 1) ? null : FluidStack.loadFluidStackFromNBT(nbt)); return this; } diff --git a/src/main/java/cyano/poweradvantage/api/fluid/IAdvantageFluidHandler.java b/src/main/java/cyano/poweradvantage/api/fluid/IAdvantageFluidHandler.java index 54352dff..a463c880 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/IAdvantageFluidHandler.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/IAdvantageFluidHandler.java @@ -2,8 +2,8 @@ import javax.annotation.Nullable; -import net.minecraft.fluid.Fluid; -import net.minecraft.util.Direction; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.core.Direction; /** * Stable millibucket fluid contract used by Advantage machines. On Forge 1.14 diff --git a/src/main/java/cyano/poweradvantage/api/fluid/InteractiveFluidBlock.java b/src/main/java/cyano/poweradvantage/api/fluid/InteractiveFluidBlock.java index a81598b8..0d160ac8 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/InteractiveFluidBlock.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/InteractiveFluidBlock.java @@ -2,43 +2,43 @@ import java.util.function.BiConsumer; -import net.minecraft.block.Block; -import net.minecraft.block.FlowingFluidBlock; -import net.minecraft.block.BlockState; -import net.minecraft.entity.Entity; -import net.minecraft.entity.LivingEntity; -import net.minecraft.fluid.FlowingFluid; -import net.minecraft.util.Direction; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.IBlockReader; -import net.minecraft.world.World; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.material.FlowingFluid; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; /** A native 1.13 fluid block which can affect immersed living entities. */ -public class InteractiveFluidBlock extends FlowingFluidBlock { - private final BiConsumer immersionEffect; +public class InteractiveFluidBlock extends LiquidBlock { + private final BiConsumer immersionEffect; private final boolean flammable; public InteractiveFluidBlock(FlowingFluid fluid, Block.Properties properties, boolean flammable, - BiConsumer immersionEffect) { + BiConsumer immersionEffect) { super(fluid, properties); this.flammable = flammable; this.immersionEffect = immersionEffect; } @Override - public void entityInside(BlockState state, World world, BlockPos pos, Entity entity) { + public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { if (immersionEffect != null && entity instanceof LivingEntity) { immersionEffect.accept(world, (LivingEntity) entity); } } @Override - public int getFlammability(BlockState state, IBlockReader world, BlockPos pos, Direction face) { + public int getFlammability(BlockState state, BlockGetter world, BlockPos pos, Direction face) { return flammable ? 60 : 0; } @Override - public int getFireSpreadSpeed(BlockState state, IBlockReader world, BlockPos pos, Direction face) { + public int getFireSpreadSpeed(BlockState state, BlockGetter world, BlockPos pos, Direction face) { return flammable ? 30 : 0; } } diff --git a/src/main/java/cyano/poweradvantage/api/migration/LegacyConfigMigration.java b/src/main/java/cyano/poweradvantage/api/migration/LegacyConfigMigration.java index 98a307d9..9f7780b4 100644 --- a/src/main/java/cyano/poweradvantage/api/migration/LegacyConfigMigration.java +++ b/src/main/java/cyano/poweradvantage/api/migration/LegacyConfigMigration.java @@ -72,7 +72,13 @@ private Result(boolean migrated, int importedValues, Path legacyFile, Path targe } public static Result migrate(Path configDirectory, String modId, Mapping... mappings) throws IOException { + return migrate(configDirectory, modId, modId, mappings); + } + + public static Result migrate(Path configDirectory, String modId, String legacyModId, Mapping... mappings) + throws IOException { Path legacyFile = configDirectory.resolve(modId + ".cfg"); + if (!Files.isRegularFile(legacyFile)) legacyFile = configDirectory.resolve(legacyModId + ".cfg"); Path targetFile = configDirectory.resolve(modId + "-common.toml"); Path reportFile = configDirectory.resolve(modId + "-legacy-config-migration.txt"); if (!Files.isRegularFile(legacyFile) || Files.exists(targetFile)) { @@ -107,7 +113,7 @@ public static Result migrate(Path configDirectory, String modId, Mapping... mapp } List toml = new ArrayList<>(); - toml.add("# Generated once from " + modId + ".cfg by the Advantage 1.13 migration."); + toml.add("# Generated once from " + legacyFile.getFileName() + " by the basePower migration."); toml.add("# The original cfg file has deliberately been retained."); toml.add(""); for (Map.Entry> section : sections.entrySet()) { diff --git a/src/main/java/cyano/poweradvantage/api/migration/LegacyMigrationApi.java b/src/main/java/cyano/poweradvantage/api/migration/LegacyMigrationApi.java index 765c7861..a31200b6 100644 --- a/src/main/java/cyano/poweradvantage/api/migration/LegacyMigrationApi.java +++ b/src/main/java/cyano/poweradvantage/api/migration/LegacyMigrationApi.java @@ -7,10 +7,10 @@ import java.util.Map; import java.util.Set; -import net.minecraft.block.Block; -import net.minecraft.block.BlockState; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.util.ResourceLocation; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; /** * Registration surface for Advantage add-ons that need pre-flattening world @@ -25,10 +25,11 @@ public interface BlockStateCodec { @FunctionalInterface public interface NbtMigration { - boolean migrate(CompoundNBT compound); + boolean migrate(CompoundTag compound); } private static final Set NAMESPACES = new LinkedHashSet<>(); + private static final Map NAMESPACE_ALIASES = new LinkedHashMap<>(); private static final Map BLOCK_ALIASES = new LinkedHashMap<>(); private static final Map ITEM_ALIASES = new LinkedHashMap<>(); private static final Map BLOCK_CODECS = new LinkedHashMap<>(); @@ -45,6 +46,20 @@ public static synchronized void registerNamespace(String namespace) { NAMESPACES.add(normalize(namespace)); } + /** Only installed mods contribute aliases. Explicit path aliases still take precedence. */ + public static synchronized void registerNamespaceAlias(String legacyNamespace, String currentNamespace) { + String legacy = normalize(legacyNamespace); + String current = normalize(currentNamespace); + registerNamespace(legacy); + registerNamespace(current); + NAMESPACE_ALIASES.put(legacy, current); + } + + public static synchronized ResourceLocation registryTarget(ResourceLocation id) { + String current = NAMESPACE_ALIASES.get(id.getNamespace()); + return current == null ? id : new ResourceLocation(current, id.getPath()); + } + public static synchronized void registerBlockAlias(ResourceLocation legacyId, ResourceLocation currentId) { BLOCK_ALIASES.put(legacyId, currentId); registerNamespace(legacyId.getNamespace()); @@ -65,6 +80,22 @@ public static synchronized void registerTileEntityAlias(String legacyId, String TILE_ALIASES.put(normalizeIdentity(legacyId), normalizeIdentity(currentId)); } + public static synchronized void registerTileEntityAliases(ResourceLocation current) { + String path = current.getPath(); + registerTileVariants(current.getNamespace(), path, current.toString()); + for (Map.Entry alias : NAMESPACE_ALIASES.entrySet()) { + if (alias.getValue().equals(current.getNamespace())) { + registerTileVariants(alias.getKey(), path, current.toString()); + } + } + } + + private static void registerTileVariants(String namespace, String path, String target) { + registerTileEntityAlias(namespace + ":" + path, target); + registerTileEntityAlias(namespace + "." + path, target); + registerTileEntityAlias("minecraft:" + namespace + "." + path, target); + } + public static synchronized void registerEntityAlias(String legacyId, String currentId) { ENTITY_ALIASES.put(normalizeIdentity(legacyId), normalizeIdentity(currentId)); } @@ -92,16 +123,17 @@ public static synchronized boolean handlesNamespace(String namespace) { public static synchronized ResourceLocation blockTarget(ResourceLocation legacyId) { ResourceLocation target = BLOCK_ALIASES.get(legacyId); - return target == null ? legacyId : target; + return target == null ? registryTarget(legacyId) : target; } public static synchronized ResourceLocation itemTarget(ResourceLocation legacyId) { ResourceLocation target = ITEM_ALIASES.get(legacyId); - return target == null ? legacyId : target; + return target == null ? registryTarget(legacyId) : target; } public static synchronized BlockState decodeBlockState(ResourceLocation legacyId, Block block, int metadata) { BlockStateCodec codec = BLOCK_CODECS.get(legacyId); + if (codec == null) codec = BLOCK_CODECS.get(blockTarget(legacyId)); return codec == null ? block.defaultBlockState() : codec.decode(block, metadata & 15); } @@ -129,7 +161,7 @@ public static synchronized boolean isDurableItem(ResourceLocation id) { return DURABLE_ITEMS.contains(id) || DURABLE_ITEMS.contains(itemTarget(id)); } - public static synchronized boolean migrateRegisteredNbt(CompoundNBT compound) { + public static synchronized boolean migrateRegisteredNbt(CompoundTag compound) { boolean changed = false; for (NbtMigration migration : NBT_MIGRATIONS.values()) changed |= migration.migrate(compound); return changed; diff --git a/src/main/java/cyano/poweradvantage/api/migration/LegacyOreSpawnConfigBridge.java b/src/main/java/cyano/poweradvantage/api/migration/LegacyOreSpawnConfigBridge.java index 83e34820..4f080498 100644 --- a/src/main/java/cyano/poweradvantage/api/migration/LegacyOreSpawnConfigBridge.java +++ b/src/main/java/cyano/poweradvantage/api/migration/LegacyOreSpawnConfigBridge.java @@ -59,7 +59,15 @@ private Result(boolean migrated, int rules, Path source, Path target) { * OreSpawn 4 provider-override location. */ public static synchronized Result migrate(Path configDirectory, String modId) throws IOException { + return migrate(configDirectory, modId, modId); + } + + public static synchronized Result migrate(Path configDirectory, String modId, String legacyModId) + throws IOException { Path source = configDirectory.resolve("orespawn").resolve(modId + ".json"); + if (!Files.isRegularFile(source)) { + source = configDirectory.resolve("orespawn").resolve(legacyModId + ".json"); + } Path target = configDirectory.resolve(modId + "-orespawn.json"); if (!Files.isRegularFile(source) || Files.exists(target) || Files.exists(configDirectory.resolve("orespawn-worldgen.json"))) { @@ -84,6 +92,9 @@ public static synchronized Result migrate(Path configDirectory, String modId) th JsonObject oldOre = oreElement.getAsJsonObject(); String output = string(oldOre, "blockID", ""); if (!isRegistryName(output)) continue; + if (output.startsWith(legacyModId + ":")) { + output = modId + output.substring(legacyModId.length()); + } JsonObject ore = convertOre(oldOre, output, dimensionId); if (ore == null) continue; diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/ExternalPowerRequest.java b/src/main/java/cyano/poweradvantage/api/modsupport/ExternalPowerRequest.java index 3ee7f59b..d979ec30 100644 --- a/src/main/java/cyano/poweradvantage/api/modsupport/ExternalPowerRequest.java +++ b/src/main/java/cyano/poweradvantage/api/modsupport/ExternalPowerRequest.java @@ -1,7 +1,7 @@ package cyano.poweradvantage.api.modsupport; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.math.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.BlockPos; import cyano.poweradvantage.api.PowerRequest; /** @@ -11,7 +11,7 @@ */ public class ExternalPowerRequest extends PowerRequest { - public final TileEntity externalPowerAcceptor; + public final BlockEntity externalPowerAcceptor; public final BlockPos pos; /** @@ -20,7 +20,7 @@ public class ExternalPowerRequest extends PowerRequest { * @param requestSize Amount of energy requested * @param source The source TileEntity (which does not extend PoweredEntity) */ - public ExternalPowerRequest(float requestSize, TileEntity source) { + public ExternalPowerRequest(float requestSize, BlockEntity source) { super(0, requestSize, null); externalPowerAcceptor = source; pos = source.getBlockPos(); diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/ILightWeightPowerAcceptor.java b/src/main/java/cyano/poweradvantage/api/modsupport/ILightWeightPowerAcceptor.java index 71c2edf1..3d60e9e9 100644 --- a/src/main/java/cyano/poweradvantage/api/modsupport/ILightWeightPowerAcceptor.java +++ b/src/main/java/cyano/poweradvantage/api/modsupport/ILightWeightPowerAcceptor.java @@ -1,8 +1,8 @@ package cyano.poweradvantage.api.modsupport; import cyano.poweradvantage.api.ConduitType; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; /** * This interface is used in the LightWeightPowerRegistry to allow established mods to optionally @@ -46,7 +46,7 @@ public default boolean canAcceptEnergyType(ConduitType powerType, Direction inpu * @param powerType The type of power being offered * @return How much energy of the provided type your TileEntity is willing to accept. */ - public abstract float getEnergyDemand(TileEntity yourMachine, ConduitType powerType); + public abstract float getEnergyDemand(BlockEntity yourMachine, ConduitType powerType); /** * This method is called when energy it being provided to your TileEntity. The implementation of @@ -58,6 +58,6 @@ public default boolean canAcceptEnergyType(ConduitType powerType, Direction inpu * @param powerType The type of power provided * @return The actual amount of power taken */ - public abstract float addEnergy(TileEntity yourMachine, float amountAdded, ConduitType powerType); + public abstract float addEnergy(BlockEntity yourMachine, float amountAdded, ConduitType powerType); } diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/LightWeightPowerRegistry.java b/src/main/java/cyano/poweradvantage/api/modsupport/LightWeightPowerRegistry.java index aa1bfb10..fa2dd7c3 100644 --- a/src/main/java/cyano/poweradvantage/api/modsupport/LightWeightPowerRegistry.java +++ b/src/main/java/cyano/poweradvantage/api/modsupport/LightWeightPowerRegistry.java @@ -2,11 +2,11 @@ import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.IPowerMachine; -import net.minecraft.block.Block; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.IBlockReader; -import zone.moddev.mc.poweradvantage.PowerAdvantage; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import zone.moddev.mc.basepower.PowerAdvantage; import java.util.HashMap; import java.util.Map; @@ -18,7 +18,7 @@ * considered "external" power systems because their power implementation is not controlled by Power * Advantage.

*

For example, you can add Power Advantage compatibility to an existing mod like this:

-	if(Loader.isModLoaded("poweradvantage")){
+	if(Loader.isModLoaded("basepower")){
 		LightWeightPowerRegistry.registerLightWeightPowerAcceptor(Blocks.myMachine, 
 				new ILightWeightPowerAcceptor(){
 
@@ -109,9 +109,9 @@ public boolean isExternalPowerBlock(Block b) {
 	 * @param energyType The type of energy being offered
 	 * @return How much of the offered energy the target machine would like to receive.
 	 */
-	public float getRequestedPowerAmount(IBlockReader w, BlockPos coord, ConduitType energyType) {
+	public float getRequestedPowerAmount(BlockGetter w, BlockPos coord, ConduitType energyType) {
 		Block b = w.getBlockState(coord).getBlock();
-		TileEntity te = w.getBlockEntity(coord);
+		BlockEntity te = w.getBlockEntity(coord);
 		if (te == null) return 0;
 		if (te instanceof IPowerMachine) {
 			return ((IPowerMachine) te).getPowerRequest(energyType).amount;
@@ -132,9 +132,9 @@ public float getRequestedPowerAmount(IBlockReader w, BlockPos coord, ConduitType
 	 * @return The amount of energy that the target machine actually added (may be less than the
 	 * amount provided)
 	 */
-	public float addPower(IBlockReader w, BlockPos coord, ConduitType energyType, float amount) {
+	public float addPower(BlockGetter w, BlockPos coord, ConduitType energyType, float amount) {
 		Block b = w.getBlockState(coord).getBlock();
-		TileEntity te = w.getBlockEntity(coord);
+		BlockEntity te = w.getBlockEntity(coord);
 		if (te == null) return 0;
 		if (te instanceof IPowerMachine) {
 			return ((IPowerMachine) te).addEnergy(amount, energyType);
diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java
index 402cb963..0841ea89 100644
--- a/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java
+++ b/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java
@@ -3,9 +3,9 @@
 import cyano.poweradvantage.api.ConduitType;
 import cyano.poweradvantage.api.PowerRequest;
 import cyano.poweradvantage.api.simple.TileEntitySimplePowerMachine;
-import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.CompoundNBT;
-import net.minecraft.tileentity.TileEntityType;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.world.level.block.entity.BlockEntityType;
 
 import java.util.Arrays;
 
@@ -28,8 +28,8 @@ public abstract class TileEntityConverter extends TileEntitySimplePowerMachine {
 	final int INDEX_OTHER_ENERGY_UPPER = 2;
 	private final ConduitType type;
 
-	public TileEntityConverter(TileEntityType tileType, ConduitType type, float powerAdvantageEnergyBuffer, String unlocalizedName) {
-		super(tileType, type, powerAdvantageEnergyBuffer, unlocalizedName);
+	public TileEntityConverter(BlockEntityType tileType, net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state, ConduitType type, float powerAdvantageEnergyBuffer, String unlocalizedName) {
+		super(tileType, pos, state, type, powerAdvantageEnergyBuffer, unlocalizedName);
 		halfFull = powerAdvantageEnergyBuffer * 0.5f;
 		this.type = type;
 	}
@@ -174,15 +174,15 @@ public byte getMinimumSinkPriority() {
 	}
 
 	@Override
-	public void load(net.minecraft.block.BlockState state, final CompoundNBT tagRoot) {
-		super.load(state, tagRoot);
+	public void load(final CompoundTag tagRoot) {
+		super.load(tagRoot);
 		if (tagRoot.contains("Other", 99)) {
 			this.setOtherEnergy(tagRoot.getDouble("Other"));
 		}
 	}
 
 	@Override
-	public CompoundNBT save(final CompoundNBT tagRoot) {
+	public CompoundTag save(final CompoundTag tagRoot) {
 		super .save(tagRoot);
 		tagRoot.putDouble("Other", this.getOtherEnergy());
 		return tagRoot;
diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/package-info.java b/src/main/java/cyano/poweradvantage/api/modsupport/package-info.java
index b86dd02a..16d362dc 100644
--- a/src/main/java/cyano/poweradvantage/api/modsupport/package-info.java
+++ b/src/main/java/cyano/poweradvantage/api/modsupport/package-info.java
@@ -1,7 +1,7 @@
 /**
 This package contains the light-weight API for people interested in adding Power Advantage support 
 to existing mods (instead of making a Power Advantage add-on mod). Typically, you achieve this be 
-first checking whether Power Advantage exists with if(Loader.isModLoaded("poweradvantage")){ ... }. 
+first checking whether basePower exists with if(Loader.isModLoaded("basepower")){ ... }.
 Then if you detect that Power Advantage is installed, you can add an ad-hoc power handler for each 
 of your machines like this:
 LightWeightPowerRegistry.registerLightWeightPowerAcceptor(Blocks.myMachine, 
@@ -28,4 +28,4 @@ public float addEnergy(TileEntity yourMachine,
 The above example is for registerring a machine that should accept steam and electricity power from 
 Power Advantage add-on mods.
 */
-package cyano.poweradvantage.api.modsupport;
\ No newline at end of file
+package cyano.poweradvantage.api.modsupport;
diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/rf/BlockRFConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/rf/BlockRFConverter.java
index 25248cb2..9f87dbe4 100644
--- a/src/main/java/cyano/poweradvantage/api/modsupport/rf/BlockRFConverter.java
+++ b/src/main/java/cyano/poweradvantage/api/modsupport/rf/BlockRFConverter.java
@@ -2,11 +2,11 @@
 
 import cyano.poweradvantage.api.ConduitType;
 import cyano.poweradvantage.api.simple.BlockSimplePowerMachine;
-import net.minecraft.block.material.Material;
-import net.minecraft.world.IBlockReader;
-import net.minecraft.block.BlockState;
-import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.World;
+import net.minecraft.world.level.material.Material;
+import net.minecraft.world.level.BlockGetter;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.Level;
 
 /** Historical RF-labelled converter block, backed by Forge Energy in 1.13. */
 public class BlockRFConverter extends BlockSimplePowerMachine {
@@ -19,9 +19,9 @@ public BlockRFConverter(Material material, float hardness, ConduitType type,
     }
 
     @Override
-    public TileEntityRFConverter newBlockEntity(IBlockReader world) {
+    public TileEntityRFConverter newBlockEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) {
         try {
-            return tileEntityClass.newInstance();
+            return tileEntityClass.getConstructor(BlockPos.class, BlockState.class).newInstance(pos, state);
         } catch (ReflectiveOperationException exception) {
             throw new IllegalStateException("Cannot create converter tile " + tileEntityClass.getName(), exception);
         }
@@ -30,5 +30,5 @@ public TileEntityRFConverter newBlockEntity(IBlockReader world) {
     @Override public boolean isPowerSink(ConduitType powerType) { return true; }
     @Override public boolean isPowerSource(ConduitType powerType) { return true; }
     @Override public boolean hasAnalogOutputSignal(BlockState state) { return false; }
-    @Override public int getAnalogOutputSignal(BlockState state, World world, BlockPos pos) { return 0; }
+    @Override public int getAnalogOutputSignal(BlockState state, Level world, BlockPos pos) { return 0; }
 }
diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFConverter.java
index 4e50ff14..d8ec65ac 100644
--- a/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFConverter.java
+++ b/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFConverter.java
@@ -5,14 +5,14 @@
 
 import cyano.poweradvantage.api.ConduitType;
 import cyano.poweradvantage.api.modsupport.TileEntityConverter;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.Direction;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.entity.BlockEntityType;
+import net.minecraft.core.Direction;
 import net.minecraftforge.common.capabilities.Capability;
 import net.minecraftforge.common.util.LazyOptional;
 import net.minecraftforge.energy.CapabilityEnergy;
 import net.minecraftforge.energy.IEnergyStorage;
-import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.basepower.PowerAdvantage;
 
 /** Forge Energy bridge retained behind the historical RF-labelled block IDs. */
 public class TileEntityRFConverter extends TileEntityConverter implements IEnergyStorage {
@@ -21,9 +21,9 @@ public class TileEntityRFConverter extends TileEntityConverter implements IEnerg
     private LazyOptional energyCapability = LazyOptional.of(() -> this);
     private int forgeBuffer;
 
-    protected TileEntityRFConverter(TileEntityType tileType, ConduitType type,
+    protected TileEntityRFConverter(BlockEntityType tileType, net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state, ConduitType type,
             float advantageBuffer, int forgeBufferSize, String translationKey) {
-        super(tileType, type, advantageBuffer, translationKey);
+        super(tileType, pos, state, type, advantageBuffer, translationKey);
         this.forgeBufferSize = forgeBufferSize;
         this.halfBuffer = forgeBufferSize / 2;
     }
@@ -33,7 +33,7 @@ public void tickUpdate(boolean server) {
         if (!server) return;
         int index = (int) (getLevel().getGameTime() % Direction.values().length);
         Direction side = Direction.values()[index];
-        TileEntity neighbor = getLevel().getBlockEntity(getBlockPos().relative(side));
+        BlockEntity neighbor = getLevel().getBlockEntity(getBlockPos().relative(side));
         if (neighbor == null) return;
         neighbor.getCapability(CapabilityEnergy.ENERGY, side.getOpposite()).ifPresent(storage -> {
             int delta = forgeBuffer - halfBuffer;
@@ -69,12 +69,12 @@ public  LazyOptional getCapability(@Nonnull Capability capability, @Nul
         return energyCapability != null && capability == CapabilityEnergy.ENERGY ? energyCapability.cast() : super.getCapability(capability, side);
     }
 
-    @Override protected void invalidateCaps() {
+    @Override public void invalidateCaps() {
         super.invalidateCaps();
         if (energyCapability != null) energyCapability.invalidate();
     }
 
-    @Override protected void reviveCaps() {
+    @Override public void reviveCaps() {
         super.reviveCaps();
         if (energyCapability != null && !energyCapability.isPresent()) energyCapability = LazyOptional.of(() -> this);
     }
diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFElectricityConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFElectricityConverter.java
index 8c57cf57..d23807fd 100644
--- a/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFElectricityConverter.java
+++ b/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFElectricityConverter.java
@@ -1,13 +1,13 @@
 package cyano.poweradvantage.api.modsupport.rf;
 
 import cyano.poweradvantage.api.ConduitType;
-import zone.moddev.mc.poweradvantage.init.Entities;
+import zone.moddev.mc.basepower.init.Entities;
 
 public class TileEntityRFElectricityConverter extends TileEntityRFConverter {
 
-	public TileEntityRFElectricityConverter() {
-		super(Entities.RF_ELECTRICITY_CONVERTER, new ConduitType("electricity"), 8000, 1000,
-				"tile.poweradvantage.converter_rf_electricity.name");
+	public TileEntityRFElectricityConverter(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) {
+		super(Entities.RF_ELECTRICITY_CONVERTER, pos, state, new ConduitType("electricity"), 8000, 1000,
+				"tile.basepower.converter_rf_electricity.name");
 	}
 
 }
diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFQuantumConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFQuantumConverter.java
index 88749517..ad3292fb 100644
--- a/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFQuantumConverter.java
+++ b/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFQuantumConverter.java
@@ -1,13 +1,13 @@
 package cyano.poweradvantage.api.modsupport.rf;
 
 import cyano.poweradvantage.api.ConduitType;
-import zone.moddev.mc.poweradvantage.init.Entities;
+import zone.moddev.mc.basepower.init.Entities;
 
 public class TileEntityRFQuantumConverter extends TileEntityRFConverter {
 
-	public TileEntityRFQuantumConverter() {
-		super(Entities.RF_QUANTUM_CONVERTER, new ConduitType("quantum"), 1000, 1000,
-				"tile.poweradvantage.converter_rf_quantum.name");
+	public TileEntityRFQuantumConverter(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) {
+		super(Entities.RF_QUANTUM_CONVERTER, pos, state, new ConduitType("quantum"), 1000, 1000,
+				"tile.basepower.converter_rf_quantum.name");
 	}
 
 }
diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFSteamConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFSteamConverter.java
index ccd184c8..ecdd0e8d 100644
--- a/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFSteamConverter.java
+++ b/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFSteamConverter.java
@@ -1,13 +1,13 @@
 package cyano.poweradvantage.api.modsupport.rf;
 
 import cyano.poweradvantage.api.ConduitType;
-import zone.moddev.mc.poweradvantage.init.Entities;
+import zone.moddev.mc.basepower.init.Entities;
 
 public class TileEntityRFSteamConverter extends TileEntityRFConverter {
 
-	public TileEntityRFSteamConverter() {
-		super(Entities.RF_STEAM_CONVERTER, new ConduitType("steam"), 1000, 1000,
-				"tile.poweradvantage.converter_rf_steam.name");
+	public TileEntityRFSteamConverter(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) {
+		super(Entities.RF_STEAM_CONVERTER, pos, state, new ConduitType("steam"), 1000, 1000,
+				"tile.basepower.converter_rf_steam.name");
 	}
 
 }
diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/BlockTRConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/BlockTRConverter.java
index 2e89f278..b454a25a 100644
--- a/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/BlockTRConverter.java
+++ b/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/BlockTRConverter.java
@@ -2,7 +2,7 @@
 
 import cyano.poweradvantage.api.ConduitType;
 import cyano.poweradvantage.api.modsupport.rf.BlockRFConverter;
-import net.minecraft.block.material.Material;
+import net.minecraft.world.level.material.Material;
 
 /** Historical Tech Reborn-labelled converter block, backed by Forge Energy. */
 public class BlockTRConverter extends BlockRFConverter {
diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/TileEntityTRConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/TileEntityTRConverter.java
index 0cded265..d2e778f1 100644
--- a/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/TileEntityTRConverter.java
+++ b/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/TileEntityTRConverter.java
@@ -2,16 +2,16 @@
 
 import cyano.poweradvantage.api.ConduitType;
 import cyano.poweradvantage.api.modsupport.rf.TileEntityRFConverter;
-import net.minecraft.tileentity.TileEntityType;
-import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import net.minecraft.world.level.block.entity.BlockEntityType;
+import zone.moddev.mc.basepower.PowerAdvantage;
 
 /**
  * Compatibility base for the historical Tech Reborn-labelled converter.
  * Forge Energy is the common energy API from 1.13 onward.
  */
 public abstract class TileEntityTRConverter extends TileEntityRFConverter {
-    protected TileEntityTRConverter(TileEntityType type, ConduitType conduitType, String translationKey) {
-        super(type, conduitType, 1024F, 2048, translationKey);
+    protected TileEntityTRConverter(BlockEntityType type, net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state, ConduitType conduitType, String translationKey) {
+        super(type, pos, state, conduitType, 1024F, 2048, translationKey);
     }
 
     @Override public boolean canConvertEnergy(ConduitType type) {
diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/TileEntityTRElectricityConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/TileEntityTRElectricityConverter.java
index 8ff86d9a..a6c869ac 100644
--- a/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/TileEntityTRElectricityConverter.java
+++ b/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/TileEntityTRElectricityConverter.java
@@ -1,20 +1,20 @@
 package cyano.poweradvantage.api.modsupport.techreborn;
 
 import cyano.poweradvantage.api.ConduitType;
-import net.minecraft.entity.player.PlayerEntity;
-import zone.moddev.mc.poweradvantage.init.Entities;
+import net.minecraft.world.entity.player.Player;
+import zone.moddev.mc.basepower.init.Entities;
 
 /**
  * Created by Chris on 4/10/2016.
  */
 public class TileEntityTRElectricityConverter extends TileEntityTRConverter {
-	public TileEntityTRElectricityConverter() {
-		super(Entities.TR_ELECTRICITY_CONVERTER, new ConduitType("electricity"),
-				"tile.poweradvantage.converter_tr_electricity.name");
+	public TileEntityTRElectricityConverter(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) {
+		super(Entities.TR_ELECTRICITY_CONVERTER, pos, state, new ConduitType("electricity"),
+				"tile.basepower.converter_tr_electricity.name");
 	}
 	
 	@Override
-	public boolean stillValid(PlayerEntity player) {
+	public boolean stillValid(Player player) {
 		return true;
 	}
 }
diff --git a/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidConduit.java b/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidConduit.java
index dfb92704..3733536b 100644
--- a/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidConduit.java
+++ b/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidConduit.java
@@ -3,20 +3,20 @@
 import cyano.poweradvantage.api.ConduitType;
 import cyano.poweradvantage.api.PowerConnectorContext;
 import cyano.poweradvantage.api.fluid.FluidConduitBlock;
-import zone.moddev.mc.poweradvantage.util.PowerHelper;
-import net.minecraft.block.Block;
-import net.minecraft.block.material.Material;
-import net.minecraft.block.BlockState;
-import net.minecraft.item.BlockItemUseContext;
-import net.minecraft.pathfinding.PathType;
-import net.minecraft.state.BooleanProperty;
-import net.minecraft.state.StateContainer;
-import net.minecraft.util.Direction;
-import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.shapes.VoxelShape;
-import net.minecraft.util.math.shapes.VoxelShapes;
-import net.minecraft.world.IBlockReader;
-import net.minecraft.world.IWorld;
+import zone.moddev.mc.basepower.util.PowerHelper;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.material.Material;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.item.context.BlockPlaceContext;
+import net.minecraft.world.level.pathfinder.PathComputationType;
+import net.minecraft.world.level.block.state.properties.BooleanProperty;
+import net.minecraft.world.level.block.state.StateDefinition;
+import net.minecraft.core.Direction;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.phys.shapes.VoxelShape;
+import net.minecraft.world.phys.shapes.Shapes;
+import net.minecraft.world.level.BlockGetter;
+import net.minecraft.world.level.LevelAccessor;
 
 /**
  * Standard six-way fluid conduit block used by PowerAdvantage and extensions.
@@ -43,22 +43,22 @@ public BlockSimpleFluidConduit(Material material, float hardness, float pipeRadi
     @Override public boolean isPowerSource(ConduitType conduitType) { return false; }
 
     @Override
-    protected void createBlockStateDefinition(StateContainer.Builder builder) {
+    protected void createBlockStateDefinition(StateDefinition.Builder builder) {
         builder.add(WEST, DOWN, SOUTH, EAST, UP, NORTH);
     }
 
     @Override
-    public BlockState getStateForPlacement(BlockItemUseContext context) {
+    public BlockState getStateForPlacement(BlockPlaceContext context) {
         return connectedState(defaultBlockState(), context.getLevel(), context.getClickedPos());
     }
 
     @Override
     public BlockState updateShape(BlockState state, Direction facing, BlockState facingState,
-            IWorld world, BlockPos currentPos, BlockPos facingPos) {
+            LevelAccessor world, BlockPos currentPos, BlockPos facingPos) {
         return state.setValue(property(facing), canConnectTo(world, currentPos, state, facing));
     }
 
-    private BlockState connectedState(BlockState state, IBlockReader world, BlockPos pos) {
+    private BlockState connectedState(BlockState state, BlockGetter world, BlockPos pos) {
         return state.setValue(WEST, canConnectTo(world, pos, state, Direction.WEST))
                 .setValue(DOWN, canConnectTo(world, pos, state, Direction.DOWN))
                 .setValue(SOUTH, canConnectTo(world, pos, state, Direction.SOUTH))
@@ -79,7 +79,7 @@ private static BooleanProperty property(Direction facing) {
         }
     }
 
-    protected boolean canConnectTo(IBlockReader world, BlockPos pos, BlockState state,
+    protected boolean canConnectTo(BlockGetter world, BlockPos pos, BlockState state,
             Direction face) {
         for (ConduitType type : getTypes()) {
             if (PowerHelper.areConnectable(new PowerConnectorContext(type, world, state, pos, face))) {
@@ -90,7 +90,7 @@ protected boolean canConnectTo(IBlockReader world, BlockPos pos, BlockState stat
     }
 
     @Override
-    public VoxelShape getShape(BlockState state, IBlockReader world, BlockPos pos, net.minecraft.util.math.shapes.ISelectionContext context) {
+    public VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, net.minecraft.world.phys.shapes.CollisionContext context) {
         double min = 8.0D - pipeRadius * 16.0D;
         double max = 8.0D + pipeRadius * 16.0D;
         VoxelShape shape = Block.box(min, min, min, max, max, max);
@@ -99,23 +99,23 @@ public VoxelShape getShape(BlockState state, IBlockReader world, BlockPos pos, n
             if (!state.getValue(property(facing))) continue;
             connected = true;
             switch (facing) {
-                case DOWN: shape = VoxelShapes.or(shape, Block.box(min, 0, min, max, min, max)); break;
-                case UP: shape = VoxelShapes.or(shape, Block.box(min, max, min, max, 16, max)); break;
-                case NORTH: shape = VoxelShapes.or(shape, Block.box(min, min, 0, max, max, min)); break;
-                case SOUTH: shape = VoxelShapes.or(shape, Block.box(min, min, max, max, max, 16)); break;
-                case WEST: shape = VoxelShapes.or(shape, Block.box(0, min, min, min, max, max)); break;
-                case EAST: shape = VoxelShapes.or(shape, Block.box(max, min, min, 16, max, max)); break;
+                case DOWN: shape = Shapes.or(shape, Block.box(min, 0, min, max, min, max)); break;
+                case UP: shape = Shapes.or(shape, Block.box(min, max, min, max, 16, max)); break;
+                case NORTH: shape = Shapes.or(shape, Block.box(min, min, 0, max, max, min)); break;
+                case SOUTH: shape = Shapes.or(shape, Block.box(min, min, max, max, max, 16)); break;
+                case WEST: shape = Shapes.or(shape, Block.box(0, min, min, min, max, max)); break;
+                case EAST: shape = Shapes.or(shape, Block.box(max, min, min, 16, max, max)); break;
             }
         }
         if (!connected) {
-            shape = VoxelShapes.or(shape, Block.box(0, min, min, 16, max, max));
-            shape = VoxelShapes.or(shape, Block.box(min, min, 0, max, max, 16));
+            shape = Shapes.or(shape, Block.box(0, min, min, 16, max, max));
+            shape = Shapes.or(shape, Block.box(min, min, 0, max, max, 16));
         }
         return shape;
     }
 
     public boolean isFullCube(BlockState state) { return false; }
-    @Override public boolean isPathfindable(BlockState state, IBlockReader world, BlockPos pos, PathType type) { return false; }
+    @Override public boolean isPathfindable(BlockState state, BlockGetter world, BlockPos pos, PathComputationType type) { return false; }
 
     /** Legacy metadata bridge retained for migration tests. */
     public int getMetaFromState(BlockState state) { return 0; }
diff --git a/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidMachine.java b/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidMachine.java
index c704d81b..481d29dd 100644
--- a/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidMachine.java
+++ b/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidMachine.java
@@ -1,14 +1,14 @@
 package cyano.poweradvantage.api.simple;
 
-import zone.moddev.mc.poweradvantage.init.Fluids;
-import net.minecraft.block.material.Material;
+import zone.moddev.mc.basepower.init.Fluids;
+import net.minecraft.world.level.material.Material;
 
 /**
  * This block class provides all of the standard code for creating a machine 
  * block with an inventory and user interface that gives fluids to connected 
  * fluid consumers.
* Example usage:
-int guiID = zone.moddev.mc.poweradvantage.registry.MachineGUIRegistry.addGUI(new MySimpleMachineGUI());
+int guiID = zone.moddev.mc.basepower.registry.MachineGUIRegistry.addGUI(new MySimpleMachineGUI());
 Block myMachineBlock = new MyBlockSimpleFluidSource(guiID,PowerAdvantage.getInstance());
 myMachineBlock.setTranslationKey(MODID+"."+"my_machine");
 myMachineBlock.setRegistryName(MODID, "my_machine");
@@ -31,4 +31,8 @@ public BlockSimpleFluidMachine(Material blockMaterial, float hardness) {
 		super(blockMaterial, hardness, Fluids.fluidConduit_general);
 	}
 
+    protected BlockSimpleFluidMachine(net.minecraft.world.level.block.Block.Properties properties) {
+        super(properties, Fluids.fluidConduit_general);
+    }
+
 }
diff --git a/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerConduit.java b/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerConduit.java
index 1e87d065..5c918975 100644
--- a/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerConduit.java
+++ b/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerConduit.java
@@ -3,20 +3,20 @@
 import cyano.poweradvantage.api.ConduitBlock;
 import cyano.poweradvantage.api.ConduitType;
 import cyano.poweradvantage.api.PowerConnectorContext;
-import zone.moddev.mc.poweradvantage.util.PowerHelper;
-import net.minecraft.block.Block;
-import net.minecraft.block.material.Material;
-import net.minecraft.block.BlockState;
-import net.minecraft.item.BlockItemUseContext;
-import net.minecraft.pathfinding.PathType;
-import net.minecraft.state.BooleanProperty;
-import net.minecraft.state.StateContainer;
-import net.minecraft.util.Direction;
-import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.shapes.VoxelShape;
-import net.minecraft.util.math.shapes.VoxelShapes;
-import net.minecraft.world.IBlockReader;
-import net.minecraft.world.IWorld;
+import zone.moddev.mc.basepower.util.PowerHelper;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.material.Material;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.item.context.BlockPlaceContext;
+import net.minecraft.world.level.pathfinder.PathComputationType;
+import net.minecraft.world.level.block.state.properties.BooleanProperty;
+import net.minecraft.world.level.block.state.StateDefinition;
+import net.minecraft.core.Direction;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.phys.shapes.VoxelShape;
+import net.minecraft.world.phys.shapes.Shapes;
+import net.minecraft.world.level.BlockGetter;
+import net.minecraft.world.level.LevelAccessor;
 
 public abstract class BlockSimplePowerConduit extends ConduitBlock {
     public static final BooleanProperty SOUTH = BooleanProperty.create("south");
@@ -58,22 +58,22 @@ public boolean canAcceptConnection(PowerConnectorContext connection) {
     @Override public boolean isPowerSource(ConduitType conduitType) { return false; }
 
     @Override
-    protected void createBlockStateDefinition(StateContainer.Builder builder) {
+    protected void createBlockStateDefinition(StateDefinition.Builder builder) {
         builder.add(WEST, DOWN, SOUTH, EAST, UP, NORTH);
     }
 
     @Override
-    public BlockState getStateForPlacement(BlockItemUseContext context) {
+    public BlockState getStateForPlacement(BlockPlaceContext context) {
         return connectedState(defaultBlockState(), context.getLevel(), context.getClickedPos());
     }
 
     @Override
     public BlockState updateShape(BlockState state, Direction facing, BlockState facingState,
-            IWorld world, BlockPos currentPos, BlockPos facingPos) {
+            LevelAccessor world, BlockPos currentPos, BlockPos facingPos) {
         return state.setValue(property(facing), canConnectTo(world, currentPos, state, facing, facingPos));
     }
 
-    private BlockState connectedState(BlockState state, IBlockReader world, BlockPos pos) {
+    private BlockState connectedState(BlockState state, BlockGetter world, BlockPos pos) {
         return state.setValue(WEST, canConnectTo(world, pos, state, Direction.WEST, pos.west()))
                 .setValue(DOWN, canConnectTo(world, pos, state, Direction.DOWN, pos.below()))
                 .setValue(SOUTH, canConnectTo(world, pos, state, Direction.SOUTH, pos.south()))
@@ -94,7 +94,7 @@ private static BooleanProperty property(Direction facing) {
         }
     }
 
-    protected boolean canConnectTo(IBlockReader world, BlockPos thisBlock, BlockState state,
+    protected boolean canConnectTo(BlockGetter world, BlockPos thisBlock, BlockState state,
             Direction face, BlockPos otherBlock) {
         for (ConduitType type : types) {
             if (PowerHelper.areConnectable(new PowerConnectorContext(type, world, state, thisBlock, face))) {
@@ -105,7 +105,7 @@ protected boolean canConnectTo(IBlockReader world, BlockPos thisBlock, BlockStat
     }
 
     @Override
-    public VoxelShape getShape(BlockState state, IBlockReader world, BlockPos pos, net.minecraft.util.math.shapes.ISelectionContext context) {
+    public VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, net.minecraft.world.phys.shapes.CollisionContext context) {
         double min = 8.0D - pipeRadius * 16.0D;
         double max = 8.0D + pipeRadius * 16.0D;
         VoxelShape shape = Block.box(min, min, min, max, max, max);
@@ -114,23 +114,23 @@ public VoxelShape getShape(BlockState state, IBlockReader world, BlockPos pos, n
             if (!state.getValue(property(facing))) continue;
             connected = true;
             switch (facing) {
-                case DOWN: shape = VoxelShapes.or(shape, Block.box(min, 0, min, max, min, max)); break;
-                case UP: shape = VoxelShapes.or(shape, Block.box(min, max, min, max, 16, max)); break;
-                case NORTH: shape = VoxelShapes.or(shape, Block.box(min, min, 0, max, max, min)); break;
-                case SOUTH: shape = VoxelShapes.or(shape, Block.box(min, min, max, max, max, 16)); break;
-                case WEST: shape = VoxelShapes.or(shape, Block.box(0, min, min, min, max, max)); break;
-                case EAST: shape = VoxelShapes.or(shape, Block.box(max, min, min, 16, max, max)); break;
+                case DOWN: shape = Shapes.or(shape, Block.box(min, 0, min, max, min, max)); break;
+                case UP: shape = Shapes.or(shape, Block.box(min, max, min, max, 16, max)); break;
+                case NORTH: shape = Shapes.or(shape, Block.box(min, min, 0, max, max, min)); break;
+                case SOUTH: shape = Shapes.or(shape, Block.box(min, min, max, max, max, 16)); break;
+                case WEST: shape = Shapes.or(shape, Block.box(0, min, min, min, max, max)); break;
+                case EAST: shape = Shapes.or(shape, Block.box(max, min, min, 16, max, max)); break;
             }
         }
         if (!connected) {
-            shape = VoxelShapes.or(shape, Block.box(0, min, min, 16, max, max));
-            shape = VoxelShapes.or(shape, Block.box(min, min, 0, max, max, 16));
+            shape = Shapes.or(shape, Block.box(0, min, min, 16, max, max));
+            shape = Shapes.or(shape, Block.box(min, min, 0, max, max, 16));
         }
         return shape;
     }
 
     public boolean isFullCube(BlockState state) { return false; }
-    @Override public boolean isPathfindable(BlockState state, IBlockReader world, BlockPos pos, PathType type) { return false; }
+    @Override public boolean isPathfindable(BlockState state, BlockGetter world, BlockPos pos, PathComputationType type) { return false; }
 
     /** Legacy metadata bridge retained for migration tests. */
     public int getMetaFromState(BlockState state) { return 0; }
diff --git a/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerMachine.java b/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerMachine.java
index aa676cc0..e82fe5ee 100644
--- a/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerMachine.java
+++ b/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerMachine.java
@@ -1,28 +1,28 @@
 package cyano.poweradvantage.api.simple;
 
 import cyano.poweradvantage.api.*;
-import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry;
-import net.minecraft.block.HorizontalBlock;
-import net.minecraft.block.material.Material;
-import net.minecraft.state.Property;
-import net.minecraft.state.DirectionProperty;
-import net.minecraft.state.StateContainer;
-import net.minecraft.block.BlockState;
-import net.minecraft.entity.LivingEntity;
-import net.minecraft.inventory.IInventory;
-import net.minecraft.inventory.InventoryHelper;
-import net.minecraft.item.Item;
-import net.minecraft.item.BlockItemUseContext;
-import net.minecraft.item.ItemStack;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.Direction;
-import net.minecraft.util.Mirror;
-import net.minecraft.util.Rotation;
-import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.Explosion;
-import net.minecraft.world.World;
-import net.minecraft.world.IBlockReader;
-import net.minecraft.block.Block;
+import zone.moddev.mc.basepower.conduitnetwork.ConduitRegistry;
+import net.minecraft.world.level.block.HorizontalDirectionalBlock;
+import net.minecraft.world.level.material.Material;
+import net.minecraft.world.level.block.state.properties.Property;
+import net.minecraft.world.level.block.state.properties.DirectionProperty;
+import net.minecraft.world.level.block.state.StateDefinition;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.Container;
+import net.minecraft.world.Containers;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.context.BlockPlaceContext;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.core.Direction;
+import net.minecraft.world.level.block.Mirror;
+import net.minecraft.world.level.block.Rotation;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.Explosion;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.BlockGetter;
+import net.minecraft.world.level.block.Block;
 
 import java.util.Random;
 
@@ -31,7 +31,7 @@
  * block with an inventory and user interface that gives power to connected 
  * machines.
* Example usage:
-int guiID = zone.moddev.mc.poweradvantage.registry.MachineGUIRegistry.addGUI(new MySimpleMachineGUI());
+int guiID = zone.moddev.mc.basepower.registry.MachineGUIRegistry.addGUI(new MySimpleMachineGUI());
 Block myMachineBlock = new MyBlockSimplePowerSource(guiID,PowerAdvantage.getInstance());
 myMachineBlock.setTranslationKey(MODID+"."+"my_machine");
 myMachineBlock.setRegistryName(MODID, "my_machine");
@@ -46,7 +46,7 @@ public abstract class BlockSimplePowerMachine extends GUIBlock implements ITyped
 	/**
 	 * Blockstate property indicating which direction the block is facing
 	 */
-    public static final DirectionProperty FACING = HorizontalBlock.FACING;
+    public static final DirectionProperty FACING = HorizontalDirectionalBlock.FACING;
     /**
      * Standard constructor for a machine block. Remember to set the GUI ID so that a GUI can pop-up 
      * when the player interacts with this block.
@@ -81,13 +81,13 @@ protected BlockSimplePowerMachine(Block.Properties properties, ConduitType... en
 	 * TileEntitySimplePowerMachine.
 	 */
 	@Override
-    public abstract PoweredEntity newBlockEntity(final IBlockReader world);
+    public abstract PoweredEntity newBlockEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state);
 	
 	/**
 	 * Override of default block behavior
 	 */
 	@Override
-	public void onPlace(final BlockState state, final World world, final BlockPos coord,
+	public void onPlace(final BlockState state, final Level world, final BlockPos coord,
 			final BlockState oldState, final boolean isMoving) {
 		super.onPlace(state, world, coord, oldState, isMoving);
 		this.setDefaultFacing(world, coord, state);
@@ -97,7 +97,7 @@ public void onPlace(final BlockState state, final World world, final BlockPos co
 	 * This method is called when the block is removed from the world by an entity.
 	 */
 	@Override
-	public void onRemove(BlockState state, World w, BlockPos coord, BlockState newState,
+	public void onRemove(BlockState state, Level w, BlockPos coord, BlockState newState,
 			boolean isMoving) {
 		if (state.getBlock() != newState.getBlock()) {
 			ConduitRegistry.getInstance().conduitBlockRemovedEvent(w, coord, getTypes());
@@ -108,7 +108,7 @@ public void onRemove(BlockState state, World w, BlockPos coord, BlockState newSt
 	 * This method is called when the block is destroyed by an explosion.
 	 */
 	@Override
-	public void wasExploded(World w, BlockPos coord, Explosion boom){
+	public void wasExploded(Level w, BlockPos coord, Explosion boom){
 		super.wasExploded(w, coord, boom);
 		ConduitRegistry.getInstance().conduitBlockRemovedEvent(w, coord, getTypes());
 	}
@@ -144,7 +144,7 @@ public boolean canAcceptConnection(PowerConnectorContext connection){
 	 * Creates the blockstate of this block when it is placed in the world
 	 */
     @Override
-    public BlockState getStateForPlacement(final BlockItemUseContext context) {
+    public BlockState getStateForPlacement(final BlockPlaceContext context) {
         return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
     }
     
@@ -152,11 +152,11 @@ public BlockState getStateForPlacement(final BlockItemUseContext context) {
 	 * Creates the blockstate of this block when it is placed in the world
 	 */
     @Override
-    public void setPlacedBy(final World world, final BlockPos coord, final BlockState bs,
+    public void setPlacedBy(final Level world, final BlockPos coord, final BlockState bs,
             final LivingEntity placer, final ItemStack srcItemStack) {
         world.setBlock(coord, bs.setValue((Property) FACING, (Comparable)placer.getDirection().getOpposite()), 2);
         if (srcItemStack.hasCustomHoverName()) {
-            final TileEntity tileEntity = world.getBlockEntity(coord);
+            final BlockEntity tileEntity = world.getBlockEntity(coord);
         	if (tileEntity instanceof PoweredEntity){
 				((PoweredEntity)tileEntity).setCustomInventoryName(srcItemStack.getDisplayName().getString());
         	}
@@ -173,7 +173,7 @@ public void setPlacedBy(final World world, final BlockPos coord, final BlockStat
      * @param coord Block coordinate
      * @param state Block state
      */
-    protected void setDefaultFacing(final World w, final BlockPos coord, final BlockState state) {
+    protected void setDefaultFacing(final Level w, final BlockPos coord, final BlockState state) {
         if (w.isClientSide) {
             return;
         }
@@ -234,7 +234,7 @@ else if (enumFacing == Direction.EAST && block4.isRedstoneConductor(w, coord.eas
      * @return a number from 0 to 15
      */
     @Override
-    public  abstract  int getAnalogOutputSignal(BlockState state, final World world, final BlockPos coord);
+    public  abstract  int getAnalogOutputSignal(BlockState state, final Level world, final BlockPos coord);
     
 
     /**
@@ -259,7 +259,7 @@ public int getMetaFromState(final BlockState bs) {
      * Creates a blockstate
      */
     @Override
-    protected void createBlockStateDefinition(StateContainer.Builder builder) {
+    protected void createBlockStateDefinition(StateDefinition.Builder builder) {
 		builder.add(FACING);
     }
 
diff --git a/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java b/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java
index af2250e4..bd550c4f 100644
--- a/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java
+++ b/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java
@@ -1,24 +1,24 @@
 package cyano.poweradvantage.api.simple;
 
 
-import zone.moddev.mc.poweradvantage.PowerAdvantage;
-import zone.moddev.mc.poweradvantage.math.Integer2D;
-import zone.moddev.mc.poweradvantage.registry.ITileEntityGUI;
-import net.minecraft.client.gui.screen.inventory.ContainerScreen;
-import net.minecraft.entity.player.PlayerEntity;
-import net.minecraft.entity.player.PlayerInventory;
-import net.minecraft.inventory.IInventory;
-import net.minecraft.inventory.container.Slot;
-import net.minecraft.item.ItemStack;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.ResourceLocation;
+import zone.moddev.mc.basepower.PowerAdvantage;
+import zone.moddev.mc.basepower.math.Integer2D;
+import zone.moddev.mc.basepower.registry.ITileEntityGUI;
+import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.entity.player.Inventory;
+
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.resources.ResourceLocation;
 import net.minecraftforge.api.distmarker.Dist;
 import net.minecraftforge.api.distmarker.OnlyIn;
-import com.mojang.blaze3d.matrix.MatrixStack;
+import com.mojang.blaze3d.vertex.PoseStack;
 import com.mojang.blaze3d.systems.RenderSystem;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.StringTextComponent;
-import zone.moddev.mc.poweradvantage.registry.MachineGUIRegistry;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.TextComponent;
+import zone.moddev.mc.basepower.registry.MachineGUIRegistry;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -126,16 +126,16 @@ public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int
 	 * @return An instance of net.minecraft.inventory.container.Container
 	 */
 	@Override
-	public net.minecraft.inventory.container.Container getContainer(TileEntity e, PlayerEntity player) {
-		return new Container(player.inventory,(IInventory)e);
+	public net.minecraft.world.inventory.AbstractContainerMenu getContainer(BlockEntity e, Player player) {
+		return new Container(player.getInventory(),(net.minecraft.world.Container)e);
 	}
 
-    public Container createMenu(int windowId, PlayerInventory player, IInventory inventory) {
+    public Container createMenu(int windowId, Inventory player, net.minecraft.world.Container inventory) {
         return new Container(windowId, player, inventory);
     }
 
     /** @deprecated Use the native menu factory name. */
-    @Deprecated public Container createContainer(int windowId, PlayerInventory player, IInventory inventory) {
+    @Deprecated public Container createContainer(int windowId, Inventory player, net.minecraft.world.Container inventory) {
         return createMenu(windowId, player, inventory);
     }
 
@@ -148,8 +148,8 @@ public Container createMenu(int windowId, PlayerInventory player, IInventory inv
 	 */
 	@Override
 	@OnlyIn(Dist.CLIENT)
-	public ContainerScreen getContainerGUI(TileEntity e, PlayerEntity player) {
-		return new GUIContainer(player.inventory,(IInventory)e);
+	public AbstractContainerScreen getContainerGUI(BlockEntity e, Player player) {
+		return new GUIContainer(player.getInventory(),(net.minecraft.world.Container)e);
 	}
 	
 	
@@ -158,14 +158,14 @@ public ContainerScreen getContainerGUI(TileEntity e, PlayerEntity player) {
 	 * net.minecraft.inventory.container.Container to fulfill the requirements of the
 	 * interface ITileEntityGUI.
 	 */
-	public class Container extends net.minecraft.inventory.container.Container{
+	public class Container extends net.minecraft.world.inventory.AbstractContainerMenu{
 
-		private final IInventory entity;
-		public Container(PlayerInventory playerItems, IInventory entity){
+		private final net.minecraft.world.Container entity;
+		public Container(Inventory playerItems, net.minecraft.world.Container entity){
             this(0, playerItems, entity);
         }
 
-        public Container(int windowId, PlayerInventory playerItems, IInventory entity) {
+        public Container(int windowId, Inventory playerItems, net.minecraft.world.Container entity) {
             super(MachineGUIRegistry.TYPE, windowId);
 			this.entity = entity;
 			int index = 0;
@@ -182,16 +182,16 @@ public Container(int windowId, PlayerInventory playerItems, IInventory entity) {
 		}
 
         @OnlyIn(Dist.CLIENT)
-        public GUIContainer createScreen(PlayerInventory inventory, ITextComponent title) {
+        public GUIContainer createScreen(Inventory inventory, Component title) {
             return new GUIContainer(this, inventory, title);
         }
 		
 		@Override
-		public boolean stillValid(PlayerEntity entityplayer) {
+		public boolean stillValid(Player entityplayer) {
 			return entity.stillValid(entityplayer);
 		}
 		
-		protected void bindPlayerInventory(PlayerInventory inventoryPlayer, int yOffset) {
+		protected void bindPlayerInventory(Inventory inventoryPlayer, int yOffset) {
 			for (int i = 0; i < 3; i++) {
 				for (int j = 0; j < 9; j++) {
 					addSlot(new Slot(inventoryPlayer, j + i * 9 + 9, 8 + j * 18, yOffset + i * 18));
@@ -203,7 +203,7 @@ protected void bindPlayerInventory(PlayerInventory inventoryPlayer, int yOffset)
 		}
 		
 		@Override
-		public ItemStack quickMoveStack(PlayerEntity player, int slot) {
+		public ItemStack quickMoveStack(Player player, int slot) {
 			int hostSize = inventorySlotCoordinates.length;
 			ItemStack stack = ItemStack.EMPTY;
 			Slot slotObject = (Slot) slots.get(slot);
@@ -246,17 +246,19 @@ else if (!this.moveItemStackTo(stackInSlot, 0, hostSize, false)) {
 	 */
 
 	@OnlyIn(Dist.CLIENT)
-	public class GUIContainer extends net.minecraft.client.gui.screen.inventory.ContainerScreen{
+	public class GUIContainer extends net.minecraft.client.gui.screens.inventory.AbstractContainerScreen{
 		private final Object entity;
-		private MatrixStack matrices = new MatrixStack();
+		private final Component playerInventoryLabel;
+		private PoseStack matrices = new PoseStack();
 
         public net.minecraft.client.Minecraft getMinecraft() { return minecraft; }
-		public GUIContainer(PlayerInventory playerItems, IInventory entity) {
-            this(new Container(playerItems, entity), playerItems, new StringTextComponent("Machine"));
+		public GUIContainer(Inventory playerItems, net.minecraft.world.Container entity) {
+            this(new Container(playerItems, entity), playerItems, new TextComponent("Machine"));
         }
 
-        public GUIContainer(Container container, PlayerInventory playerItems, ITextComponent title) {
+        public GUIContainer(Container container, Inventory playerItems, Component title) {
             super(container, playerItems, title);
+            this.playerInventoryLabel = playerItems.getDisplayName();
 //System.out.println("Size: "+this.xSize+"x"+this.ySize+" at coordinate ("+this.guiLeft+","+this.guiTop+")");
 			this.imageWidth = guiWidth;
 			this.imageHeight = guiHeight;
@@ -264,7 +266,7 @@ public GUIContainer(Container container, PlayerInventory playerItems, ITextCompo
 		}
 
 		@Override
-		public void render(MatrixStack matrices, int mouseX, int mouseY, float partialTicks) {
+		public void render(PoseStack matrices, int mouseX, int mouseY, float partialTicks) {
 			this.renderBackground(matrices);
 			super.render(matrices, mouseX, mouseY, partialTicks);
 			this.renderTooltip(matrices, mouseX, mouseY);
@@ -275,10 +277,10 @@ public void render(MatrixStack matrices, int mouseX, int mouseY, float partialTi
 		}
 
 		@Override
-		protected void renderLabels(MatrixStack matrices, int mouseX, int mouseY) {
+		protected void renderLabels(PoseStack matrices, int mouseX, int mouseY) {
 			String machineName = fitLabel(title.getString(), width - 8);
 			font.drawShadow(matrices, machineName, (imageWidth - font.width(machineName)) / 2, 1, 0xFFFFFF);
-			font.drawShadow(matrices, fitLabel(inventory.getDisplayName().getString(), imageWidth - 16),
+			font.drawShadow(matrices, fitLabel(playerInventoryLabel.getString(), imageWidth - 16),
 					8, MACHINE_TOP + MACHINE_HEIGHT, 0xFFFFFF);
 		}
 
@@ -288,18 +290,18 @@ private String fitLabel(String text, int availableWidth) {
 		}
 
 		@Override
-		protected void renderBg(MatrixStack matrices, float f, int i, int j) {
+		protected void renderBg(PoseStack matrices, float f, int i, int j) {
 			this.matrices = matrices;
 			RenderSystem.enableBlend();
 			RenderSystem.defaultBlendFunc();
-			RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
-			this.minecraft.getTextureManager().bind(playerInventoryBox);
+			RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
+			com.mojang.blaze3d.systems.RenderSystem.setShaderTexture(0, playerInventoryBox);
 			int x = (width - imageWidth) / 2;
 			int y = (height - imageHeight) / 2;
 			final int playerInventoryWidth = 176;
 			final int playerInventoryHeight = 90;
 			this.blit(x, y + PLAYER_PANEL_TOP, 0, 132, playerInventoryWidth, playerInventoryHeight);
-			this.minecraft.getTextureManager().bind(guiDisplayImage);
+			com.mojang.blaze3d.systems.RenderSystem.setShaderTexture(0, guiDisplayImage);
 			this.blit(x, y + MACHINE_TOP, 0, 0, guiWidth, MACHINE_HEIGHT);
 			drawGUIDecorations(entity, this, x, y + MACHINE_TOP, this.getBlitOffset());
 			RenderSystem.disableBlend();
@@ -310,7 +312,7 @@ public void blit(int x, int y, int u, int v, int width, int height) {
 			super.blit(matrices, x, y, u, v, width, height);
 		}
 
-		public MatrixStack getMatrices() { return matrices; }
+		public PoseStack getMatrices() { return matrices; }
 		/**
 		 * Gets the z-coordinate of this GUI element
 		 * @return screen depth coordinate
@@ -356,8 +358,8 @@ public int getTop(){
 	 * inventory slot
 	 */
 	public static class SlotContext {
-		public final PlayerInventory playerInventory;
-		public final IInventory machineInventory;
+		public final Inventory playerInventory;
+		public final net.minecraft.world.Container machineInventory;
 		public final int slotIndex;
 		public final int screenPositionX;
 		public final int screenPositionY;
@@ -372,8 +374,8 @@ public static class SlotContext {
 		 * @param screenPositionY  coordinate position on screen
 		 */
 		public SlotContext(
-				final PlayerInventory playerInventory,
-				final IInventory machineInventory,
+				final Inventory playerInventory,
+				final net.minecraft.world.Container machineInventory,
 				final int slotIndex,
 				final int screenPositionX,
 				final int screenPositionY
diff --git a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java
index acabbe0c..51d7ed96 100644
--- a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java
+++ b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java
@@ -5,21 +5,23 @@
 import cyano.poweradvantage.api.PowerRequest;
 import cyano.poweradvantage.api.fluid.FluidPoweredEntity;
 import cyano.poweradvantage.api.fluid.FluidRequest;
-import zone.moddev.mc.poweradvantage.init.Fluids;
-import zone.moddev.mc.poweradvantage.util.FluidIdHelper;
-import net.minecraft.entity.player.PlayerEntity;
-import net.minecraft.inventory.ISidedInventory;
-import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.CompoundNBT;
-import net.minecraft.nbt.ListNBT;
-import net.minecraft.network.NetworkManager;
-import net.minecraft.network.play.server.SUpdateTileEntityPacket;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.Direction;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.StringTextComponent;
-import net.minecraft.util.text.TranslationTextComponent;
-import net.minecraft.fluid.Fluid;
+import zone.moddev.mc.basepower.init.Fluids;
+import zone.moddev.mc.basepower.util.FluidIdHelper;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.WorldlyContainer;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.ListTag;
+import net.minecraft.network.Connection;
+import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
+import net.minecraft.world.level.block.entity.BlockEntityType;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.core.Direction;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.TextComponent;
+import net.minecraft.network.chat.TranslatableComponent;
+import net.minecraft.world.level.material.Fluid;
 import cyano.poweradvantage.api.fluid.FluidStack;
 import cyano.poweradvantage.api.fluid.FluidTank;
 
@@ -37,7 +39,7 @@
  *
  */
 @SuppressWarnings("deprecation")
-public abstract class TileEntitySimpleFluidMachine extends FluidPoweredEntity implements ISidedInventory, net.minecraft.util.INameable {
+public abstract class TileEntitySimpleFluidMachine extends FluidPoweredEntity implements WorldlyContainer, net.minecraft.world.Nameable {
 	/**
 	 * This FluidTank holds the fluids for this machine
 	 */
@@ -52,15 +54,15 @@ public abstract class TileEntitySimpleFluidMachine extends FluidPoweredEntity im
     
 	
 	/**
-	 * Main constructor for this abstract class. Your implementing class must have a no-argument 
-	 * constructor or else Minecraft will crash when it tries to load your entity from a saved 
-	 * game file.
+	 * Native block-entity factories supply the real position and block state when
+	 * creating a machine, including when it is restored from a saved chunk.
 	 * @param fluidTankCapacity How much fluid can be stored in this entity
 	 * @param unlocalizedName The default name for this TileEntity, should the player interact with 
 	 * the TileEntity in a way that gets the TileEntity's name (if in doubt, use the class name).
 	 */
-	public TileEntitySimpleFluidMachine(TileEntityType tileType, int fluidTankCapacity, String unlocalizedName){
-		super(tileType);
+	public TileEntitySimpleFluidMachine(BlockEntityType tileType, BlockPos pos, BlockState state,
+            int fluidTankCapacity, String unlocalizedName){
+		super(tileType, pos, state);
 		this.tank = new DirtyFluidTank(fluidTankCapacity);
 		this.unlocalizedName = unlocalizedName;
 	}
@@ -147,7 +149,7 @@ public void onDataFieldUpdate() {
 		int fluidID = dataFields[DATAFIELD_FLUID_ID];
 		int fluidVolume = dataFields[DATAFIELD_FLUID_VOLUME];
 		if(fluidVolume <= 0){
-			getTank().setFluid(new FluidStack(net.minecraft.fluid.Fluids.WATER,0));
+			getTank().setFluid(new FluidStack(net.minecraft.world.level.material.Fluids.WATER,0));
 		} else {
 			Fluid fluid = FluidIdHelper.getFluid(fluidID);
 			if (fluid != null) {
@@ -178,7 +180,7 @@ public void onDataFieldUpdate() {
      */
 	public void prepareDataFieldsForSync(){
 		if(getTank().getFluid() == null || getTank().getFluidAmount() <= 0){
-			dataFields[DATAFIELD_FLUID_ID] = FluidIdHelper.getFluidId(net.minecraft.fluid.Fluids.WATER);
+			dataFields[DATAFIELD_FLUID_ID] = FluidIdHelper.getFluidId(net.minecraft.world.level.material.Fluids.WATER);
 			dataFields[DATAFIELD_FLUID_VOLUME] = 0;
 		} else {
 			dataFields[DATAFIELD_FLUID_ID] = FluidIdHelper.getFluidId(getTank().getFluid().getFluid());
@@ -263,16 +265,16 @@ protected int transmitFluidToConsumers(FluidStack available, byte minumimPriorit
      * @param tagRoot The root of the NBT
      */
     @Override
-    public void load(net.minecraft.block.BlockState state, final CompoundNBT tagRoot) {
-        super.load(state, tagRoot);
+    public void load(final CompoundTag tagRoot) {
+        super.load(tagRoot);
         ItemStack[] inventory = this.getInventory();
         if(inventory != null ){
 			for(int i = 0; i < inventory.length; i++){
 				inventory[i] = null;
 			}
-	        final ListNBT nbttaglist = tagRoot.getList("Items", 10);
+	        final ListTag nbttaglist = tagRoot.getList("Items", 10);
 	        for (int i = 0; i < nbttaglist.size() && i < inventory.length; ++i) {
-	            final CompoundNBT nbttagcompound1 = nbttaglist .getCompound(i);
+	            final CompoundTag nbttagcompound1 = nbttaglist .getCompound(i);
 	            final byte n = nbttagcompound1.getByte("Slot");
 	            if (n >= 0 && n < inventory.length) {
 	                ItemStack stack = ItemStack.of(nbttagcompound1);
@@ -292,14 +294,14 @@ public void load(net.minecraft.block.BlockState state, final CompoundNBT tagRoot
      * @param tagRoot The root of the NBT
      */
     @Override
-    public CompoundNBT save(final CompoundNBT tagRoot) {
+    public CompoundTag save(final CompoundTag tagRoot) {
         super .save(tagRoot);
         ItemStack[] inventory = this.getInventory();
         if(inventory != null ){
-	        final ListNBT nbttaglist = new ListNBT();
+	        final ListTag nbttaglist = new ListTag();
 	        for (int i = 0; i < inventory.length; ++i) {
 	            if (inventory[i] != null && !inventory[i].isEmpty()) {
-	                final CompoundNBT nbttagcompound1 = new CompoundNBT();
+	                final CompoundTag nbttagcompound1 = new CompoundTag();
 	                nbttagcompound1.putByte("Slot", (byte)i);
 	                inventory[i] .save(nbttagcompound1);
 	                nbttaglist .add(nbttagcompound1);
@@ -393,11 +395,11 @@ public void setCustomInventoryName(final String newName) {
      * @return A synchronization NBT holding values from the 
      * getDataFieldArray() array
      */
-    public CompoundNBT createDataFieldUpdateTag(){
+    public CompoundTag createDataFieldUpdateTag(){
     	this.prepareDataFieldsForSync();
     	int[] dataFields = this.getDataFieldArray();
-        CompoundNBT nbtTag = new CompoundNBT();
-		CompoundNBT tankTag = new CompoundNBT();
+        CompoundTag nbtTag = new CompoundTag();
+		CompoundTag tankTag = new CompoundTag();
 		getTank().writeToNBT(tankTag);
 		nbtTag.put("Tank", tankTag);
         nbtTag.putIntArray("[]", dataFields);
@@ -411,9 +413,9 @@ public CompoundNBT createDataFieldUpdateTag(){
      * @param tag A synchronization NBT holding values from the 
      * getDataFieldArray() array
      */
-    public void readDataFieldUpdateTag(CompoundNBT tag){
+    public void readDataFieldUpdateTag(CompoundTag tag){
 		if (tag .contains("Tank", 10)) {
-			CompoundNBT tankTag = tag .getCompound("Tank");
+			CompoundTag tankTag = tag .getCompound("Tank");
 			getTank().readFromNBT(tankTag);
 			if (tankTag.contains("Empty", 1)) {
 				getTank().setFluid(null);
@@ -430,26 +432,26 @@ public void readDataFieldUpdateTag(CompoundNBT tag){
      * Turns the data field NBT into a network packet
      */
     @Override 
-    public SUpdateTileEntityPacket getUpdatePacket(){
-        CompoundNBT nbtTag = createDataFieldUpdateTag();
-        return new SUpdateTileEntityPacket(this.worldPosition, 0, nbtTag);
+    public ClientboundBlockEntityDataPacket getUpdatePacket(){
+        CompoundTag nbtTag = createDataFieldUpdateTag();
+        return new ClientboundBlockEntityDataPacket(this.worldPosition, 0, nbtTag);
     }
 
 	@Override
-	public CompoundNBT getUpdateTag() {
-		return save(new CompoundNBT());
+	public CompoundTag getUpdateTag() {
+		return save(new CompoundTag());
 	}
 
 	@Override
-	public void handleUpdateTag(net.minecraft.block.BlockState state, CompoundNBT tag) {
-		load(state, tag);
+	public void handleUpdateTag(CompoundTag tag) {
+		load(tag);
 	}
 
     /**
      * Receives the network packet made by getDescriptionPacket()
      */
     @Override
-    public void onDataPacket(NetworkManager net, SUpdateTileEntityPacket packet) {
+    public void onDataPacket(Connection net, ClientboundBlockEntityDataPacket packet) {
         readDataFieldUpdateTag(packet.getTag());
     }
     
@@ -480,7 +482,7 @@ public boolean isEmpty() {
 	 * executes when a player right-clicks and opens the inventory GUI
 	 */
 	@Override
-	public void startOpen(PlayerEntity arg0) {
+	public void startOpen(Player arg0) {
 		// do nothing
 		
 	}
@@ -489,7 +491,7 @@ public void startOpen(PlayerEntity arg0) {
 	 * executes when a player stops interacting with this machine 
 	 */
 	@Override
-	public void stopOpen(PlayerEntity arg0) {
+	public void stopOpen(Player arg0) {
 		// do nothing
 		
 	}
@@ -624,7 +626,7 @@ public boolean canPlaceItem(final int slot, final ItemStack item) {
 	 * boilerplate inventory code
 	 */
 	@Override
-    public boolean stillValid(final PlayerEntity p_isUseableByPlayer_1_) {
+    public boolean stillValid(final Player p_isUseableByPlayer_1_) {
         return this.level.getBlockEntity(this.worldPosition) == this && p_isUseableByPlayer_1_.distanceToSqr((double)this.worldPosition.getX() + 0.5, (double)this.worldPosition.getY() + 0.5, (double)this.worldPosition.getZ() + 0.5) <= 64.0;
     }
 
@@ -651,7 +653,7 @@ public void setItem(int slot, ItemStack item) {
 	 * boilerplate inventory code
 	 */
 	@Override
-    public ITextComponent getDisplayName() {
+    public Component getDisplayName() {
         return getName();
     }
 
@@ -661,14 +663,14 @@ public ITextComponent getDisplayName() {
 	 * given by the player (i.e. with an anvil)
 	 */
 	@Override
-	public ITextComponent getName() {
-		return this.hasCustomName() ? new StringTextComponent(this.customName)
-				: new TranslationTextComponent(unlocalizedName);
+	public Component getName() {
+		return this.hasCustomName() ? new TextComponent(this.customName)
+				: new TranslatableComponent(unlocalizedName);
 	}
 
 	@Override
-	public ITextComponent getCustomName() {
-		return this.hasCustomName() ? new StringTextComponent(this.customName) : null;
+	public Component getCustomName() {
+		return this.hasCustomName() ? new TextComponent(this.customName) : null;
 	}
 
 	/**
diff --git a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java
index f5504d40..e66357b8 100644
--- a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java
+++ b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java
@@ -4,19 +4,21 @@
 import cyano.poweradvantage.api.PowerConnectorContext;
 import cyano.poweradvantage.api.PowerRequest;
 import cyano.poweradvantage.api.PoweredEntity;
-import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry;
-import net.minecraft.entity.player.PlayerEntity;
-import net.minecraft.inventory.ISidedInventory;
-import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.CompoundNBT;
-import net.minecraft.nbt.ListNBT;
-import net.minecraft.network.NetworkManager;
-import net.minecraft.network.play.server.SUpdateTileEntityPacket;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.Direction;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.StringTextComponent;
-import net.minecraft.util.text.TranslationTextComponent;
+import zone.moddev.mc.basepower.conduitnetwork.ConduitRegistry;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.WorldlyContainer;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.ListTag;
+import net.minecraft.network.Connection;
+import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
+import net.minecraft.world.level.block.entity.BlockEntityType;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.core.Direction;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.TextComponent;
+import net.minecraft.network.chat.TranslatableComponent;
 
 import java.util.Arrays;
 
@@ -31,7 +33,7 @@
  * @author DrCyano
  *
  */
-public abstract class TileEntitySimplePowerMachine extends PoweredEntity implements ISidedInventory, net.minecraft.util.INameable {
+public abstract class TileEntitySimplePowerMachine extends PoweredEntity implements WorldlyContainer, net.minecraft.world.Nameable {
 	private final ConduitType[] types;
 	private final float[] energyBufferSizes;
 	private final float[] energyBuffers;
@@ -50,8 +52,9 @@ public abstract class TileEntitySimplePowerMachine extends PoweredEntity impleme
 	 * @param unlocalizedName The string used for language look-up and 
 	 * entity registration. 
 	 */
-	public TileEntitySimplePowerMachine(TileEntityType tileType, ConduitType type, float energyBufferSize, String unlocalizedName){
-		super(tileType);
+	public TileEntitySimplePowerMachine(BlockEntityType tileType, BlockPos pos, BlockState state,
+            ConduitType type, float energyBufferSize, String unlocalizedName){
+		super(tileType, pos, state);
 		this.types = new ConduitType[1];
 		this.types[0] = type;
 		this.energyBufferSizes = new float[1];
@@ -69,8 +72,9 @@ public TileEntitySimplePowerMachine(TileEntityType tileType, ConduitType type
 	 * @param unlocalizedName The string used for language look-up and
 	 * entity registration.
 	 */
-	public TileEntitySimplePowerMachine(TileEntityType tileType, ConduitType[] typeList, float[] energyBufferSize, String unlocalizedName){
-		super(tileType);
+	public TileEntitySimplePowerMachine(BlockEntityType tileType, BlockPos pos, BlockState state,
+            ConduitType[] typeList, float[] energyBufferSize, String unlocalizedName){
+		super(tileType, pos, state);
 		if(typeList.length != energyBufferSize.length) throw new IllegalArgumentException("Array length mismatch. Number of ConduitTypes and number of buffer sizes must be equal.");
 		this.types = Arrays.copyOf(typeList,typeList.length);
 		this.energyBufferSizes = Arrays.copyOf(energyBufferSize,energyBufferSize.length);
@@ -288,14 +292,14 @@ public PowerRequest getPowerRequest(ConduitType type){
 	 * @param tagRoot The root of the NBT
 	 */
 	@Override
-	public void load(net.minecraft.block.BlockState state, final CompoundNBT tagRoot) {
-		super.load(state, tagRoot);
+	public void load(final CompoundTag tagRoot) {
+		super.load(tagRoot);
 		ItemStack[] inventory = this.getInventory();
 		if(inventory != null ){
 			Arrays.fill(inventory, null);
-			final ListNBT nbttaglist = tagRoot.getList("Items", 10);
+			final ListTag nbttaglist = tagRoot.getList("Items", 10);
 			for (int i = 0; i < nbttaglist.size() && i < inventory.length; ++i) {
-				final CompoundNBT nbttagcompound1 = nbttaglist .getCompound(i);
+				final CompoundTag nbttagcompound1 = nbttaglist .getCompound(i);
 				final byte n = nbttagcompound1.getByte("Slot");
 				if (n >= 0 && n < inventory.length) {
 					ItemStack stack = ItemStack.of(nbttagcompound1);
@@ -315,14 +319,14 @@ public void load(net.minecraft.block.BlockState state, final CompoundNBT tagRoot
 	 * @param tagRoot The root of the NBT
 	 */
 	@Override
-	public CompoundNBT save(final CompoundNBT tagRoot) {
+	public CompoundTag save(final CompoundTag tagRoot) {
 		super .save(tagRoot);
 		ItemStack[] inventory = this.getInventory();
 		if(inventory != null ){
-			final ListNBT nbttaglist = new ListNBT();
+			final ListTag nbttaglist = new ListTag();
 			for (int i = 0; i < inventory.length; ++i) {
 				if (inventory[i] != null && !inventory[i].isEmpty()) {
-					final CompoundNBT nbttagcompound1 = new CompoundNBT();
+					final CompoundTag nbttagcompound1 = new CompoundTag();
 					nbttagcompound1.putByte("Slot", (byte)i);
 					inventory[i] .save(nbttagcompound1);
 					nbttaglist .add(nbttagcompound1);
@@ -400,10 +404,10 @@ public void setCustomInventoryName(final String newName) {
 	 * @return A synchronization NBT holding values from the 
 	 * getDataFieldArray() array
 	 */
-	public CompoundNBT createDataFieldUpdateTag(){
+	public CompoundTag createDataFieldUpdateTag(){
 		this.prepareDataFieldsForSync();
 		int[] dataFields = this.getDataFieldArray();
-		CompoundNBT nbtTag = new CompoundNBT();
+		CompoundTag nbtTag = new CompoundTag();
 		nbtTag.putIntArray("[]", dataFields);
 		return nbtTag;
 	}
@@ -415,7 +419,7 @@ public CompoundNBT createDataFieldUpdateTag(){
 	 * @param tag A synchronization NBT holding values from the 
 	 * getDataFieldArray() array
 	 */
-	public void readDataFieldUpdateTag(CompoundNBT tag){
+	public void readDataFieldUpdateTag(CompoundTag tag){
 		int[] newData = tag.getIntArray("[]");
 		System.arraycopy(newData, 0, this.getDataFieldArray(), 0, Math.min(newData.length, this.getDataFieldArray().length));
 		this.onDataFieldUpdate();
@@ -426,16 +430,16 @@ public void readDataFieldUpdateTag(CompoundNBT tag){
 	 * Turns the data field NBT into a network packet
 	 */
 	@Override 
-	public SUpdateTileEntityPacket getUpdatePacket(){
-		CompoundNBT nbtTag = createDataFieldUpdateTag();
-		return new SUpdateTileEntityPacket(this.worldPosition, 0, nbtTag);
+	public ClientboundBlockEntityDataPacket getUpdatePacket(){
+		CompoundTag nbtTag = createDataFieldUpdateTag();
+		return new ClientboundBlockEntityDataPacket(this.worldPosition, 0, nbtTag);
 	}
 
 	/**
 	 * Receives the network packet made by getDescriptionPacket()
 	 */
 	@Override
-	public void onDataPacket(NetworkManager net, SUpdateTileEntityPacket packet) {
+	public void onDataPacket(Connection net, ClientboundBlockEntityDataPacket packet) {
 		readDataFieldUpdateTag(packet.getTag());
 	}
 	
@@ -466,7 +470,7 @@ public boolean isEmpty() {
 	 * executes when a player right-clicks and opens the inventory GUI
 	 */
 	@Override
-	public void startOpen(PlayerEntity arg0) {
+	public void startOpen(Player arg0) {
 		// do nothing
 		
 	}
@@ -475,7 +479,7 @@ public void startOpen(PlayerEntity arg0) {
 	 * executes when a player stops interacting with this machine 
 	 */
 	@Override
-	public void stopOpen(PlayerEntity arg0) {
+	public void stopOpen(Player arg0) {
 		// do nothing
 		
 	}
@@ -610,7 +614,7 @@ public boolean canPlaceItem(final int slot, final ItemStack item) {
 	 * boilerplate inventory code
 	 */
 	@Override
-	public boolean stillValid(final PlayerEntity p_isUseableByPlayer_1_) {
+	public boolean stillValid(final Player p_isUseableByPlayer_1_) {
 		return this.level.getBlockEntity(this.worldPosition) == this && p_isUseableByPlayer_1_.distanceToSqr((double)this.worldPosition.getX() + 0.5, (double)this.worldPosition.getY() + 0.5, (double)this.worldPosition.getZ() + 0.5) <= 64.0;
 	}
 
@@ -637,7 +641,7 @@ public void setItem(int slot, ItemStack item) {
 	 * boilerplate inventory code
 	 */
 	@Override
-	public ITextComponent getDisplayName() {
+	public Component getDisplayName() {
 		return getName();
 	}
 
@@ -647,14 +651,14 @@ public ITextComponent getDisplayName() {
 	 * given by the player (i.e. with an anvil)
 	 */
 	@Override
-	public ITextComponent getName() {
-		return this.hasCustomName() ? new StringTextComponent(this.customName)
-				: new TranslationTextComponent(unlocalizedName);
+	public Component getName() {
+		return this.hasCustomName() ? new TextComponent(this.customName)
+				: new TranslatableComponent(unlocalizedName);
 	}
 
 	@Override
-	public ITextComponent getCustomName() {
-		return this.hasCustomName() ? new StringTextComponent(this.customName) : null;
+	public Component getCustomName() {
+		return this.hasCustomName() ? new TextComponent(this.customName) : null;
 	}
 
 	/**
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/PowerAdvantage.java b/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java
similarity index 78%
rename from src/main/java/zone/moddev/mc/poweradvantage/PowerAdvantage.java
rename to src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java
index 71fb7be0..167d10a9 100644
--- a/src/main/java/zone/moddev/mc/poweradvantage/PowerAdvantage.java
+++ b/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java
@@ -1,4 +1,4 @@
-package zone.moddev.mc.poweradvantage;
+package zone.moddev.mc.basepower;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -15,34 +15,34 @@
 import net.minecraftforge.api.distmarker.Dist;
 import net.minecraftforge.common.MinecraftForge;
 import net.minecraftforge.fml.DistExecutor;
-import net.minecraftforge.fml.ExtensionPoint;
+
 import net.minecraftforge.fml.ModLoadingContext;
 import net.minecraftforge.fml.common.Mod;
 import net.minecraftforge.fml.config.ModConfig;
 import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
 import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
 import net.minecraftforge.fml.loading.FMLPaths;
-import zone.moddev.mc.poweradvantage.config.PowerAdvantageConfig;
-import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry;
-import zone.moddev.mc.poweradvantage.init.Blocks;
-import zone.moddev.mc.poweradvantage.init.Fluids;
-import zone.moddev.mc.poweradvantage.init.GUI;
-import zone.moddev.mc.poweradvantage.init.Fuels;
-import zone.moddev.mc.poweradvantage.init.Items;
-import zone.moddev.mc.poweradvantage.init.ModSupport;
-import zone.moddev.mc.poweradvantage.init.Recipes;
-import zone.moddev.mc.poweradvantage.registry.MachineGUIRegistry;
-import zone.moddev.mc.poweradvantage.recipe.RecipeConditions;
-import zone.moddev.mc.poweradvantage.recipe.RecipeDiscovery;
-import zone.moddev.mc.poweradvantage.migration.AdvantageLegacyMigrations;
-import zone.moddev.mc.poweradvantage.migration.LegacyWorldDataHook;
+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.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;
 
 /** Forge 1.16 entry point for Power Advantage. */
 @Mod(PowerAdvantage.MODID)
 public final class PowerAdvantage {
-    public static final String MODID = "poweradvantage";
-    public static final String NAME = "Power Advantage";
-    public static final String VERSION = "2.4.7.116051";
+    public static final String MODID = "basepower";
+    public static final String NAME = "basePower";
+    public static final String VERSION = "2.4.9.117011";
     public static final Logger LOGGER = LogManager.getLogger(MODID);
 
     private static PowerAdvantage instance;
@@ -75,13 +75,14 @@ public PowerAdvantage() {
         instance = this;
 
         AdvantageLegacyMigrations.registerPowerAdvantage();
-        zone.moddev.mc.poweradvantage.migration.VanillaRegistryRemaps.register();
+        zone.moddev.mc.basepower.migration.VanillaRegistryRemaps.register();
         MinecraftForge.EVENT_BUS.addListener(ConduitRegistry.getInstance()::onChunkLoad);
         MinecraftForge.EVENT_BUS.addListener(ConduitRegistry.getInstance()::onChunkUnload);
         MinecraftForge.EVENT_BUS.addListener(RecipeDiscovery::onPlayerLogin);
-        MinecraftForge.EVENT_BUS.addListener(zone.moddev.mc.poweradvantage.recipe.PartsLoot::onLoad);
-        MinecraftForge.EVENT_BUS.addListener(zone.moddev.mc.poweradvantage.init.Villages::onTrades);
-        MinecraftForge.EVENT_BUS.addListener(zone.moddev.mc.poweradvantage.recipe.MaterialTags::installReader);
+        MinecraftForge.EVENT_BUS.addListener(zone.moddev.mc.basepower.recipe.PartsLoot::onLoad);
+        MinecraftForge.EVENT_BUS.addListener(zone.moddev.mc.basepower.init.Villages::onTrades);
+        FMLJavaModLoadingContext.get().getModEventBus().addListener(
+                zone.moddev.mc.basepower.recipe.MaterialTags::registerPack);
 
         Fluids.init();
         Blocks.init();
@@ -98,18 +99,18 @@ public PowerAdvantage() {
         DistExecutor.runWhenOn(Dist.CLIENT, () -> () -> {
             FMLJavaModLoadingContext.get().getModEventBus().addListener(
                     (net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent event) ->
-                            net.minecraftforge.fml.DeferredWorkQueue.runLater(() -> {
+                            event.enqueueWork(() -> {
                                 MachineGUIRegistry.registerScreens();
-                                zone.moddev.mc.poweradvantage.client.ClientOilRenderer.configureRenderLayers();
+                                zone.moddev.mc.basepower.client.ClientOilRenderer.configureRenderLayers();
                             }));
-            zone.moddev.mc.poweradvantage.client.ClientOilRenderer.register();
+            zone.moddev.mc.basepower.client.ClientOilRenderer.register();
         });
     }
 
     private static void migrateLegacyOreSpawnConfiguration() {
         try {
             LegacyOreSpawnConfigBridge.Result result =
-                    LegacyOreSpawnConfigBridge.migrate(FMLPaths.CONFIGDIR.get(), MODID);
+                    LegacyOreSpawnConfigBridge.migrate(FMLPaths.CONFIGDIR.get(), MODID, "poweradvantage");
             if (result.wasMigrated()) {
                 LOGGER.info("Imported {} legacy Power Advantage OreSpawn rule(s) from '{}' into '{}'",
                         result.getRuleCount(), result.getSource(), result.getTarget());
@@ -121,7 +122,7 @@ private static void migrateLegacyOreSpawnConfiguration() {
 
     private static void migrateLegacyConfiguration() {
         try {
-            LegacyConfigMigration.Result result = LegacyConfigMigration.migrate(FMLPaths.CONFIGDIR.get(), MODID,
+            LegacyConfigMigration.Result result = LegacyConfigMigration.migrate(FMLPaths.CONFIGDIR.get(), MODID, "poweradvantage",
                     Mapping.of("options", "recipe_mode", "options", "recipeMode", ValueType.STRING),
                     Mapping.of("options", "treasure_chest_loot_factor", "options", "treasureChestLootFactor", ValueType.DOUBLE),
                     Mapping.of("options", "plastic_equals_rubber", "options", "plasticEqualsRubber", ValueType.BOOLEAN),
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/RecipeMode.java b/src/main/java/zone/moddev/mc/basepower/RecipeMode.java
similarity index 96%
rename from src/main/java/zone/moddev/mc/poweradvantage/RecipeMode.java
rename to src/main/java/zone/moddev/mc/basepower/RecipeMode.java
index cf753ec5..03c5450f 100644
--- a/src/main/java/zone/moddev/mc/poweradvantage/RecipeMode.java
+++ b/src/main/java/zone/moddev/mc/basepower/RecipeMode.java
@@ -1,4 +1,4 @@
-package zone.moddev.mc.poweradvantage;
+package zone.moddev.mc.basepower;
 
 /**
  * Differnet recipe modes change the difficulty and means by which the players make the core items 
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/blocks/BlockFrame.java b/src/main/java/zone/moddev/mc/basepower/blocks/BlockFrame.java
similarity index 55%
rename from src/main/java/zone/moddev/mc/poweradvantage/blocks/BlockFrame.java
rename to src/main/java/zone/moddev/mc/basepower/blocks/BlockFrame.java
index b972b08f..a35eb137 100644
--- a/src/main/java/zone/moddev/mc/poweradvantage/blocks/BlockFrame.java
+++ b/src/main/java/zone/moddev/mc/basepower/blocks/BlockFrame.java
@@ -1,13 +1,13 @@
-package zone.moddev.mc.poweradvantage.blocks;
-
-import net.minecraft.block.Block;
-import net.minecraft.block.SoundType;
-import net.minecraft.block.material.Material;
-import net.minecraft.block.BlockState;
-import net.minecraft.entity.player.PlayerEntity;
-import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.World;
-import net.minecraft.world.IWorldReader;
+package zone.moddev.mc.basepower.blocks;
+
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.SoundType;
+import net.minecraft.world.level.material.Material;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.LevelReader;
 import net.minecraftforge.api.distmarker.Dist;
 import net.minecraftforge.api.distmarker.OnlyIn;
 
@@ -28,7 +28,7 @@ public boolean isFullCube(BlockState bs) {
 		return false;
 	}
 
-	public boolean canSilkHarvest(BlockState state, IWorldReader world, BlockPos pos, PlayerEntity player) {
+	public boolean canSilkHarvest(BlockState state, LevelReader world, BlockPos pos, Player player) {
 		return true;
 	}
 
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/blocks/BlockPowerSwitch.java b/src/main/java/zone/moddev/mc/basepower/blocks/BlockPowerSwitch.java
similarity index 80%
rename from src/main/java/zone/moddev/mc/poweradvantage/blocks/BlockPowerSwitch.java
rename to src/main/java/zone/moddev/mc/basepower/blocks/BlockPowerSwitch.java
index e990e22f..139c7428 100644
--- a/src/main/java/zone/moddev/mc/poweradvantage/blocks/BlockPowerSwitch.java
+++ b/src/main/java/zone/moddev/mc/basepower/blocks/BlockPowerSwitch.java
@@ -1,18 +1,18 @@
-package zone.moddev.mc.poweradvantage.blocks;
+package zone.moddev.mc.basepower.blocks;
 
 import cyano.poweradvantage.api.ConduitType;
 import cyano.poweradvantage.api.ITypedConduit;
 import cyano.poweradvantage.api.PowerConnectorContext;
-import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry;
-import net.minecraft.block.Block;
-import net.minecraft.block.material.Material;
-import net.minecraft.state.BooleanProperty;
-import net.minecraft.state.StateContainer;
-import net.minecraft.block.BlockState;
-import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.Explosion;
-import net.minecraft.world.IBlockReader;
-import net.minecraft.world.World;
+import zone.moddev.mc.basepower.conduitnetwork.ConduitRegistry;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.material.Material;
+import net.minecraft.world.level.block.state.properties.BooleanProperty;
+import net.minecraft.world.level.block.state.StateDefinition;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.Explosion;
+import net.minecraft.world.level.BlockGetter;
+import net.minecraft.world.level.Level;
 
 /**
  * This block class is a user-friendly power switch for turning power on and off. You do not need to 
@@ -54,7 +54,7 @@ public BlockPowerSwitch(ConduitType powerType) {
 	 * Creates a blockstate
 	 */
 	@Override
-	protected void createBlockStateDefinition(StateContainer.Builder builder) {
+	protected void createBlockStateDefinition(StateDefinition.Builder builder) {
 		builder.add(ACTIVE);
 	}
 
@@ -118,7 +118,7 @@ public boolean isFullCube(BlockState bs) {
 	 * Override of default block behavior
 	 */
 	@Override
-	public void onPlace(final BlockState state, final World world, final BlockPos coord, final BlockState oldState, final boolean isMoving) {
+	public void onPlace(final BlockState state, final Level world, final BlockPos coord, final BlockState oldState, final boolean isMoving) {
 		super.onPlace(state, world, coord, oldState, isMoving);
 		ConduitRegistry.getInstance().conduitBlockPlacedEvent(world, coord, getTypes());
 	}
@@ -127,7 +127,7 @@ public void onPlace(final BlockState state, final World world, final BlockPos co
 	 * This method is called when the block is removed from the world by an entity.
 	 */
 	@Override
-	public void onRemove(BlockState state, World w, BlockPos coord, BlockState newState, boolean isMoving) {
+	public void onRemove(BlockState state, Level w, BlockPos coord, BlockState newState, boolean isMoving) {
 		if (state.getBlock() != newState.getBlock()) {
 			ConduitRegistry.getInstance().conduitBlockRemovedEvent(w, coord, getTypes());
 		}
@@ -138,7 +138,7 @@ public void onRemove(BlockState state, World w, BlockPos coord, BlockState newSt
 	 * This method is called when the block is destroyed by an explosion.
 	 */
 	@Override
-	public void wasExploded(World w, BlockPos coord, Explosion boom) {
+	public void wasExploded(Level w, BlockPos coord, Explosion boom) {
 		super.wasExploded(w, coord, boom);
 		ConduitRegistry.getInstance().conduitBlockRemovedEvent(w, coord, getTypes());
 	}
@@ -146,9 +146,9 @@ public void wasExploded(World w, BlockPos coord, Explosion boom) {
 	/**
 	 * Called when a neighboring block changes.
 	 */
-	public void onNeighborChange(IBlockReader w, BlockPos pos, BlockPos neighbor) {
-		if (w instanceof World) {
-			World world = (World) w;
+	public void onNeighborChange(BlockGetter w, BlockPos pos, BlockPos neighbor) {
+		if (w instanceof Level) {
+			Level world = (Level) w;
 			BlockState state = world.getBlockState(pos);
 			// redstone power given or taken
 			boolean activated = !world.hasNeighborSignal(pos);
@@ -162,7 +162,7 @@ public void onNeighborChange(IBlockReader w, BlockPos pos, BlockPos neighbor) {
 	}
 
 	@Override
-	public void neighborChanged(BlockState state, World world, BlockPos pos, Block block, BlockPos fromPos, boolean isMoving) {
+	public void neighborChanged(BlockState state, Level world, BlockPos pos, Block block, BlockPos fromPos, boolean isMoving) {
 		onNeighborChange(world, pos, pos);
 	}
 
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/gui/package-info.java b/src/main/java/zone/moddev/mc/basepower/blocks/package-info.java
similarity index 77%
rename from src/main/java/zone/moddev/mc/poweradvantage/gui/package-info.java
rename to src/main/java/zone/moddev/mc/basepower/blocks/package-info.java
index a8bcce10..0b4337c6 100644
--- a/src/main/java/zone/moddev/mc/poweradvantage/gui/package-info.java
+++ b/src/main/java/zone/moddev/mc/basepower/blocks/package-info.java
@@ -3,10 +3,10 @@
 This package contains content that is added by PowerAdvantage implementing the PowerAdvantage API. 
 If you are writing an add-on mod, use the code in this package for reference, but you should not 
 depend on it's implementation details as future updates might change them. For a better way to get 
-PowerAdvantage content, look in the zone.moddev.mc.poweradvantage.init package.
+PowerAdvantage content, look in the zone.moddev.mc.basepower.init package.
 

Because these classes are not part of the API, they are not well documented. They were only meant for the eyes of the mod author(s).

*/ -package zone.moddev.mc.poweradvantage.gui; \ No newline at end of file +package zone.moddev.mc.basepower.blocks; \ No newline at end of file diff --git a/src/main/java/zone/moddev/mc/basepower/client/ClientOilRenderer.java b/src/main/java/zone/moddev/mc/basepower/client/ClientOilRenderer.java new file mode 100644 index 00000000..14785fca --- /dev/null +++ b/src/main/java/zone/moddev/mc/basepower/client/ClientOilRenderer.java @@ -0,0 +1,44 @@ +package zone.moddev.mc.basepower.client; + +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.ItemBlockRenderTypes; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.client.event.TextureStitchEvent; +import net.minecraftforge.common.MinecraftForge; +import zone.moddev.mc.basepower.PowerAdvantage; + +/** Forge FluidAttributes handle rendering; only the custom atlas sprites need stitching. */ +@OnlyIn(Dist.CLIENT) +public final class ClientOilRenderer { + private ClientOilRenderer() {} + + public static void register() { + MinecraftForge.EVENT_BUS.addListener(ClientOilRenderer::stitch); + } + + public static void configureRenderLayers() { + ItemBlockRenderTypes.setRenderLayer(zone.moddev.mc.basepower.init.Blocks.steel_frame, + RenderType.cutout()); + for (net.minecraft.world.level.material.Fluid fluid : new net.minecraft.world.level.material.Fluid[] { + zone.moddev.mc.basepower.init.Fluids.crude_oil, + zone.moddev.mc.basepower.init.Fluids.flowing_crude_oil, + zone.moddev.mc.basepower.init.Fluids.refined_oil, + zone.moddev.mc.basepower.init.Fluids.flowing_refined_oil}) { + ItemBlockRenderTypes.setRenderLayer(fluid, RenderType.translucent()); + } + ItemBlockRenderTypes.setRenderLayer(zone.moddev.mc.basepower.init.Blocks.crude_oil_block, + RenderType.translucent()); + ItemBlockRenderTypes.setRenderLayer(zone.moddev.mc.basepower.init.Blocks.refined_oil_block, + RenderType.translucent()); + } + + private static void stitch(TextureStitchEvent.Pre event) { + if (!TextureAtlas.LOCATION_BLOCKS.equals(event.getMap().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/poweradvantage/compat/BaseMetalsCompat.java b/src/main/java/zone/moddev/mc/basepower/compat/BaseMetalsCompat.java similarity index 83% rename from src/main/java/zone/moddev/mc/poweradvantage/compat/BaseMetalsCompat.java rename to src/main/java/zone/moddev/mc/basepower/compat/BaseMetalsCompat.java index 2f900a7d..4dbc14d8 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/compat/BaseMetalsCompat.java +++ b/src/main/java/zone/moddev/mc/basepower/compat/BaseMetalsCompat.java @@ -1,12 +1,12 @@ -package zone.moddev.mc.poweradvantage.compat; +package zone.moddev.mc.basepower.compat; -import net.minecraft.block.Block; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.util.ResourceLocation; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.fml.ModList; import net.minecraftforge.registries.ForgeRegistries; -import zone.moddev.mc.poweradvantage.PowerAdvantage; +import zone.moddev.mc.basepower.PowerAdvantage; /** * Compatibility entry points retained for old internal callers. Crushing recipes diff --git a/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManager.java b/src/main/java/zone/moddev/mc/basepower/conduitnetwork/ConduitNetworkManager.java similarity index 92% rename from src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManager.java rename to src/main/java/zone/moddev/mc/basepower/conduitnetwork/ConduitNetworkManager.java index d7b328ab..c77ddb7a 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManager.java +++ b/src/main/java/zone/moddev/mc/basepower/conduitnetwork/ConduitNetworkManager.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.conduitnetwork; +package zone.moddev.mc.basepower.conduitnetwork; import java.util.ArrayList; import java.util.Collections; @@ -9,12 +9,12 @@ import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PowerConnectorContext; -import zone.moddev.mc.poweradvantage.math.BlockPos4D; -import zone.moddev.mc.poweradvantage.util.PowerHelper; -import net.minecraft.block.BlockState; -import net.minecraft.util.Direction; -import net.minecraft.util.math.ChunkPos; -import net.minecraft.world.World; +import zone.moddev.mc.basepower.math.BlockPos4D; +import zone.moddev.mc.basepower.util.PowerHelper; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.core.Direction; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; /** * This class manages a global (trans-dimensional) cache of power networks of a single power type. @@ -131,7 +131,7 @@ public void invalidateAll() { * @param dimension dimension containing the changed chunk * @param chunk changed chunk position */ - public void invalidateChunk(net.minecraft.util.RegistryKey dimension, ChunkPos chunk) { + public void invalidateChunk(net.minecraft.resources.ResourceKey dimension, ChunkPos chunk) { lock.writeLock().lock(); try { int minX = (chunk.x << 4) - 1; @@ -155,7 +155,7 @@ public void invalidateChunk(net.minecraft.util.RegistryKey dimension, Chu * @param type The energy type that is triggering the revalidation (should be the same type as * this manager) */ - public void revalidate(BlockPos4D coord, World w, ConduitType type) { + public void revalidate(BlockPos4D coord, Level w, ConduitType type) { lock.writeLock().lock(); try { invalidate(coord); @@ -193,7 +193,7 @@ public boolean areInSameNetwork(BlockPos4D a, BlockPos4D b) { * @param src The block from which we are scanning * @param type The type of energy that will flow throught the scanned network */ - protected void recursiveScan(World w, BlockPos4D coord, BlockState src, ConduitType type) { + protected void recursiveScan(Level w, BlockPos4D coord, BlockState src, ConduitType type) { for (int i = 0; i < Direction.values().length; i++) { Direction face = Direction.values()[i]; BlockPos4D n = coord.relative(face); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitRegistry.java b/src/main/java/zone/moddev/mc/basepower/conduitnetwork/ConduitRegistry.java similarity index 86% rename from src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitRegistry.java rename to src/main/java/zone/moddev/mc/basepower/conduitnetwork/ConduitRegistry.java index 09ae70c3..82b09df1 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitRegistry.java +++ b/src/main/java/zone/moddev/mc/basepower/conduitnetwork/ConduitRegistry.java @@ -1,6 +1,6 @@ -package zone.moddev.mc.poweradvantage.conduitnetwork; +package zone.moddev.mc.basepower.conduitnetwork; -import zone.moddev.mc.poweradvantage.PowerAdvantage; +import zone.moddev.mc.basepower.PowerAdvantage; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.IPowerMachine; import cyano.poweradvantage.api.ITypedConduit; @@ -8,13 +8,13 @@ import cyano.poweradvantage.api.modsupport.ExternalPowerRequest; import cyano.poweradvantage.api.modsupport.LightWeightPowerRegistry; import cyano.poweradvantage.api.modsupport.Wrappers; -import zone.moddev.mc.poweradvantage.math.BlockPos4D; -import net.minecraft.block.Block; -import net.minecraft.block.ITileEntityProvider; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; +import zone.moddev.mc.basepower.math.BlockPos4D; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.energy.CapabilityEnergy; import net.minecraftforge.energy.IEnergyStorage; @@ -32,7 +32,7 @@ public class ConduitRegistry { private final Map networkManagers = new HashMap<>(); - private final Map, World> activeWorlds = new HashMap<>(); + private final Map, Level> activeWorlds = new HashMap<>(); // thread-safe singleton instantiation private static ConduitRegistry instance = null; @@ -73,7 +73,7 @@ public static ConduitRegistry getInstance() { * @return A list of PowerRequest instances, sorted in order of highest priority to lowest * priority. */ - public List getRequestsForPower(World w, BlockPos coord, ConduitType conduitType) { + public List getRequestsForPower(Level w, BlockPos coord, ConduitType conduitType) { return getRequestsForPower(w, coord, conduitType, conduitType); } @@ -90,9 +90,9 @@ public List getRequestsForPower(World w, BlockPos coord, ConduitTy * @return A list of PowerRequest instances, sorted in order of highest priority to lowest * priority. */ - public List getRequestsForPower(World w, BlockPos coord, ConduitType conduitType, ConduitType energyType) { + public List getRequestsForPower(Level w, BlockPos coord, ConduitType conduitType, ConduitType energyType) { if (!w.hasChunkAt(coord)) return Collections.emptyList(); - net.minecraft.util.RegistryKey dimension = w.dimension(); + net.minecraft.resources.ResourceKey dimension = w.dimension(); clearIfWorldChanged(w, dimension); ConduitNetworkManager manager = getConduitNetworkManager(conduitType); BlockPos4D bp = new BlockPos4D(dimension, coord); @@ -109,13 +109,13 @@ public List getRequestsForPower(World w, BlockPos coord, ConduitTy return requests; } - private List collectRequests(World w, List net, ConduitType conduitType, ConduitType energyType) { + private List collectRequests(Level w, List net, ConduitType conduitType, ConduitType energyType) { List requests = new ArrayList<>(); for (BlockPos4D pos : net) { if (!w.hasChunkAt(pos.pos)) continue; Block b = w.getBlockState(pos.pos).getBlock(); - if (b instanceof ITileEntityProvider) { - TileEntity e = w.getBlockEntity(pos.pos); + if (b instanceof EntityBlock) { + BlockEntity e = w.getBlockEntity(pos.pos); if (e instanceof IPowerMachine && ((ITypedConduit) e).isPowerSink(energyType)) { PowerRequest req = ((IPowerMachine) e).getPowerRequest(energyType); if (req != PowerRequest.REQUEST_NOTHING) requests.add(req); @@ -156,7 +156,7 @@ private List collectRequests(World w, List net, Condui */ public static float transmitPowerToConsumers( final float availableEnergy, ConduitType powerType, byte minimumPriority, - TileEntity provider + BlockEntity provider ) { return transmitPowerToConsumers(availableEnergy, powerType, powerType, minimumPriority, provider.getLevel(), provider.getBlockPos(), provider); } @@ -180,14 +180,14 @@ public static float transmitPowerToConsumers( */ public static float transmitPowerToConsumers( final float availableEnergy, ConduitType networkType, ConduitType powerType, byte minimumPriority, - World world, BlockPos srcPos, Object providerInstance + Level world, BlockPos srcPos, Object providerInstance ) { List requests = ConduitRegistry.getInstance().getRequestsForPower(world, srcPos, networkType, powerType); return distributePower(availableEnergy, powerType, minimumPriority, world, providerInstance, requests); } static float distributePower(float availableEnergy, ConduitType powerType, byte minimumPriority, - World world, Object providerInstance, List requests) { + Level world, Object providerInstance, List requests) { if (!Float.isFinite(availableEnergy) || availableEnergy <= 0) return 0; float e = availableEnergy; for (PowerRequest req : requests) { @@ -218,9 +218,9 @@ static float distributePower(float availableEnergy, ConduitType powerType, byte * @param location The position of the block being added * @param type The native energy type of the added conduit block */ - public void conduitBlockPlacedEvent(World w, BlockPos location, ConduitType type) { + public void conduitBlockPlacedEvent(Level w, BlockPos location, ConduitType type) { if (w.isClientSide) return; // ignore client-side - net.minecraft.util.RegistryKey dimension = w.dimension(); + net.minecraft.resources.ResourceKey dimension = w.dimension(); clearIfWorldChanged(w, dimension); BlockPos4D coord = new BlockPos4D(dimension, location); ConduitNetworkManager manager = getConduitNetworkManager(type); @@ -239,7 +239,7 @@ public void conduitBlockPlacedEvent(World w, BlockPos location, ConduitType type * @param location The position of the block being added * @param types The native energy type of the added conduit block */ - public void conduitBlockPlacedEvent(World w, BlockPos location, ConduitType... types) { + public void conduitBlockPlacedEvent(Level w, BlockPos location, ConduitType... types) { for (int i = 0; i < types.length; i++) { conduitBlockPlacedEvent(w, location, types[i]); } @@ -253,7 +253,7 @@ public void conduitBlockPlacedEvent(World w, BlockPos location, ConduitType... t * @param w world containing the loaded tile * @param types power types whose cached networks must be rebuilt */ - public void conduitBlockLoadedEvent(World w, ConduitType... types) { + public void conduitBlockLoadedEvent(Level w, ConduitType... types) { if (w.isClientSide) return; clearIfWorldChanged(w, w.dimension()); for (ConduitType type : types) { @@ -272,10 +272,10 @@ public void onChunkUnload(ChunkEvent.Unload event) { } private void invalidateChunk(ChunkEvent event) { - if (!(event.getWorld() instanceof World)) return; - World world = (World) event.getWorld(); + if (!(event.getWorld() instanceof Level)) return; + Level world = (Level) event.getWorld(); if (world.isClientSide) return; - net.minecraft.util.RegistryKey dimension = world.dimension(); + net.minecraft.resources.ResourceKey dimension = world.dimension(); clearIfWorldChanged(world, dimension); for (ConduitNetworkManager manager : networkManagers.values()) { manager.invalidateChunk(dimension, event.getChunk().getPos()); @@ -289,9 +289,9 @@ private void invalidateChunk(ChunkEvent event) { * @param location The position of the block being removed * @param type The native energy type of the removed conduit block */ - public void conduitBlockRemovedEvent(World w, BlockPos location, ConduitType type) { + public void conduitBlockRemovedEvent(Level w, BlockPos location, ConduitType type) { if (w.isClientSide) return; // ignore client-side - net.minecraft.util.RegistryKey dimension = w.dimension(); + net.minecraft.resources.ResourceKey dimension = w.dimension(); clearIfWorldChanged(w, dimension); BlockPos4D coord = new BlockPos4D(dimension, location); ConduitNetworkManager manager = getConduitNetworkManager(type); @@ -310,7 +310,7 @@ public void conduitBlockRemovedEvent(World w, BlockPos location, ConduitType typ * @param location The position of the block being removed * @param types The native energy type of the removed conduit block */ - public void conduitBlockRemovedEvent(World w, BlockPos location, ConduitType... types) { + public void conduitBlockRemovedEvent(Level w, BlockPos location, ConduitType... types) { for (int i = 0; i < types.length; i++) { conduitBlockRemovedEvent(w, location, types[i]); } @@ -327,32 +327,32 @@ private ConduitNetworkManager getConduitNetworkManager(ConduitType type) { } /** @deprecated The supplied world, not a numeric ID, identifies a dimension. */ - @Deprecated public void conduitBlockPlacedEvent(World world, int ignoredDimension, BlockPos pos, ConduitType type) { + @Deprecated public void conduitBlockPlacedEvent(Level world, int ignoredDimension, BlockPos pos, ConduitType type) { conduitBlockPlacedEvent(world, pos, type); } /** @deprecated Use the world-only overload. */ - @Deprecated public void conduitBlockPlacedEvent(World world, int ignoredDimension, BlockPos pos, ConduitType... types) { + @Deprecated public void conduitBlockPlacedEvent(Level world, int ignoredDimension, BlockPos pos, ConduitType... types) { conduitBlockPlacedEvent(world, pos, types); } /** @deprecated Use the world-only overload. */ - @Deprecated public void conduitBlockRemovedEvent(World world, int ignoredDimension, BlockPos pos, ConduitType type) { + @Deprecated public void conduitBlockRemovedEvent(Level world, int ignoredDimension, BlockPos pos, ConduitType type) { conduitBlockRemovedEvent(world, pos, type); } /** @deprecated Use the world-only overload. */ - @Deprecated public void conduitBlockRemovedEvent(World world, int ignoredDimension, BlockPos pos, ConduitType... types) { + @Deprecated public void conduitBlockRemovedEvent(Level world, int ignoredDimension, BlockPos pos, ConduitType... types) { conduitBlockRemovedEvent(world, pos, types); } /** @deprecated Use the world-only overload. */ - @Deprecated public void conduitBlockLoadedEvent(World world, int ignoredDimension, ConduitType... types) { + @Deprecated public void conduitBlockLoadedEvent(Level world, int ignoredDimension, ConduitType... types) { conduitBlockLoadedEvent(world, types); } - private void clearIfWorldChanged(World w, net.minecraft.util.RegistryKey dimension) { - World knownWorld = activeWorlds.get(dimension); + private void clearIfWorldChanged(Level w, net.minecraft.resources.ResourceKey dimension) { + Level knownWorld = activeWorlds.get(dimension); if (knownWorld == w) return; if (knownWorld != null) { networkManagers.clear(); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/package-info.java b/src/main/java/zone/moddev/mc/basepower/conduitnetwork/package-info.java similarity index 87% rename from src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/package-info.java rename to src/main/java/zone/moddev/mc/basepower/conduitnetwork/package-info.java index 0dae2dea..e16e94ab 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/package-info.java +++ b/src/main/java/zone/moddev/mc/basepower/conduitnetwork/package-info.java @@ -4,4 +4,4 @@ the same power type and caching the resulting list of sources and sinks. Every time a conduit block is placed or broken, any adjacent networks are revalidated. */ -package zone.moddev.mc.poweradvantage.conduitnetwork; \ No newline at end of file +package zone.moddev.mc.basepower.conduitnetwork; \ No newline at end of file diff --git a/src/main/java/zone/moddev/mc/poweradvantage/config/PowerAdvantageConfig.java b/src/main/java/zone/moddev/mc/basepower/config/PowerAdvantageConfig.java similarity index 94% rename from src/main/java/zone/moddev/mc/poweradvantage/config/PowerAdvantageConfig.java rename to src/main/java/zone/moddev/mc/basepower/config/PowerAdvantageConfig.java index 36a0c796..a7a0981d 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/config/PowerAdvantageConfig.java +++ b/src/main/java/zone/moddev/mc/basepower/config/PowerAdvantageConfig.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.config; +package zone.moddev.mc.basepower.config; import java.util.Arrays; import java.util.Locale; @@ -6,8 +6,8 @@ import net.minecraftforge.common.ForgeConfigSpec; import net.minecraftforge.fml.config.ModConfig; import cyano.poweradvantage.api.FluidCategoryRegistry; -import zone.moddev.mc.poweradvantage.PowerAdvantage; -import zone.moddev.mc.poweradvantage.RecipeMode; +import zone.moddev.mc.basepower.PowerAdvantage; +import zone.moddev.mc.basepower.RecipeMode; /** Common configuration with the same gameplay defaults as the legacy cfg file. */ public final class PowerAdvantageConfig { @@ -55,7 +55,7 @@ public final class PowerAdvantageConfig { private PowerAdvantageConfig() {} - public static void onConfigLoading(ModConfig.Loading event) { + public static void onConfigLoading(net.minecraftforge.fml.event.config.ModConfigEvent.Loading event) { if (event.getConfig().getSpec() != SPEC) return; loaded = true; apply(); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/graphics/package-info.java b/src/main/java/zone/moddev/mc/basepower/graphics/package-info.java similarity index 70% rename from src/main/java/zone/moddev/mc/poweradvantage/graphics/package-info.java rename to src/main/java/zone/moddev/mc/basepower/graphics/package-info.java index d75d8375..29bc810f 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/graphics/package-info.java +++ b/src/main/java/zone/moddev/mc/basepower/graphics/package-info.java @@ -2,4 +2,4 @@ This package contains classes related to on-demand rendering, such as for entities and animated blocks. */ -package zone.moddev.mc.poweradvantage.graphics; \ No newline at end of file +package zone.moddev.mc.basepower.graphics; \ No newline at end of file diff --git a/src/main/java/zone/moddev/mc/poweradvantage/gui/FilteredFluidTankGUI.java b/src/main/java/zone/moddev/mc/basepower/gui/FilteredFluidTankGUI.java similarity index 82% rename from src/main/java/zone/moddev/mc/poweradvantage/gui/FilteredFluidTankGUI.java rename to src/main/java/zone/moddev/mc/basepower/gui/FilteredFluidTankGUI.java index 596a4c41..6ba10f8f 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/gui/FilteredFluidTankGUI.java +++ b/src/main/java/zone/moddev/mc/basepower/gui/FilteredFluidTankGUI.java @@ -1,10 +1,10 @@ -package zone.moddev.mc.poweradvantage.gui; +package zone.moddev.mc.basepower.gui; import cyano.poweradvantage.api.fluid.FluidPoweredEntity; import cyano.poweradvantage.api.simple.SimpleMachineGUI; -import zone.moddev.mc.poweradvantage.math.Integer2D; +import zone.moddev.mc.basepower.math.Integer2D; import com.mojang.blaze3d.platform.GlStateManager; -import net.minecraft.util.ResourceLocation; +import net.minecraft.resources.ResourceLocation; import cyano.poweradvantage.api.fluid.FluidStack; public class FilteredFluidTankGUI extends SimpleMachineGUI { @@ -27,8 +27,8 @@ public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int guiDisplayImage, guiContainer, x, y, z); } } - com.mojang.blaze3d.systems.RenderSystem.color4f(1f, 1f, 1f, 1f); // reset tint - guiContainer.getMinecraft().getTextureManager().bind(guiDisplayImage); + com.mojang.blaze3d.systems.RenderSystem.setShaderColor(1f, 1f, 1f, 1f); // reset tint + com.mojang.blaze3d.systems.RenderSystem.setShaderTexture(0, guiDisplayImage); guiContainer.blit(x + 77, y + 6, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height) } @@ -46,7 +46,7 @@ public static void drawFluidBar( FluidTankGUI.drawFluidFilledRectangle(guiContainer, fs, x + xPos, y + yPos + barSlotHeight - h, w, h, zLevel); //guiContainer.drawModalRectWithCustomSizedTexture(x+xPos, y+yPos+barSlotHeight-h, 0, 0, w, h, 16, h);//h * texPerPixel); // x, y, u, v, width, height, textureWidth, textureHeight - guiContainer.getMinecraft().getTextureManager().bind(displayImage); + com.mojang.blaze3d.systems.RenderSystem.setShaderTexture(0, displayImage); guiContainer.blit(x + xPos - 3, y + yPos - 3, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height) } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/gui/FluidTankGUI.java b/src/main/java/zone/moddev/mc/basepower/gui/FluidTankGUI.java similarity index 75% rename from src/main/java/zone/moddev/mc/poweradvantage/gui/FluidTankGUI.java rename to src/main/java/zone/moddev/mc/basepower/gui/FluidTankGUI.java index b5349af4..171ebef7 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/gui/FluidTankGUI.java +++ b/src/main/java/zone/moddev/mc/basepower/gui/FluidTankGUI.java @@ -1,16 +1,16 @@ -package zone.moddev.mc.poweradvantage.gui; +package zone.moddev.mc.basepower.gui; import cyano.poweradvantage.api.fluid.FluidPoweredEntity; import cyano.poweradvantage.api.simple.SimpleMachineGUI; -import zone.moddev.mc.poweradvantage.math.Integer2D; +import zone.moddev.mc.basepower.math.Integer2D; import com.mojang.blaze3d.platform.GlStateManager; -import net.minecraft.client.renderer.Tessellator; -import net.minecraft.client.renderer.BufferBuilder; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.BufferBuilder; import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.client.renderer.texture.AtlasTexture; -import net.minecraft.client.renderer.vertex.DefaultVertexFormats; -import net.minecraft.util.ResourceLocation; -import net.minecraft.world.biome.BiomeColors; +import net.minecraft.client.renderer.texture.TextureAtlas; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.client.renderer.BiomeColors; import cyano.poweradvantage.api.fluid.FluidStack; @@ -35,8 +35,8 @@ public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int guiDisplayImage, guiContainer, x, y, z); } } - com.mojang.blaze3d.systems.RenderSystem.color4f(1f, 1f, 1f, 1f); // reset tint - guiContainer.getMinecraft().getTextureManager().bind(guiDisplayImage); + com.mojang.blaze3d.systems.RenderSystem.setShaderColor(1f, 1f, 1f, 1f); // reset tint + com.mojang.blaze3d.systems.RenderSystem.setShaderTexture(0, guiDisplayImage); guiContainer.blit(x + 77, y + 6, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height) } @@ -53,7 +53,7 @@ public static void drawFluidBar( FluidTankGUI.drawFluidFilledRectangle(guiContainer, fs, x + xPos, y + yPos + barSlotHeight - h, w, h, zLevel); //guiContainer.drawModalRectWithCustomSizedTexture(x+xPos, y+yPos+barSlotHeight-h, 0, 0, w, h, 16, h);//h * texPerPixel); // x, y, u, v, width, height, textureWidth, textureHeight - guiContainer.getMinecraft().getTextureManager().bind(displayImage); + com.mojang.blaze3d.systems.RenderSystem.setShaderTexture(0, displayImage); guiContainer.blit(x + xPos - 3, y + yPos - 3, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height) } @@ -65,20 +65,21 @@ public static void drawFluidFilledRectangle( ) { if (fs == null || fs.getFluid() == null || width <= 0 || height <= 0) return; // The atlas supplies the current animation frame; water's sprite is grayscale. - TextureAtlasSprite sprite = guiContainer.getMinecraft().getTextureAtlas(AtlasTexture.LOCATION_BLOCKS) + TextureAtlasSprite sprite = guiContainer.getMinecraft().getTextureAtlas(TextureAtlas.LOCATION_BLOCKS) .apply(fs.getFluid().getAttributes().getStillTexture()); - guiContainer.getMinecraft().getTextureManager().bind(AtlasTexture.LOCATION_BLOCKS); - int color = fs.getFluid().getAttributes().getColor(zone.moddev.mc.poweradvantage.util.ForgeFluidAdapter.toForge(fs)); - if (fs.getFluid().isSame(net.minecraft.fluid.Fluids.WATER)) { + com.mojang.blaze3d.systems.RenderSystem.setShaderTexture(0, TextureAtlas.LOCATION_BLOCKS); + int color = fs.getFluid().getAttributes().getColor(zone.moddev.mc.basepower.util.ForgeFluidAdapter.toForge(fs)); + if (fs.getFluid().isSame(net.minecraft.world.level.material.Fluids.WATER)) { color = guiContainer.getMinecraft().level != null && guiContainer.getMinecraft().player != null ? BiomeColors.getAverageWaterColor(guiContainer.getMinecraft().level, guiContainer.getMinecraft().player.blockPosition()) : 0x3F76E4; } float red = ((color >>> 16) & 255) / 255F; float green = ((color >>> 8) & 255) / 255F; float blue = (color & 255) / 255F; - Tessellator tessellator = Tessellator.getInstance(); + Tesselator tessellator = Tesselator.getInstance(); BufferBuilder vertexbuffer = tessellator.getBuilder(); - vertexbuffer.begin(7, DefaultVertexFormats.POSITION_TEX_COLOR); + com.mojang.blaze3d.systems.RenderSystem.setShader(net.minecraft.client.renderer.GameRenderer::getPositionTexColorShader); + vertexbuffer.begin(com.mojang.blaze3d.vertex.VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR); for (int dy = 0; dy < height; dy += 16) { int tileHeight = Math.min(16, height - dy); for (int dx = 0; dx < width; dx += 16) { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/gui/FunctionalCreativeTab.java b/src/main/java/zone/moddev/mc/basepower/gui/FunctionalCreativeTab.java similarity index 87% rename from src/main/java/zone/moddev/mc/poweradvantage/gui/FunctionalCreativeTab.java rename to src/main/java/zone/moddev/mc/basepower/gui/FunctionalCreativeTab.java index 97e248d6..5963b046 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/gui/FunctionalCreativeTab.java +++ b/src/main/java/zone/moddev/mc/basepower/gui/FunctionalCreativeTab.java @@ -1,9 +1,9 @@ -package zone.moddev.mc.poweradvantage.gui; +package zone.moddev.mc.basepower.gui; -import net.minecraft.item.ItemGroup; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.util.NonNullList; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.core.NonNullList; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; /** @@ -11,7 +11,7 @@ * @author DrCyano * */ -public class FunctionalCreativeTab extends ItemGroup { +public class FunctionalCreativeTab extends CreativeModeTab { private final java.util.function.Supplier itemSupplier; private final java.util.Comparator itemSortingAlgorithm; diff --git a/src/main/java/zone/moddev/mc/poweradvantage/gui/StillGUI.java b/src/main/java/zone/moddev/mc/basepower/gui/StillGUI.java similarity index 87% rename from src/main/java/zone/moddev/mc/poweradvantage/gui/StillGUI.java rename to src/main/java/zone/moddev/mc/basepower/gui/StillGUI.java index 2fb2c96c..a3cc90b7 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/gui/StillGUI.java +++ b/src/main/java/zone/moddev/mc/basepower/gui/StillGUI.java @@ -1,9 +1,9 @@ -package zone.moddev.mc.poweradvantage.gui; +package zone.moddev.mc.basepower.gui; import cyano.poweradvantage.api.simple.SimpleMachineGUI; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.StillTileEntity; -import zone.moddev.mc.poweradvantage.math.Integer2D; -import net.minecraft.util.ResourceLocation; +import zone.moddev.mc.basepower.machines.fluidmachines.StillTileEntity; +import zone.moddev.mc.basepower.math.Integer2D; +import net.minecraft.resources.ResourceLocation; import cyano.poweradvantage.api.fluid.FluidStack; public class StillGUI extends SimpleMachineGUI { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/items/package-info.java b/src/main/java/zone/moddev/mc/basepower/gui/package-info.java similarity index 76% rename from src/main/java/zone/moddev/mc/poweradvantage/items/package-info.java rename to src/main/java/zone/moddev/mc/basepower/gui/package-info.java index 84786047..e830c8e9 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/items/package-info.java +++ b/src/main/java/zone/moddev/mc/basepower/gui/package-info.java @@ -3,10 +3,10 @@ This package contains content that is added by PowerAdvantage implementing the PowerAdvantage API. If you are writing an add-on mod, use the code in this package for reference, but you should not depend on it's implementation details as future updates might change them. For a better way to get -PowerAdvantage content, look in the zone.moddev.mc.poweradvantage.init package. +PowerAdvantage content, look in the zone.moddev.mc.basepower.init package.

Because these classes are not part of the API, they are not well documented. They were only meant for the eyes of the mod author(s).

*/ -package zone.moddev.mc.poweradvantage.items; \ No newline at end of file +package zone.moddev.mc.basepower.gui; \ No newline at end of file diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Blocks.java b/src/main/java/zone/moddev/mc/basepower/init/Blocks.java similarity index 69% rename from src/main/java/zone/moddev/mc/poweradvantage/init/Blocks.java rename to src/main/java/zone/moddev/mc/basepower/init/Blocks.java index 9be38c10..42a73f00 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Blocks.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Blocks.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.init; +package zone.moddev.mc.basepower.init; import java.util.Collections; import java.util.LinkedHashMap; @@ -7,37 +7,37 @@ import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.GUIBlock; import cyano.poweradvantage.api.fluid.InteractiveFluidBlock; -import net.minecraft.block.Block; -import net.minecraft.block.FlowingFluidBlock; -import net.minecraft.block.material.Material; -import net.minecraft.potion.Effects; -import net.minecraft.item.Item; -import net.minecraft.item.BlockItem; -import net.minecraft.potion.EffectInstance; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.material.Material; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.effect.MobEffectInstance; import net.minecraftforge.event.RegistryEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; -import zone.moddev.mc.poweradvantage.PowerAdvantage; -import zone.moddev.mc.poweradvantage.blocks.BlockFrame; -import zone.moddev.mc.poweradvantage.blocks.BlockPowerSwitch; -import zone.moddev.mc.poweradvantage.machines.conveyors.BlockConveyor; -import zone.moddev.mc.poweradvantage.machines.conveyors.BlockConveyorFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityBlockFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityFoodFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityFuelFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityInventoryFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityOreFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityOverflowFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityPlantFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntitySmeltableFilter; -import zone.moddev.mc.poweradvantage.machines.creative.InfiniteEnergyBlock; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.FluidDischargeBlock; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.FluidDrainBlock; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.FluidPipeBlock; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.MetalTankBlock; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.StillBlock; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.StorageTankBlock; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.modsupport.TerminalFluidPipeBlock; +import zone.moddev.mc.basepower.PowerAdvantage; +import zone.moddev.mc.basepower.blocks.BlockFrame; +import zone.moddev.mc.basepower.blocks.BlockPowerSwitch; +import zone.moddev.mc.basepower.machines.conveyors.BlockConveyor; +import zone.moddev.mc.basepower.machines.conveyors.BlockConveyorFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityBlockFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityFoodFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityFuelFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityInventoryFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityOreFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityOverflowFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityPlantFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntitySmeltableFilter; +import zone.moddev.mc.basepower.machines.creative.InfiniteEnergyBlock; +import zone.moddev.mc.basepower.machines.fluidmachines.FluidDischargeBlock; +import zone.moddev.mc.basepower.machines.fluidmachines.FluidDrainBlock; +import zone.moddev.mc.basepower.machines.fluidmachines.FluidPipeBlock; +import zone.moddev.mc.basepower.machines.fluidmachines.MetalTankBlock; +import zone.moddev.mc.basepower.machines.fluidmachines.StillBlock; +import zone.moddev.mc.basepower.machines.fluidmachines.StorageTankBlock; +import zone.moddev.mc.basepower.machines.fluidmachines.modsupport.TerminalFluidPipeBlock; /** Stable block and block-item registry for the 1.13 port. */ @Mod.EventBusSubscriber(modid = PowerAdvantage.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) @@ -66,8 +66,8 @@ public final class Blocks { public static GUIBlock infinite_steam; public static GUIBlock infinite_electricity; public static GUIBlock infinite_quantum; - public static FlowingFluidBlock crude_oil_block; - public static FlowingFluidBlock refined_oil_block; + public static LiquidBlock crude_oil_block; + public static LiquidBlock refined_oil_block; private static boolean initialized; @@ -105,18 +105,18 @@ public static synchronized void init() { Block.Properties oilProperties = Block.Properties.of(Material.WATER).noCollission() .strength(100.0F).dynamicShape(); - refined_oil_block = (FlowingFluidBlock) add(new InteractiveFluidBlock(Fluids.refined_oil, - oilProperties, true, (world, entity) -> entity .addEffect(new EffectInstance(Effects.CONFUSION, 200))), + 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 = (FlowingFluidBlock) add(new InteractiveFluidBlock(Fluids.crude_oil, - oilProperties, true, (world, entity) -> entity .addEffect(new EffectInstance(Effects.MOVEMENT_SLOWDOWN, 200, 2))), + 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"); ModSupport.initBlocks(); initialized = true; } - private static GUIBlock filter(String id, Class type, + private static GUIBlock filter(String id, Class type, float hardness) { return (GUIBlock) add(new BlockConveyorFilter(Material.PISTON, hardness, type), id); } @@ -139,7 +139,7 @@ public static void registerBlockItems(RegistryEvent.Register event) { for (Map.Entry entry : ALL_BLOCKS.entrySet()) { Item.Properties properties = new Item.Properties(); if (!"fluid_pipe_terminal".equals(entry.getKey()) - && !(entry.getValue() instanceof FlowingFluidBlock)) { + && !(entry.getValue() instanceof LiquidBlock)) { properties.tab(ItemGroups.tab_powerAdvantage); } BlockItem item = new BlockItem(entry.getValue(), properties); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Entities.java b/src/main/java/zone/moddev/mc/basepower/init/Entities.java similarity index 50% rename from src/main/java/zone/moddev/mc/poweradvantage/init/Entities.java rename to src/main/java/zone/moddev/mc/basepower/init/Entities.java index fc3b3727..2aa52692 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Entities.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Entities.java @@ -1,27 +1,27 @@ -package zone.moddev.mc.poweradvantage.init; +package zone.moddev.mc.basepower.init; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.tileentity.TileEntityType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraftforge.event.RegistryEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; -import zone.moddev.mc.poweradvantage.PowerAdvantage; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityBlockFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityConveyor; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityFoodFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityFuelFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityInventoryFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityOreFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityOverflowFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityPlantFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntitySmeltableFilter; -import zone.moddev.mc.poweradvantage.machines.creative.InfiniteEnergyTileEntity; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.FluidDischargeTileEntity; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.FluidDrainTileEntity; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.MetalTankTileEntity; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.StillTileEntity; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.StorageTankTileEntity; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.modsupport.TerminalFluidPipeTileEntity; +import zone.moddev.mc.basepower.PowerAdvantage; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityBlockFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityConveyor; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityFoodFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityFuelFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityInventoryFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityOreFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityOverflowFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityPlantFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntitySmeltableFilter; +import zone.moddev.mc.basepower.machines.creative.InfiniteEnergyTileEntity; +import zone.moddev.mc.basepower.machines.fluidmachines.FluidDischargeTileEntity; +import zone.moddev.mc.basepower.machines.fluidmachines.FluidDrainTileEntity; +import zone.moddev.mc.basepower.machines.fluidmachines.MetalTankTileEntity; +import zone.moddev.mc.basepower.machines.fluidmachines.StillTileEntity; +import zone.moddev.mc.basepower.machines.fluidmachines.StorageTankTileEntity; +import zone.moddev.mc.basepower.machines.fluidmachines.modsupport.TerminalFluidPipeTileEntity; import cyano.poweradvantage.api.modsupport.rf.TileEntityRFElectricityConverter; import cyano.poweradvantage.api.modsupport.rf.TileEntityRFQuantumConverter; import cyano.poweradvantage.api.modsupport.rf.TileEntityRFSteamConverter; @@ -30,31 +30,31 @@ /** Stable tile identities with the native 1.14 valid-block lists. */ @Mod.EventBusSubscriber(modid = PowerAdvantage.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) public final class Entities { - public static TileEntityType PIPE_TERMINAL; - public static TileEntityType FLUID_DRAIN; - public static TileEntityType FLUID_DISCHARGE; - public static TileEntityType STORAGE_TANK; - public static TileEntityType METAL_TANK; - public static TileEntityType STILL; - public static TileEntityType CONVEYOR; - public static TileEntityType FILTER_BLOCK; - public static TileEntityType FILTER_FOOD; - public static TileEntityType FILTER_FUEL; - public static TileEntityType FILTER_INVENTORY; - public static TileEntityType FILTER_ORE; - public static TileEntityType FILTER_PLANT; - public static TileEntityType FILTER_SMELT; - public static TileEntityType FILTER_OVERFLOW; - public static TileEntityType INFINITE_ENERGY; - public static TileEntityType RF_STEAM_CONVERTER; - public static TileEntityType RF_ELECTRICITY_CONVERTER; - public static TileEntityType RF_QUANTUM_CONVERTER; - public static TileEntityType TR_ELECTRICITY_CONVERTER; + public static BlockEntityType PIPE_TERMINAL; + public static BlockEntityType FLUID_DRAIN; + public static BlockEntityType FLUID_DISCHARGE; + public static BlockEntityType STORAGE_TANK; + public static BlockEntityType METAL_TANK; + public static BlockEntityType STILL; + public static BlockEntityType CONVEYOR; + public static BlockEntityType FILTER_BLOCK; + public static BlockEntityType FILTER_FOOD; + public static BlockEntityType FILTER_FUEL; + public static BlockEntityType FILTER_INVENTORY; + public static BlockEntityType FILTER_ORE; + public static BlockEntityType FILTER_PLANT; + public static BlockEntityType FILTER_SMELT; + public static BlockEntityType FILTER_OVERFLOW; + public static BlockEntityType INFINITE_ENERGY; + public static BlockEntityType RF_STEAM_CONVERTER; + public static BlockEntityType RF_ELECTRICITY_CONVERTER; + public static BlockEntityType RF_QUANTUM_CONVERTER; + public static BlockEntityType TR_ELECTRICITY_CONVERTER; private Entities() {} @SubscribeEvent - public static void register(RegistryEvent.Register> event) { + public static void register(RegistryEvent.Register> event) { PIPE_TERMINAL = register(event, "tileentity.pipe_terminal", TerminalFluidPipeTileEntity::new, Blocks.fluid_pipe_terminal); FLUID_DRAIN = register(event, "tileentity.fluid_drain", FluidDrainTileEntity::new, Blocks.fluid_drain); FLUID_DISCHARGE = register(event, "tileentity.fluid_discharge", FluidDischargeTileEntity::new, Blocks.fluid_discharge); @@ -78,10 +78,10 @@ public static void register(RegistryEvent.Register> event) { TR_ELECTRICITY_CONVERTER = register(event, "tr_electricity_converter_tileentity", TileEntityTRElectricityConverter::new, ModSupport.converter_tr_electricity); } - private static TileEntityType register( - RegistryEvent.Register> event, String id, - java.util.function.Supplier factory, net.minecraft.block.Block... validBlocks) { - TileEntityType type = TileEntityType.Builder.of(factory, validBlocks).build(null); + private static BlockEntityType register( + RegistryEvent.Register> event, String id, + BlockEntityType.BlockEntitySupplier factory, net.minecraft.world.level.block.Block... validBlocks) { + BlockEntityType type = BlockEntityType.Builder.of(factory, validBlocks).build(null); type.setRegistryName(PowerAdvantage.MODID, id); event.getRegistry().register(type); return type; diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Fluids.java b/src/main/java/zone/moddev/mc/basepower/init/Fluids.java similarity index 84% rename from src/main/java/zone/moddev/mc/poweradvantage/init/Fluids.java rename to src/main/java/zone/moddev/mc/basepower/init/Fluids.java index 6f54158b..76f03974 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Fluids.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Fluids.java @@ -1,22 +1,22 @@ -package zone.moddev.mc.poweradvantage.init; +package zone.moddev.mc.basepower.init; import java.util.IdentityHashMap; import java.util.Map; import cyano.poweradvantage.api.ConduitType; -import net.minecraft.block.FlowingFluidBlock; -import net.minecraft.block.BlockState; -import net.minecraft.fluid.FlowingFluid; -import net.minecraft.fluid.Fluid; -import net.minecraft.fluid.FluidState; -import net.minecraft.item.Item; -import net.minecraft.state.StateContainer; -import net.minecraft.util.Direction; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.registry.Registry; -import net.minecraft.world.IWorld; -import net.minecraft.world.IWorldReader; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FlowingFluid; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.LevelReader; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.event.RegistryEvent; @@ -24,7 +24,7 @@ import net.minecraftforge.fluids.FluidAttributes; import net.minecraftforge.fluids.ForgeFlowingFluid; import net.minecraftforge.fml.common.Mod; -import zone.moddev.mc.poweradvantage.PowerAdvantage; +import zone.moddev.mc.basepower.PowerAdvantage; /** Native Forge fluids and their persistent PowerAdvantage conduit identities. */ @Mod.EventBusSubscriber(modid = PowerAdvantage.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) @@ -45,8 +45,8 @@ private Fluids() {} public static synchronized void init() { if (initialized) return; - fluidConduit_water = registerNewFluid(net.minecraft.fluid.Fluids.WATER); - fluidConduit_lava = registerNewFluid(net.minecraft.fluid.Fluids.LAVA); + fluidConduit_water = registerNewFluid(net.minecraft.world.level.material.Fluids.WATER); + fluidConduit_lava = registerNewFluid(net.minecraft.world.level.material.Fluids.LAVA); flowing_crude_oil = new PowerFluid.Flowing(Kind.CRUDE); crude_oil = new PowerFluid.Source(Kind.CRUDE); @@ -109,7 +109,7 @@ public static Fluid byName(String name) { private static Fluid registeredFluid(ResourceLocation id) { // Forge's defaulted fluid registry returns EMPTY, not null, for unknown names. - // Do not let a missing poweradvantage:water hide the legacy vanilla fallback. + // Do not let a missing basepower:water hide the legacy vanilla fallback. return net.minecraftforge.registries.ForgeRegistries.FLUIDS.containsKey(id) ? net.minecraftforge.registries.ForgeRegistries.FLUIDS.getValue(id) : null; } @@ -132,7 +132,7 @@ private static ForgeFlowingFluid.Properties properties(Kind kind) { () -> kind == Kind.CRUDE ? flowing_crude_oil : flowing_refined_oil, FluidAttributes.builder(new ResourceLocation(PowerAdvantage.MODID, "blocks/" + name + "_still"), new ResourceLocation(PowerAdvantage.MODID, "blocks/" + name + "_flow")) - .translationKey("fluid.poweradvantage." + name).color(0xFFFFFFFF) + .translationKey("fluid.basepower." + name).color(0xFFFFFFFF) .density(kind == Kind.CRUDE ? 850 : 720) .viscosity(kind == Kind.CRUDE ? 6000 : 1000).temperature(300)) .bucket(() -> kind == Kind.CRUDE ? Items.crude_oil_bucket : Items.refined_oil_bucket) @@ -143,7 +143,7 @@ private static ForgeFlowingFluid.Properties properties(Kind kind) { public static final class Flowing extends PowerFluid { public Flowing(Kind kind) { super(kind); } - @Override protected void createFluidStateDefinition(StateContainer.Builder builder) { + @Override protected void createFluidStateDefinition(StateDefinition.Builder builder) { super.createFluidStateDefinition(builder); builder.add(LEVEL); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Fuels.java b/src/main/java/zone/moddev/mc/basepower/init/Fuels.java similarity index 70% rename from src/main/java/zone/moddev/mc/poweradvantage/init/Fuels.java rename to src/main/java/zone/moddev/mc/basepower/init/Fuels.java index 120b28fb..386ac41c 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Fuels.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Fuels.java @@ -1,7 +1,7 @@ -package zone.moddev.mc.poweradvantage.init; +package zone.moddev.mc.basepower.init; -import zone.moddev.mc.poweradvantage.registry.FuelRegistry; -import net.minecraft.item.ItemStack; +import zone.moddev.mc.basepower.registry.FuelRegistry; +import net.minecraft.world.item.ItemStack; @@ -21,9 +21,9 @@ public static void init() { FuelRegistry.getInstance().registerFuel(Items.refined_oil_bucket, (short) Math.min(Short.MAX_VALUE, 1000 * REFINED_OIL_FUEL_PER_FLUID_UNIT)); FuelRegistry.getInstance().registerPostBurnItem(Items.crude_oil_bucket, - (ItemStack stack) -> new ItemStack(net.minecraft.item.Items.BUCKET)); + (ItemStack stack) -> new ItemStack(net.minecraft.world.item.Items.BUCKET)); FuelRegistry.getInstance().registerPostBurnItem(Items.refined_oil_bucket, - (ItemStack stack) -> new ItemStack(net.minecraft.item.Items.BUCKET)); + (ItemStack stack) -> new ItemStack(net.minecraft.world.item.Items.BUCKET)); initDone = true; diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/GUI.java b/src/main/java/zone/moddev/mc/basepower/init/GUI.java similarity index 91% rename from src/main/java/zone/moddev/mc/poweradvantage/init/GUI.java rename to src/main/java/zone/moddev/mc/basepower/init/GUI.java index 3d845d7a..35abd6b0 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/GUI.java +++ b/src/main/java/zone/moddev/mc/basepower/init/GUI.java @@ -1,11 +1,11 @@ -package zone.moddev.mc.poweradvantage.init; +package zone.moddev.mc.basepower.init; -import net.minecraft.util.ResourceLocation; -import zone.moddev.mc.poweradvantage.PowerAdvantage; +import net.minecraft.resources.ResourceLocation; +import zone.moddev.mc.basepower.PowerAdvantage; import cyano.poweradvantage.api.simple.SimpleMachineGUI; -import zone.moddev.mc.poweradvantage.gui.*; -import zone.moddev.mc.poweradvantage.math.Integer2D; -import zone.moddev.mc.poweradvantage.registry.MachineGUIRegistry; +import zone.moddev.mc.basepower.gui.*; +import zone.moddev.mc.basepower.math.Integer2D; +import zone.moddev.mc.basepower.registry.MachineGUIRegistry; public abstract class GUI { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/ItemGroups.java b/src/main/java/zone/moddev/mc/basepower/init/ItemGroups.java similarity index 70% rename from src/main/java/zone/moddev/mc/poweradvantage/init/ItemGroups.java rename to src/main/java/zone/moddev/mc/basepower/init/ItemGroups.java index 66daa2ce..33a497de 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/ItemGroups.java +++ b/src/main/java/zone/moddev/mc/basepower/init/ItemGroups.java @@ -1,16 +1,16 @@ -package zone.moddev.mc.poweradvantage.init; - -import zone.moddev.mc.poweradvantage.PowerAdvantage; -import zone.moddev.mc.poweradvantage.gui.FunctionalCreativeTab; -import net.minecraft.item.ItemGroup; -import net.minecraft.item.Item; -import net.minecraft.item.BlockItem; -import net.minecraft.item.ItemStack; +package zone.moddev.mc.basepower.init; + +import zone.moddev.mc.basepower.PowerAdvantage; +import zone.moddev.mc.basepower.gui.FunctionalCreativeTab; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; import net.minecraftforge.registries.ForgeRegistries; public class ItemGroups { - public static ItemGroup tab_powerAdvantage; + public static CreativeModeTab tab_powerAdvantage; private static boolean initDone = false; @@ -42,7 +42,7 @@ private static String catagorize(ItemStack i) { Item item = i.getItem(); sb.append(ForgeRegistries.ITEMS.getKey(item).getNamespace()); if (item instanceof BlockItem) { - if (((BlockItem) item).getBlock().hasTileEntity(((BlockItem) item).getBlock().defaultBlockState())) { + if (((BlockItem) item).getBlock().defaultBlockState().hasBlockEntity()) { sb.append("A"); } else { sb.append("B"); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Items.java b/src/main/java/zone/moddev/mc/basepower/init/Items.java similarity index 85% rename from src/main/java/zone/moddev/mc/poweradvantage/init/Items.java rename to src/main/java/zone/moddev/mc/basepower/init/Items.java index 48dbcaf5..1594f683 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Items.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Items.java @@ -1,16 +1,16 @@ -package zone.moddev.mc.poweradvantage.init; +package zone.moddev.mc.basepower.init; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; -import net.minecraft.item.Item; -import net.minecraft.item.BucketItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.BucketItem; import net.minecraftforge.event.RegistryEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; -import zone.moddev.mc.poweradvantage.PowerAdvantage; -import zone.moddev.mc.poweradvantage.items.RotationTool; +import zone.moddev.mc.basepower.PowerAdvantage; +import zone.moddev.mc.basepower.items.RotationTool; /** Stable item registry for PowerAdvantage 1.13. */ @Mod.EventBusSubscriber(modid = PowerAdvantage.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) @@ -35,9 +35,9 @@ public static synchronized void init() { 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, - new Item.Properties().tab(ItemGroups.tab_powerAdvantage).stacksTo(1).craftRemainder(net.minecraft.item.Items.BUCKET))); + 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, - new Item.Properties().tab(ItemGroups.tab_powerAdvantage).stacksTo(1).craftRemainder(net.minecraft.item.Items.BUCKET))); + 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/poweradvantage/init/Materials.java b/src/main/java/zone/moddev/mc/basepower/init/Materials.java similarity index 77% rename from src/main/java/zone/moddev/mc/poweradvantage/init/Materials.java rename to src/main/java/zone/moddev/mc/basepower/init/Materials.java index 4373aac3..91af7879 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Materials.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Materials.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.init; +package zone.moddev.mc.basepower.init; public class Materials { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/ModSupport.java b/src/main/java/zone/moddev/mc/basepower/init/ModSupport.java similarity index 92% rename from src/main/java/zone/moddev/mc/poweradvantage/init/ModSupport.java rename to src/main/java/zone/moddev/mc/basepower/init/ModSupport.java index 9120d9b6..d5cb1193 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/ModSupport.java +++ b/src/main/java/zone/moddev/mc/basepower/init/ModSupport.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.init; +package zone.moddev.mc.basepower.init; import java.util.Locale; @@ -9,10 +9,10 @@ import cyano.poweradvantage.api.modsupport.rf.TileEntityRFSteamConverter; import cyano.poweradvantage.api.modsupport.techreborn.BlockTRConverter; import cyano.poweradvantage.api.modsupport.techreborn.TileEntityTRElectricityConverter; -import net.minecraft.block.Block; -import net.minecraft.block.material.Material; -import zone.moddev.mc.poweradvantage.PowerAdvantage; -import zone.moddev.mc.poweradvantage.config.PowerAdvantageConfig; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.material.Material; +import zone.moddev.mc.basepower.PowerAdvantage; +import zone.moddev.mc.basepower.config.PowerAdvantageConfig; /** Registers the converter blocks whose historical IDs are retained in 1.13. */ public final class ModSupport { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Recipes.java b/src/main/java/zone/moddev/mc/basepower/init/Recipes.java similarity index 86% rename from src/main/java/zone/moddev/mc/poweradvantage/init/Recipes.java rename to src/main/java/zone/moddev/mc/basepower/init/Recipes.java index c2e89778..dc0e094a 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Recipes.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Recipes.java @@ -1,11 +1,11 @@ -package zone.moddev.mc.poweradvantage.init; +package zone.moddev.mc.basepower.init; -import zone.moddev.mc.poweradvantage.PowerAdvantage; +import zone.moddev.mc.basepower.PowerAdvantage; import cyano.poweradvantage.api.FluidCategoryRegistry; -import net.minecraft.fluid.Fluid; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.Registry; -import zone.moddev.mc.poweradvantage.registry.still.recipe.DistillationRecipeRegistry; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.Registry; +import zone.moddev.mc.basepower.registry.still.recipe.DistillationRecipeRegistry; public abstract class Recipes { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/TreasureChests.java b/src/main/java/zone/moddev/mc/basepower/init/TreasureChests.java similarity index 86% rename from src/main/java/zone/moddev/mc/poweradvantage/init/TreasureChests.java rename to src/main/java/zone/moddev/mc/basepower/init/TreasureChests.java index 4ea3adfd..b41b6001 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/TreasureChests.java +++ b/src/main/java/zone/moddev/mc/basepower/init/TreasureChests.java @@ -1,7 +1,7 @@ -package zone.moddev.mc.poweradvantage.init; +package zone.moddev.mc.basepower.init; -import zone.moddev.mc.poweradvantage.PowerAdvantage; -import zone.moddev.mc.poweradvantage.PowerAdvantage; +import zone.moddev.mc.basepower.PowerAdvantage; +import zone.moddev.mc.basepower.PowerAdvantage; import org.apache.logging.log4j.Level; import java.io.IOException; @@ -49,7 +49,7 @@ private static void writeLootFile(Path file, String content) { " },\n" + " {\n" + " \"type\": \"item\",\n" + - " \"name\": \"poweradvantage:sprocket\",\n" + + " \"name\": \"basepower:sprocket\",\n" + " \"weight\": 5,\n" + " \"functions\": [\n" + " {\n" + @@ -63,7 +63,7 @@ private static void writeLootFile(Path file, String content) { " },\n" + " {\n" + " \"type\": \"item\",\n" + - " \"name\": \"poweradvantage:fluid_pipe\",\n" + + " \"name\": \"basepower:fluid_pipe\",\n" + " \"weight\": 5,\n" + " \"functions\": [\n" + " {\n" + @@ -77,22 +77,22 @@ private static void writeLootFile(Path file, String content) { " },\n" + " {\n" + " \"type\": \"item\",\n" + - " \"name\": \"poweradvantage:fluid_storage_tank\",\n" + + " \"name\": \"basepower:fluid_storage_tank\",\n" + " \"weight\": 3\n" + " },\n" + " {\n" + " \"type\": \"item\",\n" + - " \"name\": \"poweradvantage:fluid_drain\",\n" + + " \"name\": \"basepower:fluid_drain\",\n" + " \"weight\": 2\n" + " },\n" + " {\n" + " \"type\": \"item\",\n" + - " \"name\": \"poweradvantage:fluid_discharge\",\n" + + " \"name\": \"basepower:fluid_discharge\",\n" + " \"weight\": 2\n" + " },\n" + " {\n" + " \"type\": \"item\",\n" + - " \"name\": \"poweradvantage:item_conveyor\",\n" + + " \"name\": \"basepower:item_conveyor\",\n" + " \"weight\": 3,\n" + " \"functions\": [\n" + " {\n" + diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Villages.java b/src/main/java/zone/moddev/mc/basepower/init/Villages.java similarity index 68% rename from src/main/java/zone/moddev/mc/poweradvantage/init/Villages.java rename to src/main/java/zone/moddev/mc/basepower/init/Villages.java index ea3e71bd..de04a4ea 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Villages.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Villages.java @@ -1,8 +1,8 @@ -package zone.moddev.mc.poweradvantage.init; +package zone.moddev.mc.basepower.init; -import net.minecraft.entity.merchant.villager.VillagerProfession; -import net.minecraft.item.ItemStack; -import net.minecraft.item.MerchantOffer; +import net.minecraft.world.entity.npc.VillagerProfession; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.trading.MerchantOffer; import net.minecraftforge.event.village.VillagerTradesEvent; /** The old farmer and toolsmith careers now have their own vanilla professions. */ @@ -12,11 +12,11 @@ private Villages() {} public static void onTrades(VillagerTradesEvent event) { if (event.getType() == VillagerProfession.TOOLSMITH) { event.getTrades().get(1).add((villager, random) -> new MerchantOffer( - new ItemStack(net.minecraft.item.Items.EMERALD), + new ItemStack(net.minecraft.world.item.Items.EMERALD), new ItemStack(Items.sprocket, 1 + random.nextInt(4)), 7, 1, 0.0F)); } else if (event.getType() == VillagerProfession.FARMER) { event.getTrades().get(1).add((villager, random) -> new MerchantOffer( - new ItemStack(net.minecraft.item.Items.EMERALD), + new ItemStack(net.minecraft.world.item.Items.EMERALD), new ItemStack(Items.bioplastic_ingot, 4 + random.nextInt(5)), 7, 1, 0.0F)); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/WorldGen.java b/src/main/java/zone/moddev/mc/basepower/init/WorldGen.java similarity index 85% rename from src/main/java/zone/moddev/mc/poweradvantage/init/WorldGen.java rename to src/main/java/zone/moddev/mc/basepower/init/WorldGen.java index 77cc2cba..e25d91ce 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/WorldGen.java +++ b/src/main/java/zone/moddev/mc/basepower/init/WorldGen.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.init; +package zone.moddev.mc.basepower.init; public abstract class WorldGen { @@ -20,7 +20,7 @@ public static void init() { + " \"dimension\":\"+\",\n" + " \"ores\":[\n" + " {\n" - + " \"blockID\":\"poweradvantage:crude_oil\",\n" + + " \"blockID\":\"basepower:crude_oil\",\n" + " \"size\":64,\n" + " \"variation\":50,\n" + " \"frequency\":0.25,\n" diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/package-info.java b/src/main/java/zone/moddev/mc/basepower/init/package-info.java similarity index 77% rename from src/main/java/zone/moddev/mc/poweradvantage/init/package-info.java rename to src/main/java/zone/moddev/mc/basepower/init/package-info.java index 2711107c..28a4285d 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/package-info.java +++ b/src/main/java/zone/moddev/mc/basepower/init/package-info.java @@ -2,4 +2,4 @@ This package contains registries for all of the new content added by PowerAdvantage. Use these classes to interact with the items and blocks. */ -package zone.moddev.mc.poweradvantage.init; \ No newline at end of file +package zone.moddev.mc.basepower.init; \ No newline at end of file diff --git a/src/main/java/zone/moddev/mc/poweradvantage/items/RotationTool.java b/src/main/java/zone/moddev/mc/basepower/items/RotationTool.java similarity index 54% rename from src/main/java/zone/moddev/mc/poweradvantage/items/RotationTool.java rename to src/main/java/zone/moddev/mc/basepower/items/RotationTool.java index b5af2b4f..d727c99c 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/items/RotationTool.java +++ b/src/main/java/zone/moddev/mc/basepower/items/RotationTool.java @@ -1,26 +1,26 @@ -package zone.moddev.mc.poweradvantage.items; +package zone.moddev.mc.basepower.items; -import net.minecraft.block.BlockState; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.util.ActionResultType; -import net.minecraft.util.Direction; -import net.minecraft.util.Hand; -import net.minecraft.util.Rotation; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; -import net.minecraft.item.Item; -import net.minecraft.item.ItemUseContext; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.InteractionResult; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.context.UseOnContext; -public class RotationTool extends net.minecraft.item.Item { +public class RotationTool extends net.minecraft.world.item.Item { public RotationTool(Item.Properties properties) { super(properties); } @Override - public ActionResultType useOn(ItemUseContext context) { - final PlayerEntity player = context.getPlayer(); - final World w = context.getLevel(); + public InteractionResult useOn(UseOnContext context) { + final Player player = context.getPlayer(); + final Level w = context.getLevel(); final BlockPos coord = context.getClickedPos(); final Direction facing = context.getClickedFace(); BlockState bs = w.getBlockState(coord); @@ -41,10 +41,10 @@ public ActionResultType useOn(ItemUseContext context) { BlockState rotated = bs.rotate(w, coord, Rotation.COUNTERCLOCKWISE_90); if (rotated != bs) { w.setBlock(coord, rotated, 3); - return ActionResultType.SUCCESS; + return InteractionResult.SUCCESS; } - return ActionResultType.PASS; + return InteractionResult.PASS; } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/blocks/package-info.java b/src/main/java/zone/moddev/mc/basepower/items/package-info.java similarity index 76% rename from src/main/java/zone/moddev/mc/poweradvantage/blocks/package-info.java rename to src/main/java/zone/moddev/mc/basepower/items/package-info.java index 5af68dab..04ef1d7b 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/blocks/package-info.java +++ b/src/main/java/zone/moddev/mc/basepower/items/package-info.java @@ -3,10 +3,10 @@ This package contains content that is added by PowerAdvantage implementing the PowerAdvantage API. If you are writing an add-on mod, use the code in this package for reference, but you should not depend on it's implementation details as future updates might change them. For a better way to get -PowerAdvantage content, look in the zone.moddev.mc.poweradvantage.init package. +PowerAdvantage content, look in the zone.moddev.mc.basepower.init package.

Because these classes are not part of the API, they are not well documented. They were only meant for the eyes of the mod author(s).

*/ -package zone.moddev.mc.poweradvantage.blocks; \ No newline at end of file +package zone.moddev.mc.basepower.items; \ No newline at end of file diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyor.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/BlockConveyor.java similarity index 54% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyor.java rename to src/main/java/zone/moddev/mc/basepower/machines/conveyors/BlockConveyor.java index 667ba351..57556588 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyor.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/BlockConveyor.java @@ -1,21 +1,21 @@ -package zone.moddev.mc.poweradvantage.machines.conveyors; +package zone.moddev.mc.basepower.machines.conveyors; import cyano.poweradvantage.api.GUIBlock; -import net.minecraft.block.Block; -import net.minecraft.block.SoundType; -import net.minecraft.block.material.Material; -import net.minecraft.block.BlockState; -import net.minecraft.item.BlockItemUseContext; -import net.minecraft.state.DirectionProperty; -import net.minecraft.state.StateContainer; -import net.minecraft.state.properties.BlockStateProperties; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; -import net.minecraft.util.Mirror; -import net.minecraft.util.Rotation; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.IBlockReader; -import net.minecraft.world.World; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.material.Material; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; /** Six-facing conveyor block; legacy metadata remains the facing index. */ public class BlockConveyor extends GUIBlock { @@ -27,20 +27,20 @@ public BlockConveyor(Material material, float hardness) { registerDefaultState(getStateDefinition().any().setValue(FACING, Direction.NORTH)); } - @Override public TileEntity newBlockEntity(IBlockReader world) { return new TileEntityConveyor(); } + @Override public BlockEntity newBlockEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { return new TileEntityConveyor(pos, state); } @Override public BlockState rotate(BlockState state, Rotation rotation) { return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); } @Override public BlockState mirror(BlockState state, Mirror mirror) { return state.rotate(mirror.getRotation(state.getValue(FACING))); } - @Override public BlockState getStateForPlacement(BlockItemUseContext context) { + @Override public BlockState getStateForPlacement(BlockPlaceContext context) { return defaultBlockState().setValue(FACING, context.getClickedFace().getOpposite()); } - @Override protected void createBlockStateDefinition(StateContainer.Builder builder) { builder.add(FACING); } + @Override protected void createBlockStateDefinition(StateDefinition.Builder builder) { builder.add(FACING); } @Override public boolean isOpaqueCube(BlockState state) { return false; } public boolean isFullCube(BlockState state) { return false; } @Override public boolean hasAnalogOutputSignal(BlockState state) { return true; } @Override - public int getAnalogOutputSignal(BlockState state, World world, BlockPos pos) { - TileEntity tile = world.getBlockEntity(pos); + public int getAnalogOutputSignal(BlockState state, Level world, BlockPos pos) { + BlockEntity tile = world.getBlockEntity(pos); return tile instanceof TileEntityConveyor && !((TileEntityConveyor) tile).getItem(0).isEmpty() ? 7 : 0; } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyorFilter.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/BlockConveyorFilter.java similarity index 58% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyorFilter.java rename to src/main/java/zone/moddev/mc/basepower/machines/conveyors/BlockConveyorFilter.java index 167e1713..8d889506 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyorFilter.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/BlockConveyorFilter.java @@ -1,26 +1,26 @@ -package zone.moddev.mc.poweradvantage.machines.conveyors; +package zone.moddev.mc.basepower.machines.conveyors; import cyano.poweradvantage.api.GUIBlock; -import net.minecraft.block.Block; -import net.minecraft.block.HorizontalBlock; -import net.minecraft.block.SoundType; -import net.minecraft.block.material.Material; -import net.minecraft.block.BlockState; -import net.minecraft.item.BlockItemUseContext; -import net.minecraft.state.DirectionProperty; -import net.minecraft.state.StateContainer; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; -import net.minecraft.util.Mirror; -import net.minecraft.util.Rotation; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.IBlockReader; -import net.minecraft.world.World; -import zone.moddev.mc.poweradvantage.PowerAdvantage; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.HorizontalDirectionalBlock; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.material.Material; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import zone.moddev.mc.basepower.PowerAdvantage; /** Horizontal conveyor filter block retaining its legacy metadata codec. */ public class BlockConveyorFilter extends GUIBlock { - public static final DirectionProperty FACING = HorizontalBlock.FACING; + public static final DirectionProperty FACING = HorizontalDirectionalBlock.FACING; private final Class tileEntityClass; public BlockConveyorFilter(Material material, float hardness, @@ -32,9 +32,9 @@ public BlockConveyorFilter(Material material, float hardness, } @Override - public TileEntity newBlockEntity(IBlockReader world) { + public BlockEntity newBlockEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { try { - return tileEntityClass.newInstance(); + return tileEntityClass.getConstructor(BlockPos.class, BlockState.class).newInstance(pos, state); } catch (ReflectiveOperationException exception) { PowerAdvantage.LOGGER.error("Could not create conveyor filter {}", tileEntityClass.getName(), exception); return null; @@ -43,17 +43,17 @@ public TileEntity newBlockEntity(IBlockReader world) { @Override public BlockState rotate(BlockState state, Rotation rotation) { return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); } @Override public BlockState mirror(BlockState state, Mirror mirror) { return state.rotate(mirror.getRotation(state.getValue(FACING))); } - @Override public BlockState getStateForPlacement(BlockItemUseContext context) { + @Override public BlockState getStateForPlacement(BlockPlaceContext context) { return defaultBlockState().setValue(FACING, context.getHorizontalDirection()); } - @Override protected void createBlockStateDefinition(StateContainer.Builder builder) { builder.add(FACING); } + @Override protected void createBlockStateDefinition(StateDefinition.Builder builder) { builder.add(FACING); } @Override public boolean isOpaqueCube(BlockState state) { return false; } public boolean isFullCube(BlockState state) { return false; } @Override public boolean hasAnalogOutputSignal(BlockState state) { return true; } @Override - public int getAnalogOutputSignal(BlockState state, World world, BlockPos pos) { - TileEntity tile = world.getBlockEntity(pos); + public int getAnalogOutputSignal(BlockState state, Level world, BlockPos pos) { + BlockEntity tile = world.getBlockEntity(pos); if (!(tile instanceof TileEntityConveyorFilter)) return 0; TileEntityConveyorFilter filter = (TileEntityConveyorFilter) tile; if (filter.getItem(0).isEmpty()) return 0; diff --git a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityBlockFilter.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityBlockFilter.java new file mode 100644 index 00000000..06a2055b --- /dev/null +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityBlockFilter.java @@ -0,0 +1,16 @@ +package zone.moddev.mc.basepower.machines.conveyors; + +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import zone.moddev.mc.basepower.init.Entities; + +public class TileEntityBlockFilter extends TileEntityConveyorFilter { + public TileEntityBlockFilter(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { super(Entities.FILTER_BLOCK, pos, state, 1); } + + @Override + public boolean matchesFilter(ItemStack item) { + if (item == null) return false; + return item.getItem() instanceof BlockItem; + } + +} diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyor.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyor.java similarity index 78% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyor.java rename to src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyor.java index 9531bc4c..a78a65f7 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyor.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyor.java @@ -1,37 +1,36 @@ -package zone.moddev.mc.poweradvantage.machines.conveyors; - -import zone.moddev.mc.poweradvantage.util.InventoryWrapper; -import net.minecraft.block.BlockState; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.ISidedInventory; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.tileentity.HopperTileEntity; -import net.minecraft.tileentity.TileEntityType; -import net.minecraft.util.Direction; -import net.minecraft.tileentity.ITickableTileEntity; -import net.minecraft.util.math.AxisAlignedBB; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.text.TextComponent; -import net.minecraft.util.text.StringTextComponent; -import net.minecraft.util.text.TranslationTextComponent; -import net.minecraft.util.text.ITextComponent; -import net.minecraft.inventory.container.INamedContainerProvider; -import net.minecraft.world.World; -import zone.moddev.mc.poweradvantage.PowerAdvantage; +package zone.moddev.mc.basepower.machines.conveyors; + +import zone.moddev.mc.basepower.util.InventoryWrapper; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.Container; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.HopperBlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.core.Direction; +import net.minecraft.world.phys.AABB; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.BaseComponent; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.network.chat.Component; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.level.Level; +import zone.moddev.mc.basepower.PowerAdvantage; import java.util.Arrays; import java.util.List; import java.util.Random; -import zone.moddev.mc.poweradvantage.init.Entities; +import zone.moddev.mc.basepower.init.Entities; -public class TileEntityConveyor extends TileEntity implements ITickableTileEntity, ISidedInventory, net.minecraft.util.INameable { - private final zone.moddev.mc.poweradvantage.util.MachineCapabilities machineCapabilities = - new zone.moddev.mc.poweradvantage.util.MachineCapabilities(this); +public class TileEntityConveyor extends BlockEntity implements WorldlyContainer, net.minecraft.world.Nameable { + private final zone.moddev.mc.basepower.util.MachineCapabilities machineCapabilities = + new zone.moddev.mc.basepower.util.MachineCapabilities(this); @Override public net.minecraftforge.common.util.LazyOptional getCapability( net.minecraftforge.common.capabilities.Capability capability, Direction face) { @@ -42,12 +41,12 @@ public class TileEntityConveyor extends TileEntity implements ITickableTileEntit return local.isPresent() ? local : super.getCapability(capability, face); } - @Override protected void invalidateCaps() { + @Override public void invalidateCaps() { if (machineCapabilities != null) machineCapabilities.invalidate(); super.invalidateCaps(); } - @Override protected void reviveCaps() { + @Override public void reviveCaps() { super.reviveCaps(); if (machineCapabilities != null) machineCapabilities.revive(); } @@ -67,19 +66,18 @@ public class TileEntityConveyor extends TileEntity implements ITickableTileEntit protected final int transferInvterval = 7; - public TileEntityConveyor(){ - this(Entities.CONVEYOR, 1); + public TileEntityConveyor(BlockPos pos, BlockState state){ + this(Entities.CONVEYOR, pos, state, 1); } - protected TileEntityConveyor(TileEntityType type, int inventorySize){ - super(type); + protected TileEntityConveyor(BlockEntityType type, BlockPos pos, BlockState state, int inventorySize){ + super(type, pos, state); inventory = new ItemStack[inventorySize]; } - @Override public void tick() { - World w = getLevel(); + Level w = getLevel(); if(w.isClientSide) return; if(transferCooldown > 0) transferCooldown--; if(transferCooldown == 0){ @@ -90,10 +88,10 @@ public void tick() { Direction myDir = dir; Direction theirDir = dir.getOpposite(); BlockPos targetPos = getBlockPos().relative(myDir); - TileEntity target = w.getBlockEntity(targetPos); + BlockEntity target = w.getBlockEntity(targetPos); if(target != null) { - if( target instanceof IInventory){ - ISidedInventory them = InventoryWrapper.wrap(HopperTileEntity.getContainerAt(getLevel(),targetPos.getX(), targetPos.getY(), targetPos.getZ())); + if( target instanceof Container){ + WorldlyContainer them = InventoryWrapper.wrap(HopperBlockEntity.getContainerAt(getLevel(), targetPos)); if(transferItem(this,myDir,them,theirDir)){ transferCooldown = transferInvterval; @@ -108,7 +106,7 @@ public void tick() { BlockPos upstreamBlock = getBlockPos().relative(myDir); if(w.isEmptyBlock(upstreamBlock)){ // grab items sitting on the ground behind the conveyor - List list = w.getEntitiesOfClass(ItemEntity.class, new AxisAlignedBB( + List list = w.getEntitiesOfClass(ItemEntity.class, new AABB( upstreamBlock.getX(), upstreamBlock.getY(), upstreamBlock.getZ(), upstreamBlock.getX()+1, upstreamBlock.getY()+1, upstreamBlock.getZ()+1)); if(!list.isEmpty()){ @@ -125,7 +123,7 @@ public void tick() { getInventory()[0].grow(1); } if(entityStack.isEmpty()){ - e.remove(); + e.discard(); } else { e.setItem(entityStack); } @@ -136,8 +134,8 @@ public void tick() { } else { target = w.getBlockEntity(upstreamBlock); if(target != null){ - if( target instanceof IInventory){ - ISidedInventory them = InventoryWrapper.wrap(HopperTileEntity.getContainerAt(getLevel(),upstreamBlock.getX(), upstreamBlock.getY(), upstreamBlock.getZ())); + if( target instanceof Container){ + WorldlyContainer them = InventoryWrapper.wrap(HopperBlockEntity.getContainerAt(getLevel(), upstreamBlock)); if(transferItem(them,theirDir,this,myDir)){ this.setChanged(); } @@ -149,11 +147,11 @@ public void tick() { } protected static boolean isLocked(Object o){ - return zone.moddev.mc.poweradvantage.util.InventoryLocks.isLocked(o); + return zone.moddev.mc.basepower.util.InventoryLocks.isLocked(o); } - protected static boolean canInsertItemInto(ItemStack item, ISidedInventory dest, Direction destFace){ + protected static boolean canInsertItemInto(ItemStack item, WorldlyContainer dest, Direction destFace){ if(item == null || item.isEmpty() || item.getItem() == null || isLocked(dest)){ return false; } @@ -173,7 +171,7 @@ protected static boolean canInsertItemInto(ItemStack item, ISidedInventory dest, return false; } - protected static boolean transferItem(ISidedInventory src, Direction srcFace, ISidedInventory dest, Direction destFace){ + protected static boolean transferItem(WorldlyContainer src, Direction srcFace, WorldlyContainer dest, Direction destFace){ if(isLocked(src) || isLocked(dest)){ return false; } @@ -245,14 +243,14 @@ protected ItemStack[] getInventory(){ * @param tagRoot The root of the NBT */ @Override - public void load(net.minecraft.block.BlockState state, final CompoundNBT tagRoot) { - super.load(state, tagRoot); + public void load(final CompoundTag tagRoot) { + super.load(tagRoot); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ Arrays.fill(inventory, null); - final ListNBT nbttaglist = tagRoot.getList("Items", 10); + final ListTag nbttaglist = tagRoot.getList("Items", 10); for (int i = 0; i < nbttaglist.size() && i < inventory.length; ++i) { - final CompoundNBT nbttagcompound1 = nbttaglist .getCompound(i); + final CompoundTag nbttagcompound1 = nbttaglist .getCompound(i); final byte n = nbttagcompound1.getByte("Slot"); if (n >= 0 && n < inventory.length) { ItemStack stack = ItemStack.of(nbttagcompound1); @@ -272,14 +270,14 @@ public void load(net.minecraft.block.BlockState state, final CompoundNBT tagRoot * @param tagRoot The root of the NBT */ @Override - public CompoundNBT save(final CompoundNBT tagRoot) { + public CompoundTag save(final CompoundTag tagRoot) { super .save(tagRoot); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ - final ListNBT nbttaglist = new ListNBT(); + final ListTag nbttaglist = new ListTag(); for (int i = 0; i < inventory.length; ++i) { if (inventory[i] != null && !inventory[i].isEmpty()) { - final CompoundNBT nbttagcompound1 = new CompoundNBT(); + final CompoundTag nbttagcompound1 = new CompoundTag(); nbttagcompound1.putByte("Slot", (byte)i); inventory[i] .save(nbttagcompound1); nbttaglist .add(nbttagcompound1); @@ -301,7 +299,7 @@ public CompoundNBT save(final CompoundNBT tagRoot) { * @param newSate State after change * @return true if this TileEntity should be invalidated (deleted and recreated), false otherwise */ - public boolean shouldRefresh(World world, BlockPos pos, BlockState oldState, BlockState newSate) { + public boolean shouldRefresh(Level world, BlockPos pos, BlockState oldState, BlockState newSate) { return oldState.getBlock() != newSate.getBlock(); } @@ -320,7 +318,7 @@ public boolean isEmpty() { } @Override - public void stopOpen(PlayerEntity p) { + public void stopOpen(Player p) { // do nothing } @@ -380,7 +378,7 @@ public boolean canPlaceItem(final int slot, final ItemStack item) { } @Override - public boolean stillValid(final PlayerEntity p_isUseableByPlayer_1_) { + public boolean stillValid(final Player p_isUseableByPlayer_1_) { return this.level.getBlockEntity(this.worldPosition) == this && p_isUseableByPlayer_1_.distanceToSqr( (double)this.worldPosition.getX() + 0.5, @@ -389,7 +387,7 @@ public boolean stillValid(final PlayerEntity p_isUseableByPlayer_1_) { } @Override - public void startOpen(PlayerEntity arg0) { + public void startOpen(Player arg0) { // do nothing } @@ -412,8 +410,8 @@ public void setItem(int slot, ItemStack item) { } @Override - public TextComponent getDisplayName() { - return (TextComponent) getName(); + public BaseComponent getDisplayName() { + return (BaseComponent) getName(); } private String customName = null; @@ -423,14 +421,14 @@ public TextComponent getDisplayName() { * given by the player (i.e. with an anvil) */ @Override - public ITextComponent getName() { - return this.hasCustomName() ? new StringTextComponent(this.customName) - : new TranslationTextComponent(this.getDescriptionId()); + public Component getName() { + return this.hasCustomName() ? new TextComponent(this.customName) + : new TranslatableComponent(this.getDescriptionId()); } @Override - public ITextComponent getCustomName() { - return this.hasCustomName() ? new StringTextComponent(this.customName) : null; + public Component getCustomName() { + return this.hasCustomName() ? new TextComponent(this.customName) : null; } /** diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyorFilter.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyorFilter.java similarity index 63% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyorFilter.java rename to src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyorFilter.java index ee5a4d78..c9f7a13f 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyorFilter.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyorFilter.java @@ -1,35 +1,31 @@ -package zone.moddev.mc.poweradvantage.machines.conveyors; +package zone.moddev.mc.basepower.machines.conveyors; -import zone.moddev.mc.poweradvantage.util.InventoryWrapper; -import net.minecraft.block.BlockState; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.ISidedInventory; -import net.minecraft.item.ItemStack; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.tileentity.HopperTileEntity; -import net.minecraft.tileentity.TileEntityType; -import net.minecraft.util.Direction; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; -import zone.moddev.mc.poweradvantage.PowerAdvantage; -import zone.moddev.mc.poweradvantage.init.Entities; +import zone.moddev.mc.basepower.util.InventoryWrapper; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.Container; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.HopperBlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import zone.moddev.mc.basepower.PowerAdvantage; +import zone.moddev.mc.basepower.init.Entities; public abstract class TileEntityConveyorFilter extends TileEntityConveyor { - public TileEntityConveyorFilter() { - this(Entities.FILTER_BLOCK, 1); - } - - protected TileEntityConveyorFilter(TileEntityType type, int slots) { - super(type, slots); + protected TileEntityConveyorFilter(BlockEntityType type, BlockPos pos, BlockState state, int slots) { + super(type, pos, state, slots); } public abstract boolean matchesFilter(ItemStack item); @Override public void tick() { - World w = getLevel(); + Level w = getLevel(); if (w.isClientSide) return; if (transferCooldown > 0) transferCooldown--; if (transferCooldown == 0) { @@ -40,10 +36,10 @@ public void tick() { Direction myDir = dir.getOpposite(); Direction theirDir = dir; BlockPos targetPos = getBlockPos().relative(myDir); - TileEntity target = w.getBlockEntity(targetPos); + BlockEntity target = w.getBlockEntity(targetPos); if (target != null) { - if (target instanceof IInventory) { - ISidedInventory them = InventoryWrapper.wrap(HopperTileEntity.getContainerAt(getLevel(), targetPos.getX(), targetPos.getY(), targetPos.getZ())); + if (target instanceof Container) { + WorldlyContainer them = InventoryWrapper.wrap(HopperBlockEntity.getContainerAt(getLevel(), targetPos)); if (transferItem(them, theirDir, this, myDir)) { this.setChanged(); } @@ -53,15 +49,15 @@ public void tick() { Direction myDir = dir; Direction theirDir = dir.getOpposite(); BlockPos targetPos = getBlockPos().relative(myDir); - TileEntity target = w.getBlockEntity(targetPos); - TileEntity dropTarget = w.getBlockEntity(getBlockPos().relative(Direction.DOWN)); + BlockEntity target = w.getBlockEntity(targetPos); + BlockEntity dropTarget = w.getBlockEntity(getBlockPos().relative(Direction.DOWN)); if (matchesFilter(this.getInventory()[0])) { if (isValidItemFor(getInventory()[0], dropTarget, Direction.UP)) { dropItem(dropTarget); return; } - } else if (target instanceof IInventory) { - ISidedInventory them = InventoryWrapper.wrap(HopperTileEntity.getContainerAt(getLevel(), targetPos.getX(), targetPos.getY(), targetPos.getZ())); + } else if (target instanceof Container) { + WorldlyContainer them = InventoryWrapper.wrap(HopperBlockEntity.getContainerAt(getLevel(), targetPos)); if (transferItem(this, myDir, them, theirDir)) { this.setChanged(); } @@ -72,11 +68,11 @@ public void tick() { } } - protected boolean isValidItemFor(ItemStack item, TileEntity target, Direction side) { + protected boolean isValidItemFor(ItemStack item, BlockEntity target, Direction side) { if (item == null) return false; - if (target instanceof IInventory) { + if (target instanceof Container) { BlockPos targetPos = target.getBlockPos(); - ISidedInventory dt = InventoryWrapper.wrap(HopperTileEntity.getContainerAt(getLevel(), targetPos.getX(), targetPos.getY(), targetPos.getZ())); + WorldlyContainer dt = InventoryWrapper.wrap(HopperBlockEntity.getContainerAt(getLevel(), targetPos)); int[] slots = dt.getSlotsForFace(side); for (int i = 0; i < slots.length; i++) { int slot = slots[i]; @@ -90,8 +86,8 @@ protected boolean isValidItemFor(ItemStack item, TileEntity target, Direction si } } - private void dropItem(TileEntity target) { - World w = getLevel(); + private void dropItem(BlockEntity target) { + Level w = getLevel(); BlockState bs = w.getBlockState(getBlockPos().below()); if (target == null && !(bs.getMaterial().blocksMotion())) { // drop item in the air @@ -101,12 +97,12 @@ private void dropItem(TileEntity target) { getInventory()[0] = null; transferCooldown = transferInvterval; this.setChanged(); - } else if (target instanceof IInventory) { + } else if (target instanceof Container) { // add item to inventory Direction myDir = Direction.DOWN; Direction theirDir = Direction.UP; BlockPos targetPos = target.getBlockPos(); - ISidedInventory targetInv = InventoryWrapper.wrap(HopperTileEntity.getContainerAt(getLevel(), targetPos.getX(), targetPos.getY(), targetPos.getZ())); + WorldlyContainer targetInv = InventoryWrapper.wrap(HopperBlockEntity.getContainerAt(getLevel(), targetPos)); if (transferItem(this, myDir, targetInv, theirDir)) { transferCooldown = transferInvterval; this.setChanged(); diff --git a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityFoodFilter.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityFoodFilter.java new file mode 100644 index 00000000..902595e8 --- /dev/null +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityFoodFilter.java @@ -0,0 +1,15 @@ +package zone.moddev.mc.basepower.machines.conveyors; + +import net.minecraft.world.item.ItemStack; +import zone.moddev.mc.basepower.init.Entities; + +public class TileEntityFoodFilter extends TileEntityConveyorFilter { + public TileEntityFoodFilter(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { super(Entities.FILTER_FOOD, pos, state, 1); } + + @Override + public boolean matchesFilter(ItemStack item) { + if (item == null) return false; + return !item.isEmpty() && item.getItem().isEdible(); + } + +} diff --git a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityFuelFilter.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityFuelFilter.java new file mode 100644 index 00000000..76d9d8c4 --- /dev/null +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityFuelFilter.java @@ -0,0 +1,17 @@ +package zone.moddev.mc.basepower.machines.conveyors; + + +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; +import zone.moddev.mc.basepower.init.Entities; + +public class TileEntityFuelFilter extends TileEntityConveyorFilter { + public TileEntityFuelFilter(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { super(Entities.FILTER_FUEL, pos, state, 1); } + + @Override + public boolean matchesFilter(ItemStack item) { + if (item == null) return false; + return AbstractFurnaceBlockEntity.isFuel(item); + } + +} diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityInventoryFilter.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityInventoryFilter.java similarity index 55% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityInventoryFilter.java rename to src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityInventoryFilter.java index 395cc606..04e4fefa 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityInventoryFilter.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityInventoryFilter.java @@ -1,15 +1,15 @@ -package zone.moddev.mc.poweradvantage.machines.conveyors; +package zone.moddev.mc.basepower.machines.conveyors; -import net.minecraft.item.ItemStack; -import zone.moddev.mc.poweradvantage.init.Entities; +import net.minecraft.world.item.ItemStack; +import zone.moddev.mc.basepower.init.Entities; public class TileEntityInventoryFilter extends TileEntityConveyorFilter { public static final int NUM_FILTER_SLOTS = 9; - public TileEntityInventoryFilter() { - super(Entities.FILTER_INVENTORY, NUM_FILTER_SLOTS + 1); + public TileEntityInventoryFilter(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { + super(Entities.FILTER_INVENTORY, pos, state, NUM_FILTER_SLOTS + 1); } @Override diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOreFilter.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityOreFilter.java similarity index 58% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOreFilter.java rename to src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityOreFilter.java index 4a2bd89c..9260e67c 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOreFilter.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityOreFilter.java @@ -1,11 +1,11 @@ -package zone.moddev.mc.poweradvantage.machines.conveyors; +package zone.moddev.mc.basepower.machines.conveyors; -import net.minecraft.item.ItemStack; +import net.minecraft.world.item.ItemStack; import net.minecraftforge.common.Tags; -import zone.moddev.mc.poweradvantage.init.Entities; +import zone.moddev.mc.basepower.init.Entities; public class TileEntityOreFilter extends TileEntityConveyorFilter { - public TileEntityOreFilter() { super(Entities.FILTER_ORE, 1); } + public TileEntityOreFilter(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { super(Entities.FILTER_ORE, pos, state, 1); } @Override public boolean matchesFilter(ItemStack item) { diff --git a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityOverflowFilter.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityOverflowFilter.java new file mode 100644 index 00000000..96f2060d --- /dev/null +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityOverflowFilter.java @@ -0,0 +1,40 @@ +package zone.moddev.mc.basepower.machines.conveyors; + +import zone.moddev.mc.basepower.util.InventoryWrapper; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; +import zone.moddev.mc.basepower.init.Entities; + +public class TileEntityOverflowFilter extends TileEntityConveyorFilter { + public TileEntityOverflowFilter(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { super(Entities.FILTER_OVERFLOW, pos, state, 1); } + + @Override + public boolean matchesFilter(ItemStack item) { + if (item == null || item.getItem() == null) return false; + boolean canDrop = canInsert(item, Direction.DOWN); + boolean canPass = canInsert(item, this.getFacing()); + if (canPass) { + return canDrop; + } else { + return true; + } + } + + private boolean canInsert(ItemStack item, Direction f) { + BlockEntity target = getLevel().getBlockEntity(getBlockPos().relative(f)); + if (target instanceof Container) { + return canInsertItemInto(item, InventoryWrapper.wrap((Container) target), f.getOpposite()); + } else { + return false; + } + } + + @Override + protected boolean isValidItemFor(ItemStack item, BlockEntity target, Direction side) { + // do all of the logic in the matchesFilter method + return true; + } + +} diff --git a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityPlantFilter.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityPlantFilter.java new file mode 100644 index 00000000..1e573305 --- /dev/null +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityPlantFilter.java @@ -0,0 +1,26 @@ +package zone.moddev.mc.basepower.machines.conveyors; + +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.BonemealableBlock; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.common.IPlantable; +import zone.moddev.mc.basepower.init.Entities; + +public class TileEntityPlantFilter extends TileEntityConveyorFilter { + public TileEntityPlantFilter(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { super(Entities.FILTER_PLANT, pos, state, 1); } + + @Override + public boolean matchesFilter(ItemStack item) { + if (item == null) return false; + if (item.getItem() instanceof IPlantable) return true; + if (item.getItem() instanceof BlockItem) { + Block b = ((BlockItem) item.getItem()).getBlock(); + if (b instanceof IPlantable) return true; + if (b instanceof BonemealableBlock) return true; + + } + return false; + } + +} diff --git a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntitySmeltableFilter.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntitySmeltableFilter.java new file mode 100644 index 00000000..f542ceda --- /dev/null +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntitySmeltableFilter.java @@ -0,0 +1,20 @@ +package zone.moddev.mc.basepower.machines.conveyors; + +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.world.item.crafting.RecipeType; +import zone.moddev.mc.basepower.init.Entities; + +public class TileEntitySmeltableFilter extends TileEntityConveyorFilter { + public TileEntitySmeltableFilter(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { super(Entities.FILTER_SMELT, pos, state, 1); } + + @Override + public boolean matchesFilter(ItemStack item) { + if (item == null || item.isEmpty() || level == null) return false; + SimpleContainer probe = new SimpleContainer(1); + probe.setItem(0, item.copy()); + return level.getRecipeManager().getRecipeFor(RecipeType.SMELTING, probe, level).isPresent(); + } + +} diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/package-info.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/package-info.java similarity index 75% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/package-info.java rename to src/main/java/zone/moddev/mc/basepower/machines/conveyors/package-info.java index 23e6fb8a..f1881ac9 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/package-info.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/package-info.java @@ -3,10 +3,10 @@ This package contains content that is added by PowerAdvantage implementing the PowerAdvantage API. If you are writing an add-on mod, use the code in this package for reference, but you should not depend on it's implementation details as future updates might change them. For a better way to get -PowerAdvantage content, look in the zone.moddev.mc.poweradvantage.init package. +PowerAdvantage content, look in the zone.moddev.mc.basepower.init package.

Because these classes are not part of the API, they are not well documented. They were only meant for the eyes of the mod author(s).

*/ -package zone.moddev.mc.poweradvantage.machines.conveyors; \ No newline at end of file +package zone.moddev.mc.basepower.machines.conveyors; \ No newline at end of file diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyBlock.java b/src/main/java/zone/moddev/mc/basepower/machines/creative/InfiniteEnergyBlock.java similarity index 66% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyBlock.java rename to src/main/java/zone/moddev/mc/basepower/machines/creative/InfiniteEnergyBlock.java index b2beb359..72c8e587 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyBlock.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/creative/InfiniteEnergyBlock.java @@ -1,13 +1,13 @@ -package zone.moddev.mc.poweradvantage.machines.creative; +package zone.moddev.mc.basepower.machines.creative; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PoweredEntity; import cyano.poweradvantage.api.simple.BlockSimplePowerMachine; -import net.minecraft.block.material.Material; -import net.minecraft.block.BlockState; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; -import net.minecraft.world.IBlockReader; +import net.minecraft.world.level.material.Material; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.BlockGetter; public class InfiniteEnergyBlock extends BlockSimplePowerMachine { @@ -16,8 +16,8 @@ public InfiniteEnergyBlock(ConduitType energyType) { } @Override - public PoweredEntity newBlockEntity(IBlockReader world) { - return new InfiniteEnergyTileEntity(this.getTypes()[0]); + public PoweredEntity newBlockEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { + return new InfiniteEnergyTileEntity(pos, state, this.getTypes()[0]); } @Override @@ -26,7 +26,7 @@ public boolean hasAnalogOutputSignal(BlockState bs) { } @Override - public int getAnalogOutputSignal(BlockState bs, World world, BlockPos coord) { + public int getAnalogOutputSignal(BlockState bs, Level world, BlockPos coord) { return 0; } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyTileEntity.java b/src/main/java/zone/moddev/mc/basepower/machines/creative/InfiniteEnergyTileEntity.java similarity index 70% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyTileEntity.java rename to src/main/java/zone/moddev/mc/basepower/machines/creative/InfiniteEnergyTileEntity.java index 3136a75e..0f24b004 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyTileEntity.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/creative/InfiniteEnergyTileEntity.java @@ -1,10 +1,10 @@ -package zone.moddev.mc.poweradvantage.machines.creative; +package zone.moddev.mc.basepower.machines.creative; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.simple.TileEntitySimplePowerMachine; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompoundNBT; -import zone.moddev.mc.poweradvantage.init.Entities; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import zone.moddev.mc.basepower.init.Entities; public class InfiniteEnergyTileEntity extends TileEntitySimplePowerMachine { @@ -12,13 +12,13 @@ public class InfiniteEnergyTileEntity extends TileEntitySimplePowerMachine { private final int[] dataArray = new int[0]; private final ConduitType[] type = new ConduitType[1]; - public InfiniteEnergyTileEntity(ConduitType energyType) { - super(Entities.INFINITE_ENERGY, energyType, 1000f, "tile.poweradvantage.infinite_energy_source.name"); + public InfiniteEnergyTileEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state, ConduitType energyType) { + super(Entities.INFINITE_ENERGY, pos, state, energyType, 1000f, "tile.basepower.infinite_energy_source.name"); this.type[0] = energyType; } - public InfiniteEnergyTileEntity() { - super(Entities.INFINITE_ENERGY, new ConduitType("power"), 1000f, "tile.poweradvantage.infinite_energy_source.name"); + public InfiniteEnergyTileEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { + super(Entities.INFINITE_ENERGY, pos, state, new ConduitType("power"), 1000f, "tile.basepower.infinite_energy_source.name"); this.type[0] = new ConduitType("power"); } @@ -89,15 +89,15 @@ public void powerUpdate() { } @Override - public CompoundNBT save(final CompoundNBT tagRoot) { + public CompoundTag save(final CompoundTag tagRoot) { super .save(tagRoot); tagRoot.putString("InfType", this.type[0].toString()); return tagRoot; } @Override - public void load(net.minecraft.block.BlockState state, final CompoundNBT tagRoot) { - super.load(state, tagRoot); + public void load(final CompoundTag tagRoot) { + super.load(tagRoot); if (tagRoot.contains("InfType", 8)) { this.type[0] = new ConduitType(tagRoot.getString("InfType")); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeBlock.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDischargeBlock.java similarity index 64% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeBlock.java rename to src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDischargeBlock.java index 85b1b4da..896d75f0 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeBlock.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDischargeBlock.java @@ -1,17 +1,17 @@ -package zone.moddev.mc.poweradvantage.machines.fluidmachines; +package zone.moddev.mc.basepower.machines.fluidmachines; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PoweredEntity; import cyano.poweradvantage.api.simple.BlockSimpleFluidMachine; import cyano.poweradvantage.api.simple.TileEntitySimplePowerMachine; -import net.minecraft.block.material.Material; -import net.minecraft.block.BlockState; -import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.InventoryHelper; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.IBlockReader; -import net.minecraft.world.World; +import net.minecraft.world.level.material.Material; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.Container; +import net.minecraft.world.Containers; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; public class FluidDischargeBlock extends BlockSimpleFluidMachine { @@ -20,8 +20,8 @@ public FluidDischargeBlock() { } @Override - public PoweredEntity newBlockEntity(IBlockReader w) { - return new FluidDischargeTileEntity(); + public PoweredEntity newBlockEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { + return new FluidDischargeTileEntity(pos, state); } /** @@ -29,11 +29,11 @@ public PoweredEntity newBlockEntity(IBlockReader w) { * breaks. */ @Override - public void onRemove(BlockState state, World world, BlockPos coord, BlockState newState, boolean isMoving) { + public void onRemove(BlockState state, Level world, BlockPos coord, BlockState newState, boolean isMoving) { if (state.getBlock() != newState.getBlock()) { - final TileEntity tileEntity = world.getBlockEntity(coord); + final BlockEntity tileEntity = world.getBlockEntity(coord); if (tileEntity instanceof TileEntitySimplePowerMachine) { - InventoryHelper.dropContents(world, coord, (IInventory) tileEntity); + Containers.dropContents(world, coord, (Container) tileEntity); world .updateNeighbourForOutputSignal(coord, this); } } @@ -46,8 +46,8 @@ public boolean hasAnalogOutputSignal(BlockState state) { } @Override - public int getAnalogOutputSignal(BlockState state, World world, BlockPos coord) { - TileEntity te = world.getBlockEntity(coord); + public int getAnalogOutputSignal(BlockState state, Level world, BlockPos coord) { + BlockEntity te = world.getBlockEntity(coord); if (te != null && te instanceof FluidDischargeTileEntity) { return ((FluidDischargeTileEntity) te).getRedstoneOutput(); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeTileEntity.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDischargeTileEntity.java similarity index 69% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeTileEntity.java rename to src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDischargeTileEntity.java index 7c2d4b7f..c79f6fa4 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeTileEntity.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDischargeTileEntity.java @@ -1,28 +1,28 @@ -package zone.moddev.mc.poweradvantage.machines.fluidmachines; +package zone.moddev.mc.basepower.machines.fluidmachines; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.FluidTank; import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; import cyano.poweradvantage.api.simple.TileEntitySimpleFluidMachine; -import net.minecraft.fluid.Fluid; -import net.minecraft.fluid.FluidState; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.network.NetworkManager; -import net.minecraft.network.play.server.SUpdateTileEntityPacket; -import net.minecraft.util.Direction; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; -import zone.moddev.mc.poweradvantage.init.Entities; -import zone.moddev.mc.poweradvantage.util.FluidHandlerHelper; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import zone.moddev.mc.basepower.init.Entities; +import zone.moddev.mc.basepower.util.FluidHandlerHelper; /** Places stored fluid into the world or into a tank below. */ public class FluidDischargeTileEntity extends TileEntitySimpleFluidMachine { private int oldLevel = -1; - public FluidDischargeTileEntity() { - super(Entities.FLUID_DISCHARGE, FluidTank.BUCKET_VOLUME, "tile.poweradvantage.fluid_discharge.name"); + public FluidDischargeTileEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { + super(Entities.FLUID_DISCHARGE, pos, state, FluidTank.BUCKET_VOLUME, "tile.basepower.fluid_discharge.name"); } @Override @@ -47,7 +47,7 @@ public void powerUpdate() { super.powerUpdate(); } - private static BlockPos findPlacement(World world, BlockPos initial, Fluid fluid, int range) { + private static BlockPos findPlacement(Level world, BlockPos initial, Fluid fluid, int range) { BlockPos position = initial; Direction[] directions = { Direction.DOWN, Direction.NORTH, Direction.WEST, Direction.SOUTH, Direction.EAST }; while (range-- > 0) { @@ -81,26 +81,26 @@ private static BlockPos findPlacement(World world, BlockPos initial, Fluid fluid @Override public boolean isFluidSource() { return false; } @Override public boolean isFluidSink() { return true; } - @Override public CompoundNBT getUpdateTag() { - CompoundNBT tag = new CompoundNBT(); + @Override public CompoundTag getUpdateTag() { + CompoundTag tag = new CompoundTag(); getTank().writeToNBT(tag); return tag; } - @Override public void handleUpdateTag(net.minecraft.block.BlockState state, CompoundNBT tag) { getTank().readFromNBT(tag); } - @Override public SUpdateTileEntityPacket getUpdatePacket() { - return new SUpdateTileEntityPacket(getBlockPos(), 0, getUpdateTag()); + @Override public void handleUpdateTag(CompoundTag tag) { getTank().readFromNBT(tag); } + @Override public ClientboundBlockEntityDataPacket getUpdatePacket() { + return new ClientboundBlockEntityDataPacket(getBlockPos(), 0, getUpdateTag()); } - @Override public void onDataPacket(NetworkManager net, SUpdateTileEntityPacket packet) { - handleUpdateTag(getBlockState(), packet.getTag()); + @Override public void onDataPacket(Connection net, ClientboundBlockEntityDataPacket packet) { + handleUpdateTag(packet.getTag()); } - public static BlockPos scanFluidSpaceForNonsourceBlock(World world, BlockPos initial, Fluid fluid, int range) { + public static BlockPos scanFluidSpaceForNonsourceBlock(Level world, BlockPos initial, Fluid fluid, int range) { return findPlacement(world, initial, fluid, range); } - public static boolean isFluidBlock(net.minecraft.block.Block block, Fluid fluid) { + public static boolean isFluidBlock(net.minecraft.world.level.block.Block block, Fluid fluid) { return FluidDrainTileEntity.isFluidBlock(block, fluid); } - public static boolean isFluidSourceBlock(net.minecraft.block.Block block, Fluid fluid, World world, BlockPos pos) { + public static boolean isFluidSourceBlock(net.minecraft.world.level.block.Block block, Fluid fluid, Level world, BlockPos pos) { return FluidDrainTileEntity.isFluidSourceBlock(block, fluid, world, pos); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainBlock.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDrainBlock.java similarity index 64% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainBlock.java rename to src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDrainBlock.java index ff0467a7..5126a826 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainBlock.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDrainBlock.java @@ -1,14 +1,14 @@ -package zone.moddev.mc.poweradvantage.machines.fluidmachines; +package zone.moddev.mc.basepower.machines.fluidmachines; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PoweredEntity; import cyano.poweradvantage.api.simple.BlockSimpleFluidMachine; -import net.minecraft.block.material.Material; -import net.minecraft.block.BlockState; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.IBlockReader; -import net.minecraft.world.World; +import net.minecraft.world.level.material.Material; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; public class FluidDrainBlock extends BlockSimpleFluidMachine { @@ -18,8 +18,8 @@ public FluidDrainBlock() { } @Override - public PoweredEntity newBlockEntity(IBlockReader w) { - return new FluidDrainTileEntity(); + public PoweredEntity newBlockEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { + return new FluidDrainTileEntity(pos, state); } @Override @@ -28,8 +28,8 @@ public boolean hasAnalogOutputSignal(BlockState state) { } @Override - public int getAnalogOutputSignal(BlockState state, World world, BlockPos coord) { - TileEntity te = world.getBlockEntity(coord); + public int getAnalogOutputSignal(BlockState state, Level world, BlockPos coord) { + BlockEntity te = world.getBlockEntity(coord); if (te != null && te instanceof FluidDrainTileEntity) { return ((FluidDrainTileEntity) te).getRedstoneOutput(); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainTileEntity.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDrainTileEntity.java similarity index 72% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainTileEntity.java rename to src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDrainTileEntity.java index ccda8d6d..9b0d546b 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainTileEntity.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDrainTileEntity.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.machines.fluidmachines; +package zone.moddev.mc.basepower.machines.fluidmachines; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.fluid.FluidStack; @@ -6,23 +6,23 @@ import cyano.poweradvantage.api.fluid.FluidTankInfo; import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; import cyano.poweradvantage.api.simple.TileEntitySimpleFluidMachine; -import net.minecraft.fluid.Fluid; -import net.minecraft.fluid.FluidState; -import net.minecraft.block.BlockState; -import net.minecraft.block.IBucketPickupHandler; -import net.minecraft.world.IWorld; -import net.minecraft.item.ItemStack; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; -import zone.moddev.mc.poweradvantage.init.Entities; -import zone.moddev.mc.poweradvantage.util.FluidHandlerHelper; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.BucketPickup; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import zone.moddev.mc.basepower.init.Entities; +import zone.moddev.mc.basepower.util.FluidHandlerHelper; /** Pulls source blocks or adjacent Advantage tanks into the pipe network. */ public class FluidDrainTileEntity extends TileEntitySimpleFluidMachine { - public FluidDrainTileEntity() { - super(Entities.FLUID_DRAIN, FluidTank.BUCKET_VOLUME, "tile.poweradvantage.fluid_drain.name"); + public FluidDrainTileEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { + super(Entities.FLUID_DRAIN, pos, state, FluidTank.BUCKET_VOLUME, "tile.basepower.fluid_drain.name"); } @Override @@ -54,17 +54,18 @@ public void powerUpdate() { BlockPos source = scanFluidSpaceForSourceBlock(getLevel(), getBlockPos().above(), 32); if (source != null) { Fluid fluid = pickupSource(getLevel(), source, getLevel().getBlockState(source)); - if (fluid != net.minecraft.fluid.Fluids.EMPTY) + if (fluid != net.minecraft.world.level.material.Fluids.EMPTY) getTank().fill(new FluidStack(fluid, FluidTank.BUCKET_VOLUME), true); } } super.powerUpdate(); } - static Fluid pickupSource(IWorld world, BlockPos position, BlockState state) { - if (!(state.getBlock() instanceof IBucketPickupHandler)) return net.minecraft.fluid.Fluids.EMPTY; - Fluid picked = ((IBucketPickupHandler)state.getBlock()).takeLiquid(world, position, state); - return picked == null ? net.minecraft.fluid.Fluids.EMPTY : picked; + static Fluid pickupSource(LevelAccessor world, BlockPos position, BlockState state) { + if (!(state.getBlock() instanceof BucketPickup)) return net.minecraft.world.level.material.Fluids.EMPTY; + Fluid fluid = state.getFluidState().getType(); + ItemStack bucket = ((BucketPickup)state.getBlock()).pickupBlock(world, position, state); + return bucket.isEmpty() ? net.minecraft.world.level.material.Fluids.EMPTY : fluid; } private void tryPushFluid(BlockPos position, Direction face) { @@ -88,13 +89,13 @@ private void tryPushFluid(BlockPos position, Direction face) { @Override public boolean isFluidSource() { return true; } @Override public boolean isFluidSink() { return false; } - public static BlockPos scanFluidSpaceForSourceBlock(World world, BlockPos initial, int range) { + public static BlockPos scanFluidSpaceForSourceBlock(Level world, BlockPos initial, int range) { BlockPos position = initial; Direction[] directions = { Direction.UP, Direction.NORTH, Direction.WEST, Direction.SOUTH, Direction.EAST }; while (range-- > 0) { FluidState state = world.getFluidState(position); if (!state.isEmpty() && state.isSource() - && world.getBlockState(position).getBlock() instanceof IBucketPickupHandler) return position; + && world.getBlockState(position).getBlock() instanceof BucketPickup) return position; java.util.List candidates = new java.util.ArrayList<>(); for (Direction direction : directions) { BlockPos candidate = position.relative(direction); @@ -108,11 +109,11 @@ public static BlockPos scanFluidSpaceForSourceBlock(World world, BlockPos initia return null; } - public static boolean isFluidBlock(net.minecraft.block.Block block, Fluid fluid) { + public static boolean isFluidBlock(net.minecraft.world.level.block.Block block, Fluid fluid) { return block.defaultBlockState().getFluidState().getType().isSame(fluid); } - public static boolean isFluidSourceBlock(net.minecraft.block.Block block, Fluid fluid, World world, BlockPos pos) { + public static boolean isFluidSourceBlock(net.minecraft.world.level.block.Block block, Fluid fluid, Level world, BlockPos pos) { FluidState state = world.getFluidState(pos); return !state.isEmpty() && state.isSource() && state.getType().isSame(fluid); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidPipeBlock.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidPipeBlock.java similarity index 53% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidPipeBlock.java rename to src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidPipeBlock.java index 38568e2c..5238ea81 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidPipeBlock.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidPipeBlock.java @@ -1,17 +1,17 @@ -package zone.moddev.mc.poweradvantage.machines.fluidmachines; +package zone.moddev.mc.basepower.machines.fluidmachines; import cyano.poweradvantage.api.simple.BlockSimpleFluidConduit; -import zone.moddev.mc.poweradvantage.init.Blocks; -import zone.moddev.mc.poweradvantage.util.FluidHandlerHelper; -import net.minecraft.block.Block; -import net.minecraft.block.material.Material; -import net.minecraft.block.BlockState; -import net.minecraft.entity.LivingEntity; -import net.minecraft.item.ItemStack; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; +import zone.moddev.mc.basepower.init.Blocks; +import zone.moddev.mc.basepower.util.FluidHandlerHelper; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.material.Material; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; @SuppressWarnings("deprecation") public class FluidPipeBlock extends BlockSimpleFluidConduit { @@ -25,19 +25,19 @@ public FluidPipeBlock() { * Called when a neighboring block changes. */ @Override - public void neighborChanged(BlockState state, World worldIn, BlockPos pos, Block blockIn, BlockPos fromPos, boolean isMoving) { + public void neighborChanged(BlockState state, Level worldIn, BlockPos pos, Block blockIn, BlockPos fromPos, boolean isMoving) { pipeCheck(worldIn, pos); } /** * Called by ItemBlocks after a block is set in the world, to allow post-place logic */ - public void setPlacedBy(World worldIn, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) { + public void setPlacedBy(Level worldIn, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) { super.setPlacedBy(worldIn, pos, state, placer, stack); pipeCheck(worldIn, pos); } - private void pipeCheck(World w, BlockPos pos) { + private void pipeCheck(Level w, BlockPos pos) { if (!w.isClientSide) { if (numberOfAdjacentFluidHandlers(w, pos) > 0) { w.setBlock(pos, Blocks.fluid_pipe_terminal.defaultBlockState(), 3); @@ -46,10 +46,10 @@ private void pipeCheck(World w, BlockPos pos) { } - protected static int numberOfAdjacentFluidHandlers(World w, BlockPos pos) { + protected static int numberOfAdjacentFluidHandlers(Level w, BlockPos pos) { int sum = 0; for (Direction f : Direction.values()) { - TileEntity e = w.getBlockEntity(pos.relative(f)); + BlockEntity e = w.getBlockEntity(pos.relative(f)); if (FluidHandlerHelper.hasHandler(e, f.getOpposite()) && !(e instanceof cyano.poweradvantage.api.PoweredEntity)) { sum++; } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankBlock.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/MetalTankBlock.java similarity index 62% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankBlock.java rename to src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/MetalTankBlock.java index 238730ac..0f4a5a69 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankBlock.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/MetalTankBlock.java @@ -1,14 +1,14 @@ -package zone.moddev.mc.poweradvantage.machines.fluidmachines; +package zone.moddev.mc.basepower.machines.fluidmachines; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PoweredEntity; import cyano.poweradvantage.api.simple.BlockSimpleFluidMachine; -import net.minecraft.block.material.Material; -import net.minecraft.block.BlockState; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; -import net.minecraft.world.IBlockReader; +import net.minecraft.world.level.material.Material; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.BlockGetter; public class MetalTankBlock extends BlockSimpleFluidMachine { @@ -17,8 +17,8 @@ public MetalTankBlock() { } @Override - public PoweredEntity newBlockEntity(IBlockReader world) { - return new MetalTankTileEntity(); + public PoweredEntity newBlockEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { + return new MetalTankTileEntity(pos, state); } @Override @@ -28,8 +28,8 @@ public boolean hasAnalogOutputSignal(BlockState bs) { @Override - public int getAnalogOutputSignal(BlockState bs, World world, BlockPos coord) { - TileEntity te = world.getBlockEntity(coord); + public int getAnalogOutputSignal(BlockState bs, Level world, BlockPos coord) { + BlockEntity te = world.getBlockEntity(coord); if (te != null && te instanceof MetalTankTileEntity) { return ((MetalTankTileEntity) te).getRedstoneOutput(); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankTileEntity.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/MetalTankTileEntity.java similarity index 90% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankTileEntity.java rename to src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/MetalTankTileEntity.java index bd502279..5f18f196 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankTileEntity.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/MetalTankTileEntity.java @@ -1,21 +1,21 @@ -package zone.moddev.mc.poweradvantage.machines.fluidmachines; +package zone.moddev.mc.basepower.machines.fluidmachines; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PowerRequest; import cyano.poweradvantage.api.fluid.FluidRequest; import cyano.poweradvantage.api.simple.TileEntitySimpleFluidMachine; -import net.minecraft.item.ItemStack; -import net.minecraft.fluid.Fluid; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.material.Fluid; import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.FluidTank; -import zone.moddev.mc.poweradvantage.init.Entities; -import zone.moddev.mc.poweradvantage.util.FluidContainerHelper; +import zone.moddev.mc.basepower.init.Entities; +import zone.moddev.mc.basepower.util.FluidContainerHelper; @SuppressWarnings("deprecation") public class MetalTankTileEntity extends TileEntitySimpleFluidMachine { - public MetalTankTileEntity() { - super(Entities.METAL_TANK, FluidTank.BUCKET_VOLUME * 10, "tile.poweradvantage.fluid_metal_tank.name"); + public MetalTankTileEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { + super(Entities.METAL_TANK, pos, state, FluidTank.BUCKET_VOLUME * 10, "tile.basepower.fluid_metal_tank.name"); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillBlock.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StillBlock.java similarity index 68% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillBlock.java rename to src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StillBlock.java index 83c23034..c8e0b855 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillBlock.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StillBlock.java @@ -1,18 +1,18 @@ -package zone.moddev.mc.poweradvantage.machines.fluidmachines; +package zone.moddev.mc.basepower.machines.fluidmachines; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PoweredEntity; import cyano.poweradvantage.api.simple.BlockSimpleFluidMachine; -import net.minecraft.block.material.Material; -import net.minecraft.state.BooleanProperty; -import net.minecraft.state.StateContainer; -import net.minecraft.block.BlockState; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.IBlockReader; -import net.minecraft.world.IWorldReader; -import net.minecraft.world.World; +import net.minecraft.world.level.material.Material; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.Level; public class StillBlock extends BlockSimpleFluidMachine { @@ -23,7 +23,8 @@ public class StillBlock extends BlockSimpleFluidMachine { public static final BooleanProperty ACTIVE = BooleanProperty.create("active"); public StillBlock() { - super(Material.PISTON, 3f); + super(net.minecraft.world.level.block.Block.Properties.of(Material.PISTON) + .strength(3f).lightLevel(state -> state.getValue(ACTIVE) ? 12 : 0)); this.registerDefaultState(defaultBlockState().setValue(ACTIVE, false)); } @@ -31,7 +32,7 @@ public StillBlock() { * Creates a blockstate */ @Override - protected void createBlockStateDefinition(StateContainer.Builder builder) { + protected void createBlockStateDefinition(StateDefinition.Builder builder) { super.createBlockStateDefinition(builder); builder.add(ACTIVE); } @@ -95,13 +96,8 @@ private Direction metaToFacing(int i) { } @Override - public int getLightValue(BlockState bs, net.minecraft.world.IBlockReader world, BlockPos coord) { - return bs.getValue(ACTIVE) ? 12 : 0; - } - - @Override - public PoweredEntity newBlockEntity(IBlockReader world) { - return new StillTileEntity(); + public PoweredEntity newBlockEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { + return new StillTileEntity(pos, state); } @Override @@ -110,8 +106,8 @@ public boolean hasAnalogOutputSignal(BlockState bs) { } @Override - public int getAnalogOutputSignal(BlockState bs, World world, BlockPos coord) { - TileEntity te = world.getBlockEntity(coord); + public int getAnalogOutputSignal(BlockState bs, Level world, BlockPos coord) { + BlockEntity te = world.getBlockEntity(coord); if (te instanceof StillTileEntity) { return ((StillTileEntity) te).getRedstoneOutput(); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillTileEntity.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StillTileEntity.java similarity index 89% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillTileEntity.java rename to src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StillTileEntity.java index 96fccde9..11cabd96 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillTileEntity.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StillTileEntity.java @@ -1,22 +1,22 @@ -package zone.moddev.mc.poweradvantage.machines.fluidmachines; +package zone.moddev.mc.basepower.machines.fluidmachines; import cyano.poweradvantage.api.fluid.FluidRequest; import cyano.poweradvantage.api.simple.TileEntitySimpleFluidMachine; -import zone.moddev.mc.poweradvantage.registry.FuelRegistry; -import zone.moddev.mc.poweradvantage.registry.still.recipe.DistillationRecipe; -import zone.moddev.mc.poweradvantage.registry.still.recipe.DistillationRecipeRegistry; -import zone.moddev.mc.poweradvantage.util.FluidIdHelper; -import net.minecraft.block.BlockState; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.util.Direction; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; -import net.minecraft.fluid.Fluid; +import zone.moddev.mc.basepower.registry.FuelRegistry; +import zone.moddev.mc.basepower.registry.still.recipe.DistillationRecipe; +import zone.moddev.mc.basepower.registry.still.recipe.DistillationRecipeRegistry; +import zone.moddev.mc.basepower.util.FluidIdHelper; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.FluidTank; import cyano.poweradvantage.api.fluid.FluidTankInfo; -import zone.moddev.mc.poweradvantage.init.Entities; +import zone.moddev.mc.basepower.init.Entities; @SuppressWarnings("deprecation") public class StillTileEntity extends TileEntitySimpleFluidMachine { @@ -40,8 +40,8 @@ public class StillTileEntity extends TileEntitySimpleFluidMachine { private short totalBurnTime = 0; - public StillTileEntity() { - super(Entities.STILL, FluidTank.BUCKET_VOLUME, "tile.poweradvantage.still.name"); + public StillTileEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { + super(Entities.STILL, pos, state, FluidTank.BUCKET_VOLUME, "tile.basepower.still.name"); } @Override @@ -74,7 +74,7 @@ public void tickUpdate(boolean isServerWorld) { } @Override - public boolean shouldRefresh(World world, BlockPos pos, BlockState oldState, BlockState newState) { + public boolean shouldRefresh(Level world, BlockPos pos, BlockState oldState, BlockState newState) { // used to allow change in blockstate without interrupting the TileEntity or the GUI return (oldState.getBlock() != newState.getBlock()); } @@ -247,9 +247,9 @@ public FluidTankInfo[] getTankInfo(Direction face) { * @param tagRoot An NBT tag */ @Override - public CompoundNBT save(final CompoundNBT tagRoot) { + public CompoundTag save(final CompoundTag tagRoot) { super .save(tagRoot); - CompoundNBT tankTag2 = new CompoundNBT(); + CompoundTag tankTag2 = new CompoundTag(); inputTank.writeToNBT(tankTag2); tagRoot.put("InputTank", tankTag2); tagRoot.putShort("burnTime", this.burnTime); @@ -263,10 +263,10 @@ public CompoundNBT save(final CompoundNBT tagRoot) { * @param tagRoot An NBT tag */ @Override - public void load(net.minecraft.block.BlockState state, final CompoundNBT tagRoot) { - super.load(state, tagRoot); + public void load(final CompoundTag tagRoot) { + super.load(tagRoot); if (tagRoot.contains("InputTank", 10)) { - CompoundNBT tankTag2 = tagRoot .getCompound("InputTank"); + CompoundTag tankTag2 = tagRoot .getCompound("InputTank"); inputTank.readFromNBT(tankTag2); if (tankTag2.contains("Empty", 1)) { // empty the tank if NBT says its empty (not default behavior of Tank.readFromNBT(...) ) @@ -334,7 +334,7 @@ public void onDataFieldUpdate() { int fluidID2 = dataFields[DATAFIELD_FLUID_ID2]; int fluidVolume2 = dataFields[DATAFIELD_FLUID_VOLUME2]; if (fluidVolume1 <= 0) { - inputTank.setFluid(new FluidStack(net.minecraft.fluid.Fluids.WATER, 0)); + inputTank.setFluid(new FluidStack(net.minecraft.world.level.material.Fluids.WATER, 0)); } else { Fluid fluid = FluidIdHelper.getFluid(fluidID1); if (fluid != null) { @@ -343,7 +343,7 @@ public void onDataFieldUpdate() { } } if (fluidVolume2 <= 0) { - getTank().setFluid(new FluidStack(net.minecraft.fluid.Fluids.WATER, 0)); + getTank().setFluid(new FluidStack(net.minecraft.world.level.material.Fluids.WATER, 0)); } else { Fluid fluid = FluidIdHelper.getFluid(fluidID2); if (fluid != null) { @@ -376,14 +376,14 @@ public void onDataFieldUpdate() { */ public void prepareDataFieldsForSync() { if (inputTank.getFluid() == null || inputTank.getFluidAmount() <= 0) { - dataFields[DATAFIELD_FLUID_ID1] = FluidIdHelper.getFluidId(net.minecraft.fluid.Fluids.WATER); + dataFields[DATAFIELD_FLUID_ID1] = FluidIdHelper.getFluidId(net.minecraft.world.level.material.Fluids.WATER); dataFields[DATAFIELD_FLUID_VOLUME1] = 0; } else { dataFields[DATAFIELD_FLUID_ID1] = FluidIdHelper.getFluidId(inputTank.getFluid().getFluid()); dataFields[DATAFIELD_FLUID_VOLUME1] = inputTank.getFluidAmount(); } if (getTank().getFluid() == null || getTank().getFluidAmount() <= 0) { - dataFields[DATAFIELD_FLUID_ID2] = FluidIdHelper.getFluidId(net.minecraft.fluid.Fluids.WATER); + dataFields[DATAFIELD_FLUID_ID2] = FluidIdHelper.getFluidId(net.minecraft.world.level.material.Fluids.WATER); dataFields[DATAFIELD_FLUID_VOLUME2] = 0; } else { dataFields[DATAFIELD_FLUID_ID2] = FluidIdHelper.getFluidId(getTank().getFluid().getFluid()); @@ -394,13 +394,13 @@ public void prepareDataFieldsForSync() { } @Override - public CompoundNBT createDataFieldUpdateTag() { + public CompoundTag createDataFieldUpdateTag() { this.prepareDataFieldsForSync(); - CompoundNBT nbtTag = new CompoundNBT(); - CompoundNBT inputTankTag = new CompoundNBT(); + CompoundTag nbtTag = new CompoundTag(); + CompoundTag inputTankTag = new CompoundTag(); inputTank.writeToNBT(inputTankTag); nbtTag.put("InputTank", inputTankTag); - CompoundNBT outputTankTag = new CompoundNBT(); + CompoundTag outputTankTag = new CompoundTag(); getTank().writeToNBT(outputTankTag); nbtTag.put("Tank", outputTankTag); nbtTag.putShort("burnTime", this.burnTime); @@ -410,10 +410,10 @@ public CompoundNBT createDataFieldUpdateTag() { } @Override - public void readDataFieldUpdateTag(CompoundNBT tag) { + public void readDataFieldUpdateTag(CompoundTag tag) { boolean usedFluidNbt = false; if (tag .contains("InputTank", 10)) { - CompoundNBT inputTankTag = tag .getCompound("InputTank"); + CompoundTag inputTankTag = tag .getCompound("InputTank"); inputTank.readFromNBT(inputTankTag); if (inputTankTag.contains("Empty", 1)) { inputTank.setFluid(null); @@ -421,7 +421,7 @@ public void readDataFieldUpdateTag(CompoundNBT tag) { usedFluidNbt = true; } if (tag .contains("Tank", 10)) { - CompoundNBT outputTankTag = tag .getCompound("Tank"); + CompoundTag outputTankTag = tag .getCompound("Tank"); getTank().readFromNBT(outputTankTag); if (outputTankTag.contains("Empty", 1)) { getTank().setFluid(null); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StorageTankBlock.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StorageTankBlock.java similarity index 54% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StorageTankBlock.java rename to src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StorageTankBlock.java index bc8cd6f4..45637785 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StorageTankBlock.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StorageTankBlock.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.machines.fluidmachines; +package zone.moddev.mc.basepower.machines.fluidmachines; import java.util.List; @@ -9,45 +9,45 @@ import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.FluidTank; import cyano.poweradvantage.api.simple.BlockSimpleFluidMachine; -import net.minecraft.block.material.Material; -import net.minecraft.block.BlockState; -import net.minecraft.entity.LivingEntity; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.client.util.ITooltipFlag; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.registry.Registry; -import net.minecraft.util.text.ITextComponent; -import net.minecraft.util.text.StringTextComponent; -import net.minecraft.util.text.TranslationTextComponent; -import net.minecraft.world.IBlockReader; -import net.minecraft.world.World; -import zone.moddev.mc.poweradvantage.init.Fluids; +import net.minecraft.world.level.material.Material; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import zone.moddev.mc.basepower.init.Fluids; /** Portable tank block. FluidID and Volume remain unchanged for old item stacks. */ public class StorageTankBlock extends BlockSimpleFluidMachine { public StorageTankBlock() { super(Material.PISTON, 3F); } - @Override public PoweredEntity newBlockEntity(IBlockReader world) { return new StorageTankTileEntity(); } + @Override public PoweredEntity newBlockEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { return new StorageTankTileEntity(pos, state); } @Override public boolean hasAnalogOutputSignal(BlockState state) { return true; } - @Override public int getAnalogOutputSignal(BlockState state, World world, BlockPos pos) { - TileEntity tile = world.getBlockEntity(pos); + @Override public int getAnalogOutputSignal(BlockState state, Level world, BlockPos pos) { + BlockEntity tile = world.getBlockEntity(pos); return tile instanceof StorageTankTileEntity ? ((StorageTankTileEntity) tile).getRedstoneOutput() : 0; } @Override public boolean isPowerSink(ConduitType type) { return true; } @Override public boolean isPowerSource(ConduitType type) { return true; } @Override - public void setPlacedBy(World world, BlockPos pos, BlockState state, + public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { super.setPlacedBy(world, pos, state, placer, stack); - CompoundNBT tag = stack.getTag(); - TileEntity tile = world.getBlockEntity(pos); + CompoundTag tag = stack.getTag(); + BlockEntity tile = world.getBlockEntity(pos); if (tag != null && tag.contains("FluidID", 8) && tag.contains("Volume", 99) && tile instanceof StorageTankTileEntity) { - net.minecraft.fluid.Fluid fluid = Fluids.byName(tag.getString("FluidID")); + net.minecraft.world.level.material.Fluid fluid = Fluids.byName(tag.getString("FluidID")); if (fluid != null) ((StorageTankTileEntity) tile).getTank() .setFluid(new FluidStack(fluid, tag.getInt("Volume"))); } @@ -56,8 +56,8 @@ public void setPlacedBy(World world, BlockPos pos, BlockState state, public ItemStack createItem(StorageTankTileEntity tile) { ItemStack stack = new ItemStack(this); if (tile.getTank().getFluidAmount() > 0) { - CompoundNBT tag = new CompoundNBT(); - net.minecraft.util.ResourceLocation id = net.minecraftforge.registries.ForgeRegistries.FLUIDS + CompoundTag tag = new CompoundTag(); + net.minecraft.resources.ResourceLocation id = net.minecraftforge.registries.ForgeRegistries.FLUIDS .getKey(tile.getTank().getFluid().getFluid()); if (id != null) tag.putString("FluidID", id.toString()); tag.putInt("Volume", tile.getTank().getFluidAmount()); @@ -67,15 +67,15 @@ public ItemStack createItem(StorageTankTileEntity tile) { } @Override - public void appendHoverText(ItemStack stack, @Nullable IBlockReader world, - List tooltip, ITooltipFlag flag) { - CompoundNBT tag = stack.getTag(); + public void appendHoverText(ItemStack stack, @Nullable BlockGetter world, + List tooltip, TooltipFlag flag) { + CompoundTag tag = stack.getTag(); if (tag == null || !tag.contains("FluidID", 8) || !tag.contains("Volume", 99)) { - tooltip.add(new TranslationTextComponent("tooltip.poweradvantage.empty")); + tooltip.add(new TranslatableComponent("tooltip.basepower.empty")); return; } float buckets = tag.getInt("Volume") / (float) FluidTank.BUCKET_VOLUME; - tooltip.add(new StringTextComponent(buckets + " buckets of " + tag.getString("FluidID"))); + tooltip.add(new TextComponent(buckets + " buckets of " + tag.getString("FluidID"))); } /** Retained for callers from the 1.12 implementation. */ diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StorageTankTileEntity.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StorageTankTileEntity.java similarity index 87% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StorageTankTileEntity.java rename to src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StorageTankTileEntity.java index 8b88604a..a2294831 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StorageTankTileEntity.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StorageTankTileEntity.java @@ -1,20 +1,20 @@ -package zone.moddev.mc.poweradvantage.machines.fluidmachines; +package zone.moddev.mc.basepower.machines.fluidmachines; import cyano.poweradvantage.api.PowerRequest; import cyano.poweradvantage.api.fluid.FluidRequest; import cyano.poweradvantage.api.simple.TileEntitySimpleFluidMachine; -import net.minecraft.item.ItemStack; -import net.minecraft.fluid.Fluid; -import net.minecraft.fluid.Fluid; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluid; import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.FluidTank; -import zone.moddev.mc.poweradvantage.init.Entities; +import zone.moddev.mc.basepower.init.Entities; @SuppressWarnings("deprecation") public class StorageTankTileEntity extends TileEntitySimpleFluidMachine { - public StorageTankTileEntity() { - super(Entities.STORAGE_TANK, FluidTank.BUCKET_VOLUME * 4, "tile.poweradvantage.fluid_storage_tank.name"); + public StorageTankTileEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { + super(Entities.STORAGE_TANK, pos, state, FluidTank.BUCKET_VOLUME * 4, "tile.basepower.fluid_storage_tank.name"); } /** diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java similarity index 59% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java rename to src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java index c2d8cc4c..0f8e99b9 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java @@ -1,26 +1,26 @@ -package zone.moddev.mc.poweradvantage.machines.fluidmachines.modsupport; +package zone.moddev.mc.basepower.machines.fluidmachines.modsupport; import cyano.poweradvantage.api.ConduitType; -import zone.moddev.mc.poweradvantage.init.Blocks; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.FluidPipeBlock; -import zone.moddev.mc.poweradvantage.util.FluidHandlerHelper; -import net.minecraft.block.Block; -import net.minecraft.block.ITileEntityProvider; -import net.minecraft.block.BlockState; -import net.minecraft.entity.LivingEntity; -import net.minecraft.item.ItemStack; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.IBlockReader; -import net.minecraft.world.World; +import zone.moddev.mc.basepower.init.Blocks; +import zone.moddev.mc.basepower.machines.fluidmachines.FluidPipeBlock; +import zone.moddev.mc.basepower.util.FluidHandlerHelper; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; @SuppressWarnings("deprecation") -public class TerminalFluidPipeBlock extends FluidPipeBlock implements ITileEntityProvider { +public class TerminalFluidPipeBlock extends FluidPipeBlock implements EntityBlock { @Override - public TileEntity newBlockEntity(IBlockReader worldIn) { - return new TerminalFluidPipeTileEntity(); + public BlockEntity newBlockEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { + return new TerminalFluidPipeTileEntity(pos, state); } @@ -38,19 +38,19 @@ public boolean isPowerSource(ConduitType powerType) { * Called when a neighboring block changes. */ @Override - public void neighborChanged(BlockState state, World worldIn, BlockPos pos, Block blockIn, BlockPos fromPos, boolean isMoving) { + public void neighborChanged(BlockState state, Level worldIn, BlockPos pos, Block blockIn, BlockPos fromPos, boolean isMoving) { pipeCheck(worldIn, pos); } /** * Called by ItemBlocks after a block is set in the world, to allow post-place logic */ - public void setPlacedBy(World worldIn, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) { + public void setPlacedBy(Level worldIn, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) { super.setPlacedBy(worldIn, pos, state, placer, stack); pipeCheck(worldIn, pos); } - private void pipeCheck(World w, BlockPos pos) { + private void pipeCheck(Level w, BlockPos pos) { if (!w.isClientSide) { if (numberOfAdjacentFluidHandlers(w, pos) == 0) { w.setBlock(pos, Blocks.fluid_pipe.defaultBlockState(), 3); @@ -72,7 +72,7 @@ private void pipeCheck(World w, BlockPos pos) { * method. */ @Override - protected boolean canConnectTo(IBlockReader w, BlockPos thisBlock, BlockState bs, Direction face) { + protected boolean canConnectTo(BlockGetter w, BlockPos thisBlock, BlockState bs, Direction face) { BlockPos otherBlock = thisBlock.relative(face); return super.canConnectTo(w, thisBlock, bs, face) || FluidHandlerHelper.hasHandler(w.getBlockEntity(otherBlock), face.getOpposite()); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java similarity index 87% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java rename to src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java index 3d5b1494..ee16337b 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.machines.fluidmachines.modsupport; +package zone.moddev.mc.basepower.machines.fluidmachines.modsupport; import java.util.List; @@ -9,25 +9,25 @@ import cyano.poweradvantage.api.fluid.FluidRequest; import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; -import net.minecraft.fluid.Fluid; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; -import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry; -import zone.moddev.mc.poweradvantage.init.Entities; -import zone.moddev.mc.poweradvantage.init.Fluids; -import zone.moddev.mc.poweradvantage.util.FluidHandlerHelper; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; +import zone.moddev.mc.basepower.conduitnetwork.ConduitRegistry; +import zone.moddev.mc.basepower.init.Entities; +import zone.moddev.mc.basepower.init.Fluids; +import zone.moddev.mc.basepower.util.FluidHandlerHelper; /** Bridges adjacent Advantage fluid handlers onto the conduit request network. */ public class TerminalFluidPipeTileEntity extends PoweredEntity { private final Direction[] faces = Direction.values(); private final IAdvantageFluidHandler[] neighbors = new IAdvantageFluidHandler[faces.length]; - public TerminalFluidPipeTileEntity() { super(Entities.PIPE_TERMINAL); } + public TerminalFluidPipeTileEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { super(Entities.PIPE_TERMINAL, pos, state); } @Override public void tickUpdate(boolean server) { if (!server) return; for (int index = 0; index < faces.length; index++) { - TileEntity tile = getLevel().getBlockEntity(getBlockPos().relative(faces[index])); + BlockEntity tile = getLevel().getBlockEntity(getBlockPos().relative(faces[index])); neighbors[index] = FluidHandlerHelper.getHandler(tile, faces[index].getOpposite()); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/package-info.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/package-info.java similarity index 74% rename from src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/package-info.java rename to src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/package-info.java index 2df28e68..488d9fc9 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/package-info.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/package-info.java @@ -3,10 +3,10 @@ This package contains content that is added by PowerAdvantage implementing the PowerAdvantage API. If you are writing an add-on mod, use the code in this package for reference, but you should not depend on its implementation details as future updates might change them. For a better way to get -PowerAdvantage content, look in the zone.moddev.mc.poweradvantage.init package. +PowerAdvantage content, look in the zone.moddev.mc.basepower.init package.

Because these classes are not part of the API, they are not well documented. They were only meant for the eyes of the mod author(s).

*/ -package zone.moddev.mc.poweradvantage.machines.fluidmachines; +package zone.moddev.mc.basepower.machines.fluidmachines; diff --git a/src/main/java/zone/moddev/mc/poweradvantage/math/BlockPos4D.java b/src/main/java/zone/moddev/mc/basepower/math/BlockPos4D.java similarity index 86% rename from src/main/java/zone/moddev/mc/poweradvantage/math/BlockPos4D.java rename to src/main/java/zone/moddev/mc/basepower/math/BlockPos4D.java index 8964bd8f..6cfce6b9 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/math/BlockPos4D.java +++ b/src/main/java/zone/moddev/mc/basepower/math/BlockPos4D.java @@ -1,14 +1,14 @@ -package zone.moddev.mc.poweradvantage.math; - -import net.minecraft.nbt.INBT; -import net.minecraft.nbt.IntArrayNBT; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.util.Direction; -import net.minecraft.util.RegistryKey; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.Registry; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; +package zone.moddev.mc.basepower.math; + +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.Registry; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; /** * This class is used to store 4D integer coordinates @@ -19,7 +19,7 @@ public class BlockPos4D { /** * 4th dimension coordinate */ - public final RegistryKey dimension; + public final ResourceKey dimension; /** * X,Y,Z coordinate */ @@ -30,7 +30,7 @@ public class BlockPos4D { * * @return The dimension of the coordinate (e.g. 0 for overworld, -1 for nether, 1 for the end) */ - public RegistryKey getDim() { + public ResourceKey getDim() { return dimension; } @@ -71,7 +71,7 @@ public int getZ() { * @param y Y-coordinate of D,X,Y,Z coordinate tuple * @param z Z-coordinate of D,X,Y,Z coordinate tuple */ - public BlockPos4D(RegistryKey d, int x, int y, int z) { + public BlockPos4D(ResourceKey d, int x, int y, int z) { this(d, new BlockPos(x, y, z)); } @@ -81,7 +81,7 @@ public BlockPos4D(RegistryKey d, int x, int y, int z) { * @param dimension Dimension of the block * @param bp location of the block */ - public BlockPos4D(RegistryKey dimension, BlockPos bp) { + public BlockPos4D(ResourceKey dimension, BlockPos bp) { this.dimension = java.util.Objects.requireNonNull(dimension, "dimension"); this.pos = bp.immutable(); hashCache = (((dimension.hashCode() * 31 + pos.getZ()) * 31 + pos.getX()) * 31 + pos.getY()); @@ -264,7 +264,7 @@ public BlockPos4D east(final int dist) { * 1 for the end) * @return A new BlockPos4D representing the new position */ - public BlockPos4D toDimension(RegistryKey newDimension) { + public BlockPos4D toDimension(ResourceKey newDimension) { return new BlockPos4D(newDimension, this.pos); } @@ -276,32 +276,32 @@ public BlockPos4D[] getNeighbors() { return n; } - public INBT toNBT() { - CompoundNBT data = new CompoundNBT(); + public Tag toNBT() { + CompoundTag data = new CompoundTag(); data.putString("Dimension", dimension.location().toString()); data.putIntArray("Pos", new int[] {pos.getX(), pos.getY(), pos.getZ()}); return data; } - public static BlockPos4D fromNBT(INBT tag) { - if (tag instanceof IntArrayNBT) { - int[] data = ((IntArrayNBT) tag).getAsIntArray(); + public static BlockPos4D fromNBT(Tag tag) { + if (tag instanceof IntArrayTag) { + int[] data = ((IntArrayTag) tag).getAsIntArray(); if (data.length != 4) throw new IllegalArgumentException("Invalid legacy position"); return new BlockPos4D(legacyDimension(data[0]), data[1], data[2], data[3]); } - CompoundNBT data = (CompoundNBT) tag; + CompoundTag data = (CompoundTag) tag; int[] pos = data.getIntArray("Pos"); if (pos.length != 3) throw new IllegalArgumentException("Invalid position"); - return new BlockPos4D(RegistryKey.create(Registry.DIMENSION_REGISTRY, + return new BlockPos4D(ResourceKey.create(Registry.DIMENSION_REGISTRY, new ResourceLocation(data.getString("Dimension"))), pos[0], pos[1], pos[2]); } /** Only the three vanilla numeric IDs have an unambiguous legacy meaning. */ - public static RegistryKey legacyDimension(int dimension) { + public static ResourceKey legacyDimension(int dimension) { switch (dimension) { - case 0: return World.OVERWORLD; - case -1: return World.NETHER; - case 1: return World.END; + case 0: return Level.OVERWORLD; + case -1: return Level.NETHER; + case 1: return Level.END; default: throw new IllegalArgumentException("No registry key for legacy dimension " + dimension); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/math/Integer2D.java b/src/main/java/zone/moddev/mc/basepower/math/Integer2D.java similarity index 95% rename from src/main/java/zone/moddev/mc/poweradvantage/math/Integer2D.java rename to src/main/java/zone/moddev/mc/basepower/math/Integer2D.java index 987a8d9f..7722b0b1 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/math/Integer2D.java +++ b/src/main/java/zone/moddev/mc/basepower/math/Integer2D.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.math; +package zone.moddev.mc.basepower.math; /** * This class is used to store 2D integer coordinates diff --git a/src/main/java/zone/moddev/mc/poweradvantage/math/package-info.java b/src/main/java/zone/moddev/mc/basepower/math/package-info.java similarity index 55% rename from src/main/java/zone/moddev/mc/poweradvantage/math/package-info.java rename to src/main/java/zone/moddev/mc/basepower/math/package-info.java index 72c1ee2a..2ec83a06 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/math/package-info.java +++ b/src/main/java/zone/moddev/mc/basepower/math/package-info.java @@ -3,4 +3,4 @@ This package contains helper classes.

*/ -package zone.moddev.mc.poweradvantage.math; \ No newline at end of file +package zone.moddev.mc.basepower.math; \ No newline at end of file diff --git a/src/main/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrations.java b/src/main/java/zone/moddev/mc/basepower/migration/AdvantageLegacyMigrations.java similarity index 67% rename from src/main/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrations.java rename to src/main/java/zone/moddev/mc/basepower/migration/AdvantageLegacyMigrations.java index ce56da31..6185a607 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrations.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/AdvantageLegacyMigrations.java @@ -1,16 +1,16 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; import cyano.poweradvantage.api.migration.LegacyMigrationApi; import cyano.poweradvantage.api.simple.BlockSimplePowerMachine; -import net.minecraft.block.Block; -import net.minecraft.block.FlowingFluidBlock; -import net.minecraft.block.BlockState; -import net.minecraft.util.ResourceLocation; -import zone.moddev.mc.poweradvantage.PowerAdvantage; -import zone.moddev.mc.poweradvantage.blocks.BlockPowerSwitch; -import zone.moddev.mc.poweradvantage.machines.conveyors.BlockConveyor; -import zone.moddev.mc.poweradvantage.machines.conveyors.BlockConveyorFilter; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.StillBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.resources.ResourceLocation; +import zone.moddev.mc.basepower.PowerAdvantage; +import zone.moddev.mc.basepower.blocks.BlockPowerSwitch; +import zone.moddev.mc.basepower.machines.conveyors.BlockConveyor; +import zone.moddev.mc.basepower.machines.conveyors.BlockConveyorFilter; +import zone.moddev.mc.basepower.machines.fluidmachines.StillBlock; /** Registers PowerAdvantage's 1.10 and 1.12 identities with the shared hook. */ public final class AdvantageLegacyMigrations { @@ -20,7 +20,7 @@ private AdvantageLegacyMigrations() {} public static synchronized void registerPowerAdvantage() { if (registered) return; - LegacyMigrationApi.registerNamespace(PowerAdvantage.MODID); + LegacyMigrationApi.registerNamespaceAlias("poweradvantage", PowerAdvantage.MODID); for (String path : new String[] { "fluid_drain", "fluid_discharge", "fluid_storage_tank", "fluid_metal_tank", "still", @@ -55,21 +55,22 @@ public static synchronized void registerPowerAdvantage() { registerTile("rf_quantum_converter_tileentity"); registerTile("tr_electricity_converter_tileentity"); - LegacyMigrationApi.registerFluidAlias("crude_oil", "poweradvantage:crude_oil"); - LegacyMigrationApi.registerFluidAlias("refined_oil", "poweradvantage:refined_oil"); + LegacyMigrationApi.registerFluidAlias("crude_oil", "basepower:crude_oil"); + LegacyMigrationApi.registerFluidAlias("refined_oil", "basepower:refined_oil"); + LegacyMigrationApi.registerFluidAlias("poweradvantage:crude_oil", "basepower:crude_oil"); + LegacyMigrationApi.registerFluidAlias("poweradvantage:refined_oil", "basepower:refined_oil"); + LegacyMigrationApi.registerFluidAlias("poweradvantage:flowing_crude_oil", "basepower:flowing_crude_oil"); + LegacyMigrationApi.registerFluidAlias("poweradvantage:flowing_refined_oil", "basepower:flowing_refined_oil"); LegacyMigrationApi.registerFluidAlias("mineralogy_crude_oil", "mineralogy:crude_oil"); LegacyMigrationApi.registerLegacyFluidBucket("crude_oil", id("crude_oil_bucket")); - LegacyMigrationApi.registerLegacyFluidBucket("poweradvantage:crude_oil", id("crude_oil_bucket")); + LegacyMigrationApi.registerLegacyFluidBucket("basepower:crude_oil", id("crude_oil_bucket")); LegacyMigrationApi.registerLegacyFluidBucket("refined_oil", id("refined_oil_bucket")); - LegacyMigrationApi.registerLegacyFluidBucket("poweradvantage:refined_oil", id("refined_oil_bucket")); + LegacyMigrationApi.registerLegacyFluidBucket("basepower:refined_oil", id("refined_oil_bucket")); registered = true; } private static void registerTile(String path) { - String current = id(path).toString(); - LegacyMigrationApi.registerTileEntityAlias(current, current); - LegacyMigrationApi.registerTileEntityAlias(PowerAdvantage.MODID + "." + path, current); - LegacyMigrationApi.registerTileEntityAlias("minecraft:" + PowerAdvantage.MODID + "." + path, current); + LegacyMigrationApi.registerTileEntityAliases(id(path)); } private static BlockState decodeState(Block block, int metadata) { @@ -80,8 +81,8 @@ private static BlockState decodeState(Block block, int metadata) { if (block instanceof BlockSimplePowerMachine) { return ((BlockSimplePowerMachine) block).getStateFromMeta(metadata); } - if (block instanceof FlowingFluidBlock) { - return block.defaultBlockState().setValue(FlowingFluidBlock.LEVEL, Integer.valueOf(metadata & 15)); + if (block instanceof LiquidBlock) { + return block.defaultBlockState().setValue(LiquidBlock.LEVEL, Integer.valueOf(metadata & 15)); } return block.defaultBlockState(); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyEntityDataFixes.java b/src/main/java/zone/moddev/mc/basepower/migration/LegacyEntityDataFixes.java similarity index 67% rename from src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyEntityDataFixes.java rename to src/main/java/zone/moddev/mc/basepower/migration/LegacyEntityDataFixes.java index 40f513ad..4185d6a6 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyEntityDataFixes.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyEntityDataFixes.java @@ -1,17 +1,17 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; import com.mojang.datafixers.DataFixUtils; import com.mojang.datafixers.DataFixer; import com.mojang.serialization.Dynamic; import java.util.Set; import java.util.UUID; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.NBTDynamicOps; -import net.minecraft.util.SharedConstants; -import net.minecraft.util.datafix.DataFixesManager; -import net.minecraft.util.datafix.TypeReferences; -import net.minecraft.util.datafix.fixes.EntityUUID; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.SharedConstants; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.fixes.References; +import net.minecraft.util.datafix.fixes.EntityUUIDFix; /** Isolates vanilla entity choices from mod choices during the chunk datafix. */ final class LegacyEntityDataFixes { @@ -26,41 +26,42 @@ final class LegacyEntityDataFixes { private LegacyEntityDataFixes() {} - static void prepare(CompoundNBT root) { + static void prepare(CompoundTag root) { if (root.contains(STAGED)) return; int version = root.contains("DataVersion", 99) ? root.getInt("DataVersion") : -1; int current = SharedConstants.getCurrentVersion().getWorldVersion(); - if (version >= current || !root.contains("Level", 10)) return; - CompoundNBT level = root.getCompound("Level"); + if (version >= current) return; + CompoundTag level = entityOwner(root); + if (level == null) return; if (!level.contains("Entities")) return; requireCompoundList(level, "Entities"); - ListNBT entities = level.getList("Entities", 10); + ListTag entities = level.getList("Entities", 10); if (entities.isEmpty()) return; - DataFixer fixer = DataFixesManager.getDataFixer(); + DataFixer fixer = DataFixers.getDataFixer(); Set vanillaIds = fixer.getSchema(DataFixUtils.makeKey(version)) - .findChoiceType(TypeReferences.ENTITY).types().keySet(); - ListNBT fixed = new ListNBT(); + .findChoiceType(References.ENTITY).types().keySet(); + ListTag fixed = new ListTag(); for (int index = 0; index < entities.size(); index++) { fixed.add(upgrade(entities.getCompound(index), version, current, fixer, vanillaIds)); } // Keep these out of vanilla's mixed-choice traversal and avoid fixing them twice. root.put(STAGED, fixed); - level.put("Entities", new ListNBT()); + level.put("Entities", new ListTag()); } - private static CompoundNBT upgrade(CompoundNBT source, int version, int current, + private static CompoundTag upgrade(CompoundTag source, int version, int current, DataFixer fixer, Set vanillaIds) { - CompoundNBT entity = source.copy(); + CompoundTag entity = source.copy(); renameForgeAttributes(entity); boolean hasPassengers = source.contains("Passengers"); - ListNBT passengers = source.getList("Passengers", 10); + ListTag passengers = source.getList("Passengers", 10); entity.remove("Passengers"); - Dynamic dynamic = new Dynamic<>(NBTDynamicOps.INSTANCE, entity); + Dynamic dynamic = new Dynamic<>(NbtOps.INSTANCE, entity); if (vanillaIds.contains(entity.getString("id"))) { - entity = (CompoundNBT) fixer.update(TypeReferences.ENTITY, dynamic, version, current).getValue(); + entity = (CompoundTag) fixer.update(References.ENTITY, dynamic, version, current).getValue(); } else { // All entities inherit this root UUID format, including unknown mod types. - entity = (CompoundNBT) EntityUUID.updateEntityUUID(dynamic).getValue(); + entity = (CompoundTag) EntityUUIDFix.updateEntityUUID(dynamic).getValue(); } // Vanilla's UUID fixer rejects zero halves, although they are valid UUID bits. if (source.hasUUID("UUID")) { @@ -74,7 +75,7 @@ private static CompoundNBT upgrade(CompoundNBT source, int version, int current, } if (hasPassengers) { requireCompoundList(source, "Passengers"); - ListNBT fixed = new ListNBT(); + ListTag fixed = new ListTag(); for (int index = 0; index < passengers.size(); index++) { fixed.add(upgrade(passengers.getCompound(index), version, current, fixer, vanillaIds)); } @@ -83,35 +84,35 @@ private static CompoundNBT upgrade(CompoundNBT source, int version, int current, return entity; } - static void preparePlayerAttributes(CompoundNBT player) { + static void preparePlayerAttributes(CompoundTag player) { renameForgeAttributes(player); if (!player.contains("RootVehicle")) return; if (!player.contains("RootVehicle", 10)) throw new IllegalStateException("Malformed player RootVehicle"); - CompoundNBT vehicle = player.getCompound("RootVehicle"); + CompoundTag vehicle = player.getCompound("RootVehicle"); if (!vehicle.contains("Entity")) return; if (!vehicle.contains("Entity", 10)) throw new IllegalStateException("Malformed player RootVehicle.Entity"); prepareAttributeTree(vehicle.getCompound("Entity")); } - private static void prepareAttributeTree(CompoundNBT entity) { + private static void prepareAttributeTree(CompoundTag entity) { renameForgeAttributes(entity); if (!entity.contains("Passengers")) return; requireCompoundList(entity, "Passengers"); - ListNBT passengers = entity.getList("Passengers", 10); + ListTag passengers = entity.getList("Passengers", 10); for (int index = 0; index < passengers.size(); index++) { prepareAttributeTree(passengers.getCompound(index)); } } - private static void renameForgeAttributes(CompoundNBT entity) { + private static void renameForgeAttributes(CompoundTag entity) { if (!entity.contains("Attributes")) return; requireCompoundList(entity, "Attributes"); - ListNBT attributes = entity.getList("Attributes", 10); + ListTag attributes = entity.getList("Attributes", 10); for (String[] alias : FORGE_ATTRIBUTES) { - CompoundNBT legacy = null; + CompoundTag legacy = null; int identities = 0; for (int index = 0; index < attributes.size(); index++) { - CompoundNBT attribute = attributes.getCompound(index); + CompoundTag attribute = attributes.getCompound(index); if (attribute.getString("Name").equals(alias[0])) { legacy = attribute; identities++; @@ -124,13 +125,13 @@ private static void renameForgeAttributes(CompoundNBT entity) { } } - static void finish(CompoundNBT root) { + static void finish(CompoundTag root) { if (root == null || !root.contains(STAGED)) return; - if (!root.contains(STAGED, 9) || !root.contains("Level", 10)) { + if (!root.contains(STAGED, 9) || entityOwner(root) == null) { throw new IllegalStateException("Malformed staged Advantage entity migration"); } requireCompoundList(root, STAGED); - CompoundNBT level = root.getCompound("Level"); + CompoundTag level = entityOwner(root); if (level.contains("Entities")) requireCompoundList(level, "Entities"); if (!level.getList("Entities", 10).isEmpty()) { throw new IllegalStateException("Entity data appeared during isolated Advantage migration"); @@ -139,9 +140,18 @@ static void finish(CompoundNBT root) { root.remove(STAGED); } - private static void requireCompoundList(CompoundNBT owner, String key) { + private static CompoundTag entityOwner(CompoundTag root) { + if (root.contains("Level", 10)) return root.getCompound("Level"); + if (!root.contains("Position", 11)) return null; + if (root.getIntArray("Position").length != 2) { + throw new IllegalStateException("Saved entity chunk has no containing chunk coordinates"); + } + return root; + } + + private static void requireCompoundList(CompoundTag owner, String key) { if (!owner.contains(key, 9)) throw new IllegalStateException("Malformed entity list: " + key); - ListNBT list = (ListNBT) owner.get(key); + ListTag list = (ListTag) owner.get(key); if (!list.isEmpty() && list.getElementType() != 10) { throw new IllegalStateException("Non-compound entity list: " + key); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyMachineItemDataFixes.java b/src/main/java/zone/moddev/mc/basepower/migration/LegacyMachineItemDataFixes.java similarity index 67% rename from src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyMachineItemDataFixes.java rename to src/main/java/zone/moddev/mc/basepower/migration/LegacyMachineItemDataFixes.java index b80f117b..9da5c62d 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyMachineItemDataFixes.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyMachineItemDataFixes.java @@ -1,18 +1,18 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; import cyano.poweradvantage.api.migration.LegacyMigrationApi; import com.mojang.datafixers.DataFixer; import com.mojang.serialization.Dynamic; import java.util.ArrayList; import java.util.Objects; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.INBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.NBTDynamicOps; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.SharedConstants; -import net.minecraft.util.datafix.DataFixesManager; -import net.minecraft.util.datafix.TypeReferences; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.SharedConstants; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.fixes.References; /** Vanilla cannot traverse inventories behind unknown mod block-entity choices. */ final class LegacyMachineItemDataFixes { @@ -20,13 +20,13 @@ final class LegacyMachineItemDataFixes { private LegacyMachineItemDataFixes() {} - static void prepare(CompoundNBT root) { + static void prepare(CompoundTag root) { int version = root.contains("DataVersion", 99) ? root.getInt("DataVersion") : -1; int current = SharedConstants.getCurrentVersion().getWorldVersion(); if (version >= current || root.getBoolean(PREPARED) || !root.contains("Level", 10)) return; - ListNBT tiles = root.getCompound("Level").getList("TileEntities", 10); + ListTag tiles = root.getCompound("Level").getList("TileEntities", 10); for (int index = 0; index < tiles.size(); index++) { - CompoundNBT tile = tiles.getCompound(index); + CompoundTag tile = tiles.getCompound(index); ResourceLocation id = ResourceLocation.tryParse(tile.getString("id")); if (id != null && LegacyMigrationApi.handlesNamespace(id.getNamespace())) { rewrite(tile, version, current); @@ -35,16 +35,16 @@ static void prepare(CompoundNBT root) { root.putBoolean(PREPARED, true); } - private static INBT rewrite(INBT value, int version, int current) { - if (value instanceof CompoundNBT) { - CompoundNBT compound = (CompoundNBT) value; + private static Tag rewrite(Tag value, int version, int current) { + if (value instanceof CompoundTag) { + CompoundTag compound = (CompoundTag) value; ResourceLocation id = ResourceLocation.tryParse(compound.getString("id")); if (compound.contains("Count", 99) && id != null) { if (supported(id)) { - DataFixer fixer = DataFixesManager.getDataFixer(); - CompoundNBT input = version < 704 ? upgradeTo704(compound, version, fixer) : compound.copy(); - CompoundNBT fixed = (CompoundNBT) fixer.update(TypeReferences.ITEM_STACK, - new Dynamic<>(NBTDynamicOps.INSTANCE, input), Math.max(version, 704), current).getValue(); + DataFixer fixer = DataFixers.getDataFixer(); + CompoundTag input = version < 704 ? upgradeTo704(compound, version, fixer) : compound.copy(); + CompoundTag fixed = (CompoundTag) fixer.update(References.ITEM_STACK, + new Dynamic<>(NbtOps.INSTANCE, input), Math.max(version, 704), current).getValue(); // The native item schema also fixes its nested vanilla inventories. // Do not visit them again with the old version and reset tool damage. return preserve(compound, fixed); @@ -54,30 +54,30 @@ private static INBT rewrite(INBT value, int version, int current) { for (String key : new ArrayList<>(compound.getAllKeys())) { compound.put(key, rewrite(compound.get(key), version, current)); } - } else if (value instanceof ListNBT) { - ListNBT list = (ListNBT) value; + } else if (value instanceof ListTag) { + ListTag list = (ListTag) value; for (int index = 0; index < list.size(); index++) list.set(index, rewrite(list.get(index), version, current)); } return value; } - static CompoundNBT upgradeTo704(CompoundNBT source, int version, DataFixer fixer) { + static CompoundTag upgradeTo704(CompoundTag source, int version, DataFixer fixer) { // A root ITEM_STACK hits the unchecked 704 hook conversion too early. // Native PLAYER.Inventory lets the nested block-entity choice rename run. - CompoundNBT player = new CompoundNBT(); - ListNBT inventory = new ListNBT(); + CompoundTag player = new CompoundTag(); + ListTag inventory = new ListTag(); inventory.add(source.copy()); player.put("Inventory", inventory); - Object converted = fixer.update(TypeReferences.PLAYER, new Dynamic<>(NBTDynamicOps.INSTANCE, player), + Object converted = fixer.update(References.PLAYER, new Dynamic<>(NbtOps.INSTANCE, player), version, 704).getValue(); - if (!(converted instanceof CompoundNBT)) { + if (!(converted instanceof CompoundTag)) { throw new IllegalStateException("Native item envelope is not a player compound"); } - INBT items = ((CompoundNBT) converted).get("Inventory"); - if (!(items instanceof ListNBT) || ((ListNBT) items).size() != 1 || ((ListNBT) items).getElementType() != 10) { + Tag items = ((CompoundTag) converted).get("Inventory"); + if (!(items instanceof ListTag) || ((ListTag) items).size() != 1 || ((ListTag) items).getElementType() != 10) { throw new IllegalStateException("Native item envelope changed singleton inventory cardinality or type"); } - CompoundNBT result = ((ListNBT) items).getCompound(0); + CompoundTag result = ((ListTag) items).getCompound(0); ResourceLocation id = ResourceLocation.tryParse(result.getString("id")); if (!result.contains("id", 8) || id == null || id.getPath().isEmpty() || !result.contains("Count", 99) || !result.get("Count").equals(source.get("Count")) @@ -92,9 +92,9 @@ private static boolean supported(ResourceLocation id) { || id.getNamespace().equals("mmdlib") || LegacyMigrationApi.handlesNamespace(id.getNamespace()); } - private static INBT preserve(INBT original, INBT result) { - if (original instanceof CompoundNBT && result instanceof CompoundNBT) { - CompoundNBT before = (CompoundNBT) original, after = (CompoundNBT) result; + private static Tag preserve(Tag original, Tag result) { + if (original instanceof CompoundTag && result instanceof CompoundTag) { + CompoundTag before = (CompoundTag) original, after = (CompoundTag) result; ResourceLocation id = ResourceLocation.tryParse(before.getString("id")); if (before.contains("Count", 99) && id != null) { if (!after.contains("Count", 99) || before.getInt("Count") != after.getInt("Count") @@ -109,10 +109,10 @@ private static INBT preserve(INBT original, INBT result) { for (String key : new ArrayList<>(before.getAllKeys())) { if (after.contains(key)) after.put(key, preserve(before.get(key), after.get(key))); } - } else if (original instanceof ListNBT && result instanceof ListNBT) { - ListNBT before = (ListNBT) original, after = (ListNBT) result; - boolean inventory = before.stream().anyMatch(value -> value instanceof CompoundNBT - && ((CompoundNBT) value).contains("Count", 99) && ((CompoundNBT) value).contains("id", 8)); + } else if (original instanceof ListTag && result instanceof ListTag) { + ListTag before = (ListTag) original, after = (ListTag) result; + boolean inventory = before.stream().anyMatch(value -> value instanceof CompoundTag + && ((CompoundTag) value).contains("Count", 99) && ((CompoundTag) value).contains("id", 8)); if (inventory && before.size() != after.size()) { throw new IllegalStateException("Native item migration changed inventory cardinality"); } @@ -123,9 +123,9 @@ private static INBT preserve(INBT original, INBT result) { return result; } - private static void restoreNamedEnchantments(CompoundNBT source, CompoundNBT target, String oldKey, String newKey) { - ListNBT before = source.getCompound("tag").getList(oldKey, 10); - ListNBT after = target.getCompound("tag").getList(newKey, 10); + private static void restoreNamedEnchantments(CompoundTag source, CompoundTag target, String oldKey, String newKey) { + ListTag before = source.getCompound("tag").getList(oldKey, 10); + ListTag after = target.getCompound("tag").getList(newKey, 10); for (int index = 0; index < before.size(); index++) { if (!before.getCompound(index).contains("id", 8)) continue; if (after.size() != before.size()) { @@ -137,7 +137,7 @@ private static void restoreNamedEnchantments(CompoundNBT source, CompoundNBT tar } } - static void finish(CompoundNBT root) { + static void finish(CompoundTag root) { if (root != null) root.remove(PREPARED); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyPlayerDataFixes.java b/src/main/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes.java similarity index 61% rename from src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyPlayerDataFixes.java rename to src/main/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes.java index b86ab28f..0ef7b81f 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyPlayerDataFixes.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes.java @@ -1,36 +1,36 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; import com.mojang.datafixers.DataFixer; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.INBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.NBTUtil; -import net.minecraft.util.SharedConstants; -import net.minecraft.util.datafix.DefaultTypeReferences; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.SharedConstants; +import net.minecraft.util.datafix.DataFixTypes; /** Keeps saved Forge enchantment names across the native numeric player fixers. */ public final class LegacyPlayerDataFixes { private LegacyPlayerDataFixes() {} - public static CompoundNBT update(DataFixer fixer, DefaultTypeReferences type, CompoundNBT source, int version) { - if (type != DefaultTypeReferences.PLAYER) throw new IllegalArgumentException("Expected native PLAYER type"); - CompoundNBT result = source.copy(); + public static CompoundTag update(DataFixer fixer, DataFixTypes type, CompoundTag source, int version) { + if (type != DataFixTypes.PLAYER) throw new IllegalArgumentException("Expected native PLAYER type"); + CompoundTag result = source.copy(); int current = SharedConstants.getCurrentVersion().getWorldVersion(); if (version >= current) return result; LegacyEntityDataFixes.preparePlayerAttributes(result); if (version < 704) { // Re-read the native namespaced container schema before later choice fixes. - result = NBTUtil.update(fixer, type, result, version, 704); + result = NbtUtils.update(fixer, type, result, version, 704); version = 704; } - result = NBTUtil.update(fixer, type, result, version, current); + result = NbtUtils.update(fixer, type, result, version, current); preserveNames(source, result); return result; } - private static void preserveNames(INBT original, INBT result) { - if (original instanceof CompoundNBT && result instanceof CompoundNBT) { - CompoundNBT before = (CompoundNBT) original, after = (CompoundNBT) result; + private static void preserveNames(Tag original, Tag result) { + if (original instanceof CompoundTag && result instanceof CompoundTag) { + CompoundTag before = (CompoundTag) original, after = (CompoundTag) result; if (isStack(before)) { if (!before.get("Count").equals(after.get("Count")) || (before.contains("Slot") != after.contains("Slot")) @@ -46,8 +46,8 @@ else if (containsStack(before.get(key))) { throw new IllegalStateException("Native player migration removed an inventory branch"); } } - } else if (original instanceof ListNBT && result instanceof ListNBT) { - ListNBT before = (ListNBT) original, after = (ListNBT) result; + } else if (original instanceof ListTag && result instanceof ListTag) { + ListTag before = (ListTag) original, after = (ListTag) result; boolean inventory = containsStack(before) || containsStack(after); if (inventory && before.size() != after.size()) { throw new IllegalStateException("Native player migration changed inventory cardinality"); @@ -60,25 +60,25 @@ else if (containsStack(before.get(key))) { } } - private static boolean isStack(INBT value) { - return value instanceof CompoundNBT && ((CompoundNBT) value).contains("id") - && ((CompoundNBT) value).contains("Count", 99); + private static boolean isStack(Tag value) { + return value instanceof CompoundTag && ((CompoundTag) value).contains("id") + && ((CompoundTag) value).contains("Count", 99); } - private static boolean containsStack(INBT value) { + private static boolean containsStack(Tag value) { if (isStack(value)) return true; - if (value instanceof CompoundNBT) { - CompoundNBT compound = (CompoundNBT) value; + if (value instanceof CompoundTag) { + CompoundTag compound = (CompoundTag) value; for (String key : compound.getAllKeys()) if (containsStack(compound.get(key))) return true; - } else if (value instanceof ListNBT) { - for (INBT child : (ListNBT) value) if (containsStack(child)) return true; + } else if (value instanceof ListTag) { + for (Tag child : (ListTag) value) if (containsStack(child)) return true; } return false; } - private static void restoreNamed(CompoundNBT source, CompoundNBT target, String oldKey, String newKey) { - ListNBT before = source.getCompound("tag").getList(oldKey, 10); - ListNBT after = target.getCompound("tag").getList(newKey, 10); + private static void restoreNamed(CompoundTag source, CompoundTag target, String oldKey, String newKey) { + ListTag before = source.getCompound("tag").getList(oldKey, 10); + ListTag after = target.getCompound("tag").getList(newKey, 10); for (int index = 0; index < before.size(); index++) { if (!before.getCompound(index).contains("id", 8)) continue; if (before.size() != after.size()) { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook.java b/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldDataHook.java similarity index 72% rename from src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook.java rename to src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldDataHook.java index 99a5f885..0b23f1fa 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldDataHook.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; import java.io.File; import java.io.FileInputStream; @@ -25,21 +25,21 @@ import cpw.mods.modlauncher.api.INameMappingService; import cyano.poweradvantage.api.migration.LegacyMigrationApi; -import net.minecraft.block.Block; -import net.minecraft.block.BlockState; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.INBT; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.NBTUtil; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.datafix.fixes.BlockStateFlatteningMap; -import net.minecraft.world.storage.SaveFormat.LevelSave; -import net.minecraft.world.storage.IServerConfiguration; -import net.minecraftforge.fml.WorldPersistenceHooks; -import net.minecraftforge.fml.common.ObfuscationReflectionHelper; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +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.poweradvantage.PowerAdvantage; +import zone.moddev.mc.basepower.PowerAdvantage; /** * Shared Advantage pre-flattening migration hook for Minecraft 1.10 and 1.12 @@ -48,7 +48,7 @@ public final class LegacyWorldDataHook implements WorldPersistenceHooks.WorldPersistenceHook { private static final LegacyWorldDataHook INSTANCE = new LegacyWorldDataHook(); private static final ResourceLocation BLOCK_REGISTRY = new ResourceLocation("minecraft", "blocks"); - private static final Map LEGACY_WORLD_DATA = new ConcurrentHashMap<>(); + private static final Map LEGACY_WORLD_DATA = new ConcurrentHashMap<>(); 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<>()); @@ -76,23 +76,42 @@ public static synchronized void prepareLegacyWorld(File levelDat) { LEGACY_BLOCK_COUNTS.clear(); removeRetiredProfessionSnapshot(levelDat); File source = legacyRegistrySource(levelDat); - if (source == null) return; - try (FileInputStream input = new FileInputStream(source)) { - CompoundNBT root = CompressedStreamTools.readCompressed(input); - if (!root.contains("FML", 10)) return; - prepareLegacyData(levelDat.getParentFile(), root.getCompound("FML")); - migrateLoosePlayerData(levelDat, root); + try { + if (source != null) { + try (FileInputStream input = new FileInputStream(source)) { + CompoundTag registryArchive = NbtIo.readCompressed(input); + prepareLegacyData(levelDat.getParentFile(), registryArchive.getCompound("FML")); + } + } + if (levelDat.isFile()) { + // A backup can supply registry IDs, but never replace newer world data. + try (FileInputStream current = new FileInputStream(levelDat)) { + CompoundTag root = NbtIo.readCompressed(current); + if (source != null || hasAdvantageNamespaceAliases(root)) migrateLoosePlayerData(levelDat, root); + } + } } catch (IOException exception) { - PowerAdvantage.LOGGER.warn("Could not inspect '{}' for legacy Advantage registry data", source, exception); + throw new IllegalStateException("Could not prepare legacy Advantage data in " + levelDat, exception); + } + } + + private static boolean hasAdvantageNamespaceAliases(CompoundTag root) { + CompoundTag registries = root.getCompound("fml").getCompound("Registries"); + for (String registry : registries.getAllKeys()) { + for (Tag entry : registries.getCompound(registry).getList("ids", Tag.TAG_COMPOUND)) { + ResourceLocation id = safeId(((CompoundTag) entry).getString("K")); + if (id != null && !LegacyMigrationApi.registryTarget(id).equals(id)) return true; + } } + return false; } private static void removeRetiredProfessionSnapshot(File levelDat) { if (!levelDat.isFile()) return; try { - CompoundNBT root; + CompoundTag root; try (FileInputStream input = new FileInputStream(levelDat)) { - root = CompressedStreamTools.readCompressed(input); + root = NbtIo.readCompressed(input); } boolean changed = removeRetiredVanillaProfessionRegistry(root); changed |= renameFlattenedBiomeRegistry(root); @@ -102,13 +121,13 @@ private static void removeRetiredProfessionSnapshot(File levelDat) { } } - static boolean renameFlattenedBiomeRegistry(CompoundNBT root) { + static boolean renameFlattenedBiomeRegistry(CompoundTag root) { // Uppercase FML remains the untouched pre-flattening registry archive. if (root.getCompound("Data").getInt("DataVersion") < 1519) return false; - CompoundNBT registries = root.getCompound("fml").getCompound("Registries"); + CompoundTag registries = root.getCompound("fml").getCompound("Registries"); String old = "minecraft:biomes", current = "minecraft:worldgen/biome"; if (!registries.contains(old, 10)) return false; - CompoundNBT snapshot = registries.getCompound(old); + CompoundTag snapshot = registries.getCompound(old); if (registries.contains(current) && !snapshot.equals(registries.get(current))) { throw new IllegalStateException("Conflicting old and current biome registry snapshots; preserve the world backup and resolve the registry conflict before loading"); } @@ -117,22 +136,22 @@ static boolean renameFlattenedBiomeRegistry(CompoundNBT root) { return true; } - static boolean removeRetiredVanillaProfessionRegistry(CompoundNBT root) { + static boolean removeRetiredVanillaProfessionRegistry(CompoundTag root) { boolean changed = false; for (String namespace : new String[] {"fml", "FML"}) { - CompoundNBT registries = root.getCompound(namespace).getCompound("Registries"); + CompoundTag registries = root.getCompound(namespace).getCompound("Registries"); String key = "minecraft:villagerprofessions"; if (!registries.contains(key, 10)) continue; - CompoundNBT snapshot = registries.getCompound(key); + CompoundTag snapshot = registries.getCompound(key); if (!snapshot.contains("ids", 9)) continue; boolean vanillaOnly = true; - for (INBT entry : (ListNBT) snapshot.get("ids")) { - if (!(entry instanceof CompoundNBT) || !Arrays.asList("minecraft:farmer", "minecraft:librarian", + for (Tag entry : (ListTag) snapshot.get("ids")) { + if (!(entry instanceof CompoundTag) || !Arrays.asList("minecraft:farmer", "minecraft:librarian", "minecraft:priest", "minecraft:smith", "minecraft:butcher", "minecraft:nitwit") - .contains(((CompoundNBT) entry).getString("K"))) vanillaOnly = false; + .contains(((CompoundTag) entry).getString("K"))) vanillaOnly = false; } for (String list : new String[] {"aliases", "overrides", "dummied"}) { - if (snapshot.contains(list, 9) && !((ListNBT) snapshot.get(list)).isEmpty()) vanillaOnly = false; + if (snapshot.contains(list, 9) && !((ListTag) snapshot.get(list)).isEmpty()) vanillaOnly = false; } if (snapshot.getIntArray("blocked").length > 0 || !vanillaOnly) continue; // Vanilla fixes the villagers and offers. Remove only the retired vanilla ID index. @@ -143,11 +162,13 @@ static boolean removeRetiredVanillaProfessionRegistry(CompoundNBT root) { } /** Called by the coremod immediately after a legacy chunk NBT is read. */ - public static synchronized void prepareLegacyChunk(CompoundNBT root) { + public static synchronized void prepareLegacyChunk(CompoundTag root) { if (root == null) return; migrateLegacyNbt(root); - if (!legacyWorldActive || !root.contains("Level", 10)) return; - CompoundNBT level = root.getCompound("Level"); + if (!root.contains("Level", Tag.TAG_COMPOUND)) return; + CompoundTag level = root.getCompound("Level"); + remapFlattenedPalette(level); + if (!legacyWorldActive) return; int legacyBlocks = countLegacyBlocks(level); if (legacyBlocks > 0) { LEGACY_BLOCK_COUNTS.put(Long.valueOf(chunkKey(level.getInt("xPos"), level.getInt("zPos"))), @@ -158,13 +179,41 @@ public static synchronized void prepareLegacyChunk(CompoundNBT root) { level.putBoolean(PRESERVE_CHUNK_MARKER, true); } + static void remapFlattenedPalette(CompoundTag level) { + for (Tag section : level.getList("Sections", Tag.TAG_COMPOUND)) { + for (Tag state : ((CompoundTag) section).getList("Palette", Tag.TAG_COMPOUND)) { + CompoundTag blockState = (CompoundTag) state; + ResourceLocation original = safeId(blockState.getString("Name")); + if (original == null) continue; + ResourceLocation target = LegacyMigrationApi.blockTarget(original); + if (target.equals(original)) continue; + if (!ForgeRegistries.BLOCKS.containsKey(target)) { + throw new IllegalStateException("No replacement for saved Advantage block " + original); + } + blockState.putString("Name", target.toString()); + } + } + } + /** Detaches pending-save data before either mod's migration hook can mutate it. */ - public static CompoundNBT copyChunkForMigration(CompoundNBT root) { + public static CompoundTag copyChunkForMigration(CompoundTag root) { return root == null ? null : root.copy(); } + /** Native 1.17 stores entities separately; never mutate an I/O worker's cached data. */ + public static CompoundTag prepareEntityChunkForMigration(CompoundTag source) { + if (source == null || !source.contains("Position", 11) + || source.getIntArray("Position").length != 2 || !source.contains("Entities", 9)) { + throw new IllegalStateException("Malformed saved entity chunk; refusing unsafe construction"); + } + CompoundTag root = source.copy(); + migrateLegacyNbt(root); + LegacyEntityDataFixes.prepare(root); + return root; + } + /** Called after all mods' entry hooks, immediately before native chunk datafixing. */ - public static void prepareLegacyEntities(CompoundNBT root) { + public static void prepareLegacyEntities(CompoundTag root) { if (root != null) { LegacyMachineItemDataFixes.prepare(root); LegacyEntityDataFixes.prepare(root); @@ -172,11 +221,11 @@ public static void prepareLegacyEntities(CompoundNBT root) { } /** Called after vanilla datafixing, before a legacy chunk is returned. */ - public static CompoundNBT finalizeLegacyChunk(CompoundNBT root) { + public static CompoundTag finalizeLegacyChunk(CompoundTag root) { LegacyEntityDataFixes.finish(root); LegacyMachineItemDataFixes.finish(root); if (root != null && root.contains("Level", 10)) { - CompoundNBT level = root.getCompound("Level"); + CompoundTag level = root.getCompound("Level"); if (level.getBoolean(PRESERVE_CHUNK_MARKER)) { level.putString("Status", "full"); level.remove(PRESERVE_CHUNK_MARKER); @@ -185,7 +234,7 @@ public static CompoundNBT finalizeLegacyChunk(CompoundNBT root) { return root; } - public static boolean shouldBlockWorldgenWrite(net.minecraft.util.math.BlockPos position) { + public static boolean shouldBlockWorldgenWrite(net.minecraft.core.BlockPos position) { return legacyWorldActive && position != null && LEGACY_CHUNKS.contains(Long.valueOf(chunkKey(position.getX() >> 4, position.getZ() >> 4))); } @@ -193,19 +242,19 @@ public static boolean shouldBlockWorldgenWrite(net.minecraft.util.math.BlockPos @Override public String getModId() { return "FML"; } @Override - public CompoundNBT getDataForWriting(LevelSave handler, IServerConfiguration info) { - CompoundNBT legacy = LEGACY_WORLD_DATA.get(worldKey(handler.getWorldDir().toFile())); - return legacy == null ? new CompoundNBT() : legacy.copy(); + 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(LevelSave handler, IServerConfiguration info, CompoundNBT tag) { + public void readData(LevelStorageAccess handler, WorldData info, CompoundTag tag) { prepareLegacyData(handler.getWorldDir().toFile(), tag); } - private static synchronized void prepareLegacyData(File worldDirectory, CompoundNBT tag) { + private static synchronized void prepareLegacyData(File worldDirectory, CompoundTag tag) { if (!tag.contains("Registries", 10)) return; - CompoundNBT registries = tag.getCompound("Registries"); + CompoundTag registries = tag.getCompound("Registries"); if (!registries.contains(BLOCK_REGISTRY.toString(), 10)) return; installLegacyEnchantments(registries); String key = worldKey(worldDirectory); @@ -221,16 +270,19 @@ private static synchronized void prepareLegacyData(File worldDirectory, Compound } } - private static int installLegacyBlockStates(CompoundNBT snapshot) { + private static int installLegacyBlockStates(CompoundTag snapshot) { LEGACY_BLOCK_IDS.clear(); Map ids = new LinkedHashMap<>(); - ListNBT savedIds = snapshot.getList("ids", 10); + ListTag savedIds = snapshot.getList("ids", 10); int highestState = 0; for (int index = 0; index < savedIds.size(); index++) { - CompoundNBT entry = savedIds.getCompound(index); + CompoundTag entry = savedIds.getCompound(index); ResourceLocation id = safeId(normalizeLegacyRegistryName(entry.getString("K"))); if (id == null || !LegacyMigrationApi.handlesNamespace(id.getNamespace())) continue; int numeric = entry.getInt("V"); + if (numeric < 0 || numeric > 4095) { + throw new IllegalStateException("Invalid legacy block ID for " + id + ": " + numeric); + } ids.put(id, Integer.valueOf(numeric)); LEGACY_BLOCK_IDS.set(numeric); highestState = Math.max(highestState, (numeric << 4) | 15); @@ -242,40 +294,40 @@ private static int installLegacyBlockStates(CompoundNBT snapshot) { for (Map.Entry entry : ids.entrySet()) { ResourceLocation target = LegacyMigrationApi.blockTarget(entry.getKey()); Block block = ForgeRegistries.BLOCKS.getValue(target); - if (block == null) { + if (!ForgeRegistries.BLOCKS.containsKey(target) || block == null) { throw new IllegalStateException("No Advantage replacement for legacy block " + entry.getKey() + " (expected " + target + ")"); } for (int metadata = 0; metadata < 16; metadata++) { BlockState state = LegacyMigrationApi.decodeBlockState(entry.getKey(), block, metadata); table[(entry.getValue().intValue() << 4) | metadata] = - BlockStateFlatteningMap.parse(NBTUtil.writeBlockState(state).toString()); + BlockStateData.parse(NbtUtils.writeBlockState(state).toString()); mapped++; } } return mapped; } - private static void installLegacyEnchantments(CompoundNBT registries) { + private static void installLegacyEnchantments(CompoundTag registries) { LEGACY_ENCHANTMENTS.clear(); - ListNBT entries = registries.getCompound("minecraft:enchantments").getList("ids", 10); + ListTag entries = registries.getCompound("minecraft:enchantments").getList("ids", 10); for (int index = 0; index < entries.size(); index++) { - CompoundNBT entry = entries.getCompound(index); + CompoundTag entry = entries.getCompound(index); ResourceLocation name = safeId(entry.getString("K")); if (name != null) LEGACY_ENCHANTMENTS.put(Integer.valueOf(entry.getInt("V")), name.toString()); } } /** Resolve saved IDs, not today's registry order or vanilla's conflicting numeric IDs. */ - static boolean migrateLegacyEnchantments(CompoundNBT compound, Map savedIds) { + static boolean migrateLegacyEnchantments(CompoundTag compound, Map savedIds) { boolean changed = migrateEnchantmentList(compound, "StoredEnchantments", savedIds); changed |= migrateEnchantmentList(compound, "Enchantments", savedIds); if (compound.contains("ench", 9)) { - ListNBT enchantments = compound.getList("ench", 10); + ListTag enchantments = compound.getList("ench", 10); if (!enchantments.isEmpty() && !savedIds.isEmpty()) { // A partial conversion would leave an unreadable mixed old/new list. for (int index = 0; index < enchantments.size(); index++) { - CompoundNBT enchantment = enchantments.getCompound(index); + CompoundTag enchantment = enchantments.getCompound(index); if (enchantment.contains("id", 99) && !savedIds.containsKey(Integer.valueOf(enchantment.getInt("id")))) { throw new IllegalStateException("Missing saved enchantment mapping for " + enchantment.getInt("id")); @@ -290,22 +342,26 @@ static boolean migrateLegacyEnchantments(CompoundNBT compound, Map savedIds) { + private static boolean migrateEnchantmentList(CompoundTag compound, String key, Map savedIds) { boolean changed = false; - ListNBT list = compound.getList(key, 10); + ListTag list = compound.getList(key, 10); for (int index = 0; index < list.size(); index++) { - CompoundNBT enchantment = list.getCompound(index); - if (!enchantment.contains("id", 99)) continue; - String name = savedIds.get(Integer.valueOf(enchantment.getInt("id"))); - if (name != null) { - enchantment.putString("id", name); + CompoundTag enchantment = list.getCompound(index); + boolean numeric = enchantment.contains("id", 99); + String name = numeric ? savedIds.get(Integer.valueOf(enchantment.getInt("id"))) + : enchantment.getString("id"); + ResourceLocation id = safeId(name); + if (id == null) continue; + String target = LegacyMigrationApi.registryTarget(id).toString(); + if (numeric || !target.equals(name)) { + enchantment.putString("id", target); changed = true; } } return changed; } - private static void migrateLoosePlayerData(File levelDat, CompoundNBT root) throws IOException { + private static void migrateLoosePlayerData(File levelDat, CompoundTag root) throws IOException { boolean changed = migrateLegacyNbt(root) > 0; if (changed && levelDat.isFile()) writeWithBackup(levelDat, root); File playerData = new File(levelDat.getParentFile(), "playerdata"); @@ -314,16 +370,16 @@ private static void migrateLoosePlayerData(File levelDat, CompoundNBT root) thro Arrays.sort(files); for (File file : files) { try (FileInputStream input = new FileInputStream(file)) { - CompoundNBT player = CompressedStreamTools.readCompressed(input); + CompoundTag player = NbtIo.readCompressed(input); if (migrateLegacyNbt(player) > 0) writeWithBackup(file, player); } } } - private static int migrateLegacyNbt(INBT value) { + private static int migrateLegacyNbt(Tag value) { int changed = 0; - if (value instanceof CompoundNBT) { - CompoundNBT compound = (CompoundNBT) value; + if (value instanceof CompoundTag) { + CompoundTag compound = (CompoundTag) value; String id = compound.getString("id"); String vanillaBlockEntity = VANILLA_BLOCK_ENTITY_IDS.get(id); if (vanillaBlockEntity != null) { @@ -341,7 +397,7 @@ private static int migrateLegacyNbt(INBT value) { } if ("forge:bucketfilled".equalsIgnoreCase(id) && compound.contains("tag", 10)) { - CompoundNBT tag = compound.getCompound("tag"); + CompoundTag tag = compound.getCompound("tag"); ResourceLocation bucket = LegacyMigrationApi.bucketTarget(tag.getString("FluidName")); if (bucket != null) { compound.putString("id", bucket.toString()); @@ -355,13 +411,17 @@ private static int migrateLegacyNbt(INBT value) { if (oldId != null && LegacyMigrationApi.handlesNamespace(oldId.getNamespace())) { ResourceLocation target = LegacyMigrationApi.itemTarget(oldId); if (!target.equals(oldId)) { + if (!ForgeRegistries.ITEMS.containsKey(target)) { + throw new IllegalStateException("No replacement for saved Advantage item " + + oldId + " (expected " + target + ")"); + } compound.putString("id", target.toString()); changed++; } if (LegacyMigrationApi.isDurableItem(oldId) && compound.contains("Damage", 99) && compound.getInt("Damage") > 0) { - CompoundNBT tag = compound.contains("tag", 10) - ? compound.getCompound("tag") : new CompoundNBT(); + CompoundTag tag = compound.contains("tag", 10) + ? compound.getCompound("tag") : new CompoundTag(); if (!tag.contains("Damage", 99)) { tag.putInt("Damage", compound.getInt("Damage")); compound.put("tag", tag); @@ -376,17 +436,17 @@ private static int migrateLegacyNbt(INBT value) { if (migrateLegacyEnchantments(compound, LEGACY_ENCHANTMENTS)) changed++; if (LegacyMigrationApi.migrateRegisteredNbt(compound)) changed++; for (String key : new ArrayList<>(compound.getAllKeys())) { - INBT child = compound.get(key); + Tag child = compound.get(key); if (child != null) changed += migrateLegacyNbt(child); } - } else if (value instanceof ListNBT) { - ListNBT list = (ListNBT) value; + } else if (value instanceof ListTag) { + ListTag list = (ListTag) value; for (int index = 0; index < list.size(); index++) changed += migrateLegacyNbt(list.get(index)); } return changed; } - private static int migrateFluidName(CompoundNBT compound, String key) { + private static int migrateFluidName(CompoundTag compound, String key) { if (!compound.contains(key, 8)) return 0; String oldName = compound.getString(key); String target = LegacyMigrationApi.fluidTarget(oldName); @@ -399,15 +459,15 @@ private static File legacyRegistrySource(File levelDat) { for (File candidate : new File[] { levelDat, new File(levelDat.getParentFile(), "level.dat_old") }) { if (!candidate.isFile()) continue; try (FileInputStream input = new FileInputStream(candidate)) { - CompoundNBT root = CompressedStreamTools.readCompressed(input); + CompoundTag root = NbtIo.readCompressed(input); if (root.contains("FML", 10) && containsLegacyRegistryEntry(root)) return candidate; } catch (IOException ignored) {} } return null; } - private static boolean containsLegacyRegistryEntry(CompoundNBT root) { - ListNBT ids = root.getCompound("FML").getCompound("Registries") + private static boolean containsLegacyRegistryEntry(CompoundTag root) { + ListTag ids = root.getCompound("FML").getCompound("Registries") .getCompound(BLOCK_REGISTRY.toString()).getList("ids", 10); for (int index = 0; index < ids.size(); index++) { ResourceLocation id = safeId(normalizeLegacyRegistryName(ids.getCompound(index).getString("K"))); @@ -416,11 +476,11 @@ private static boolean containsLegacyRegistryEntry(CompoundNBT root) { return false; } - private static int countLegacyBlocks(CompoundNBT level) { + private static int countLegacyBlocks(CompoundTag level) { int found = 0; - ListNBT sections = level.getList("Sections", 10); + ListTag sections = level.getList("Sections", 10); for (int sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++) { - CompoundNBT section = sections.getCompound(sectionIndex); + CompoundTag section = sections.getCompound(sectionIndex); byte[] blocks = section.getByteArray("Blocks"); if (blocks.length != 4096) continue; byte[] add = section.getByteArray("Add"); @@ -472,12 +532,12 @@ private static int indexLegacyChunks(File worldDirectory) { @SuppressWarnings("unchecked") private static Dynamic[] expandFlatteningTable(int requiredLength) { try { - String fieldName = ObfuscationReflectionHelper.remapName(INameMappingService.Domain.FIELD, "field_199200_b"); - Field valuesField = BlockStateFlatteningMap.class.getDeclaredField(fieldName); + String fieldName = ObfuscationReflectionHelper.remapName(INameMappingService.Domain.FIELD, "f_14934_"); + Field valuesField = BlockStateData.class.getDeclaredField(fieldName); valuesField.setAccessible(true); - Field modifiersField = Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(valuesField, valuesField.getModifiers() & ~Modifier.FINAL); + if (Modifier.isFinal(valuesField.getModifiers())) { + throw new IllegalStateException("The Advantage flattening-table hook did not run"); + } Dynamic[] current = (Dynamic[]) valuesField.get(null); if (current.length >= requiredLength) return current; Dynamic[] expanded = Arrays.copyOf(current, requiredLength); @@ -488,12 +548,12 @@ private static Dynamic[] expandFlatteningTable(int requiredLength) { } } - private static void writeWithBackup(File file, CompoundNBT data) throws IOException { - File backup = new File(file.getParentFile(), file.getName() + ".poweradvantage-legacy-backup"); + private static void writeWithBackup(File file, CompoundTag data) throws IOException { + File backup = new File(file.getParentFile(), file.getName() + ".basepower-legacy-backup"); if (!backup.exists()) Files.copy(file.toPath(), backup.toPath(), StandardCopyOption.COPY_ATTRIBUTES); - File temporary = new File(file.getParentFile(), file.getName() + ".poweradvantage.tmp"); + File temporary = new File(file.getParentFile(), file.getName() + ".basepower.tmp"); try (FileOutputStream output = new FileOutputStream(temporary)) { - CompressedStreamTools.writeCompressed(data, output); + NbtIo.writeCompressed(data, output); } try { Files.move(temporary.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING, @@ -504,7 +564,7 @@ private static void writeWithBackup(File file, CompoundNBT data) throws IOExcept } private static void writeMigrationReport(File worldDirectory, int states, int chunks) { - File report = new File(worldDirectory, "poweradvantage-migration-report.txt"); + File report = new File(worldDirectory, "basepower-migration-report.txt"); if (report.exists()) return; List lines = Arrays.asList( "Advantage 1.14 legacy migration", diff --git a/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldMetadata.java b/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldMetadata.java new file mode 100644 index 00000000..ecca8515 --- /dev/null +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldMetadata.java @@ -0,0 +1,69 @@ +package zone.moddev.mc.basepower.migration; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +import cyano.poweradvantage.api.migration.LegacyMigrationApi; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.fml.ModList; + +/** Translate renamed mods before vanilla selects packs or Forge reads its saved mod list. */ +public final class LegacyWorldMetadata { + private LegacyWorldMetadata() {} + + public static CompoundTag read(File levelDat) throws IOException { + CompoundTag root = NbtIo.readCompressed(levelDat); + remap(root, 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<>(); + for (int i = 0; i < mods.size(); ++i) recordedMods.add(mods.getCompound(i).getString("ModId")); + for (int i = 0; i < mods.size(); ++i) { + CompoundTag mod = mods.getCompound(i); + String id = mod.getString("ModId"); + String target = target(id, loaded); + if (target.equals(id)) continue; + if (recordedMods.add(target)) mod.putString("ModId", target); + else mods.remove(i--); + } + + CompoundTag packs = root.getCompound("Data").getCompound("DataPacks"); + ListTag enabled = packs.getList("Enabled", Tag.TAG_STRING); + ListTag disabled = packs.getList("Disabled", Tag.TAG_STRING); + Set recordedPacks = new HashSet<>(); + enabled.forEach(pack -> recordedPacks.add(pack.getAsString())); + disabled.forEach(pack -> recordedPacks.add(pack.getAsString())); + remapPacks(enabled, recordedPacks, loaded); + remapPacks(disabled, recordedPacks, loaded); + } + + private static void remapPacks(ListTag packs, Set recorded, Predicate loaded) { + for (int i = 0; i < packs.size(); ++i) { + String pack = packs.getString(i); + if (!pack.startsWith("mod:")) continue; + String replacement = "mod:" + target(pack.substring(4), loaded); + if (replacement.equals(pack)) continue; + if (recorded.add(replacement)) packs.set(i, StringTag.valueOf(replacement)); + else packs.remove(i--); + } + } + + private static String target(String id, Predicate loaded) { + if (id.isEmpty() || id.indexOf(':') >= 0) return id; + ResourceLocation key = ResourceLocation.tryParse(id + ":metadata"); + if (key == null) return id; + String replacement = LegacyMigrationApi.registryTarget(key).getNamespace(); + return !replacement.equals(id) && loaded.test(replacement) && !loaded.test(id) ? replacement : id; + } +} diff --git a/src/main/java/zone/moddev/mc/poweradvantage/migration/VanillaRegistryRemaps.java b/src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java similarity index 50% rename from src/main/java/zone/moddev/mc/poweradvantage/migration/VanillaRegistryRemaps.java rename to src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java index 5d04fde8..eafecda3 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/migration/VanillaRegistryRemaps.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java @@ -1,36 +1,91 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; -import net.minecraft.entity.EntityType; -import net.minecraft.entity.ai.brain.memory.MemoryModuleType; -import net.minecraft.item.Item; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.SoundEvent; -import net.minecraft.world.biome.Biome; +import java.util.function.UnaryOperator; +import cyano.poweradvantage.api.migration.LegacyMigrationApi; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.level.biome.Biome; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.RegistryEvent; import net.minecraftforge.registries.IForgeRegistryEntry; import net.minecraftforge.registries.IForgeRegistry; import net.minecraftforge.registries.ForgeRegistries; -import zone.moddev.mc.poweradvantage.PowerAdvantage; +import zone.moddev.mc.basepower.PowerAdvantage; /** Vanilla's data fixers rename saved data, but not Forge's registry snapshot. */ public final class VanillaRegistryRemaps { private VanillaRegistryRemaps() {} public static void register() { + MinecraftForge.EVENT_BUS.addGenericListener(Block.class, VanillaRegistryRemaps::blocks); MinecraftForge.EVENT_BUS.addGenericListener(Item.class, VanillaRegistryRemaps::items); MinecraftForge.EVENT_BUS.addGenericListener(EntityType.class, VanillaRegistryRemaps::entities); MinecraftForge.EVENT_BUS.addGenericListener(SoundEvent.class, VanillaRegistryRemaps::sounds); MinecraftForge.EVENT_BUS.addGenericListener(Biome.class, VanillaRegistryRemaps::biomes); MinecraftForge.EVENT_BUS.addGenericListener(MemoryModuleType.class, VanillaRegistryRemaps::memories); + MinecraftForge.EVENT_BUS.addGenericListener(BlockEntityType.class, VanillaRegistryRemaps::blockEntities); + MinecraftForge.EVENT_BUS.addGenericListener(Fluid.class, VanillaRegistryRemaps::fluids); + MinecraftForge.EVENT_BUS.addGenericListener(Enchantment.class, VanillaRegistryRemaps::enchantments); + MinecraftForge.EVENT_BUS.addGenericListener(MenuType.class, VanillaRegistryRemaps::menus); + MinecraftForge.EVENT_BUS.addGenericListener(RecipeSerializer.class, VanillaRegistryRemaps::recipeSerializers); } - private static void items(RegistryEvent.MissingMappings event) { remap(event, ForgeRegistries.ITEMS); } - private static void entities(RegistryEvent.MissingMappings> event) { remap(event, ForgeRegistries.ENTITIES); } + private static void blocks(RegistryEvent.MissingMappings event) { + remap(event, ForgeRegistries.BLOCKS); + remapAdvantage(event, ForgeRegistries.BLOCKS, LegacyMigrationApi::blockTarget); + } + private static void items(RegistryEvent.MissingMappings event) { + remap(event, ForgeRegistries.ITEMS); + remapAdvantage(event, ForgeRegistries.ITEMS, LegacyMigrationApi::itemTarget); + } + private static void entities(RegistryEvent.MissingMappings> event) { + remap(event, ForgeRegistries.ENTITIES); + remapAdvantage(event, ForgeRegistries.ENTITIES, + id -> new ResourceLocation(LegacyMigrationApi.entityTarget(id.toString()))); + } private static void sounds(RegistryEvent.MissingMappings event) { remap(event, ForgeRegistries.SOUND_EVENTS); } private static void biomes(RegistryEvent.MissingMappings event) { remap(event, ForgeRegistries.BIOMES); } private static void memories(RegistryEvent.MissingMappings> event) { remap(event, ForgeRegistries.MEMORY_MODULE_TYPES); } + private static void blockEntities(RegistryEvent.MissingMappings> event) { + remapAdvantage(event, ForgeRegistries.BLOCK_ENTITIES, + id -> new ResourceLocation(LegacyMigrationApi.tileTarget(id.toString()))); + } + private static void fluids(RegistryEvent.MissingMappings event) { + remapAdvantage(event, ForgeRegistries.FLUIDS, + id -> new ResourceLocation(LegacyMigrationApi.fluidTarget(id.toString()))); + } + private static void enchantments(RegistryEvent.MissingMappings event) { + remapAdvantage(event, ForgeRegistries.ENCHANTMENTS, LegacyMigrationApi::registryTarget); + } + private static void menus(RegistryEvent.MissingMappings> event) { + remapAdvantage(event, ForgeRegistries.CONTAINERS, LegacyMigrationApi::registryTarget); + } + private static void recipeSerializers(RegistryEvent.MissingMappings> event) { + remapAdvantage(event, ForgeRegistries.RECIPE_SERIALIZERS, LegacyMigrationApi::registryTarget); + } + + private static > void remapAdvantage(RegistryEvent.MissingMappings event, + IForgeRegistry active, UnaryOperator replacement) { + for (RegistryEvent.MissingMappings.Mapping mapping : event.getAllMappings()) { + if (mapping.getAction() != RegistryEvent.MissingMappings.Action.DEFAULT) continue; + ResourceLocation target = replacement.apply(mapping.key); + if (!mapping.key.equals(target) && active.containsKey(target)) { + mapping.remap(active.getValue(target)); + PowerAdvantage.LOGGER.info("Migrated Advantage registry entry {} to {}", mapping.key, target); + } + } + } + static > void remap(RegistryEvent.MissingMappings event, IForgeRegistry active) { for (RegistryEvent.MissingMappings.Mapping mapping : event.getMappings("minecraft")) { if (mapping.getAction() != RegistryEvent.MissingMappings.Action.DEFAULT) continue; @@ -62,7 +117,10 @@ static boolean retiredTransientMemory(ResourceLocation registry, ResourceLocatio static ResourceLocation replacement(ResourceLocation registry, ResourceLocation old) { if (!"minecraft".equals(old.getNamespace())) return null; String target = null; - if (registry.equals(new ResourceLocation("minecraft:entity_type")) && old.getPath().equals("zombie_pigman")) { + if ((registry.equals(new ResourceLocation("minecraft:block")) || registry.equals(new ResourceLocation("minecraft:item"))) + && old.getPath().equals("grass_path")) { + target = "dirt_path"; + } else if (registry.equals(new ResourceLocation("minecraft:entity_type")) && old.getPath().equals("zombie_pigman")) { target = "zombified_piglin"; } else if (registry.equals(new ResourceLocation("minecraft:item")) && old.getPath().equals("zombie_pigman_spawn_egg")) { target = "zombified_piglin_spawn_egg"; @@ -74,6 +132,7 @@ static ResourceLocation replacement(ResourceLocation registry, ResourceLocation } // The former single Nether biome becomes Nether Wastes in 1.16. if (old.getPath().equals("music.nether")) target = "music.nether.nether_wastes"; + if (old.getPath().equals("item.sweet_berries.pick_from_bush")) target = "block.sweet_berry_bush.pick_berries"; } return target == null ? null : new ResourceLocation("minecraft", target); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/package-info.java b/src/main/java/zone/moddev/mc/basepower/package-info.java similarity index 81% rename from src/main/java/zone/moddev/mc/poweradvantage/package-info.java rename to src/main/java/zone/moddev/mc/basepower/package-info.java index b594f496..e4cdd203 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/package-info.java +++ b/src/main/java/zone/moddev/mc/basepower/package-info.java @@ -2,4 +2,4 @@ This package contains the main mod class. If you are making an add-on mod, you probably want to use classes from the cyano.poweradvantage.api.simple package */ -package zone.moddev.mc.poweradvantage; \ No newline at end of file +package zone.moddev.mc.basepower; \ No newline at end of file diff --git a/src/main/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPack.java b/src/main/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPack.java new file mode 100644 index 00000000..87bf29bb --- /dev/null +++ b/src/main/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPack.java @@ -0,0 +1,94 @@ +package zone.moddev.mc.basepower.recipe; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.metadata.MetadataSectionSerializer; +import net.minecraft.server.packs.metadata.pack.PackMetadataSection; +import net.minecraftforge.fml.ModList; +import zone.moddev.mc.basepower.PowerAdvantage; +import zone.moddev.mc.basepower.RecipeMode; + +/** Additive native tags: server policy is resolved and synchronized by vanilla. */ +final class MaterialPolicyPack implements PackResources { + private static final String NAME = "Power Advantage material aliases"; + private final Map resources = new LinkedHashMap<>(); + + MaterialPolicyPack() { + // Pack instances are created for each reload, after common config loading. + this(PowerAdvantage.plasticIsAlsoRubber, + PowerAdvantage.recipeMode == RecipeMode.NORMAL && ModList.get().isLoaded("baseelectric")); + } + + MaterialPolicyPack(boolean plasticIsRubber, boolean normalSolder) { + JsonArray rubber = new JsonArray(); + if (plasticIsRubber) rubber.add("basepower:bioplastic_ingot"); + put("rubber", rubber); + put("ingots/rubber", rubber); + JsonArray solder = new JsonArray(); + if (normalSolder) { + for (String metal : new String[] {"lead", "tin", "silver"}) { + JsonObject optional = new JsonObject(); + optional.addProperty("id", "#forge:ingots/" + metal); + optional.addProperty("required", false); + solder.add(optional); + } + } + put("solder", solder); + } + + private void put(String path, JsonArray values) { + JsonObject tag = new JsonObject(); + tag.addProperty("replace", false); + tag.add("values", values); + resources.put(new ResourceLocation("forge", "tags/items/" + path + ".json"), tag.toString()); + } + + @Override public InputStream getRootResource(String name) throws FileNotFoundException { + throw new FileNotFoundException(name); + } + + @Override public InputStream getResource(PackType type, ResourceLocation id) throws FileNotFoundException { + if (!hasResource(type, id)) throw new FileNotFoundException(id.toString()); + return new ByteArrayInputStream(resources.get(id).getBytes(StandardCharsets.UTF_8)); + } + + @Override public Collection getResources(PackType type, String namespace, + String prefix, int depth, Predicate filter) { + if (type != PackType.SERVER_DATA) return Collections.emptyList(); + java.util.List found = new java.util.ArrayList<>(); + for (ResourceLocation id : resources.keySet()) { + String path = id.getPath(); + if (namespace.equals(id.getNamespace()) && path.startsWith(prefix + "/") + && filter.test(path.substring(path.lastIndexOf('/') + 1))) found.add(id); + } + return found; + } + + @Override public boolean hasResource(PackType type, ResourceLocation id) { + return type == PackType.SERVER_DATA && resources.containsKey(id); + } + @Override public Set getNamespaces(PackType type) { + return type == PackType.SERVER_DATA ? Collections.singleton("forge") : Collections.emptySet(); + } + @Override @SuppressWarnings("unchecked") + public T getMetadataSection(MetadataSectionSerializer serializer) { + return serializer == PackMetadataSection.SERIALIZER + ? (T)new PackMetadataSection(new TextComponent(NAME), 7) : 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 new file mode 100644 index 00000000..721cfbc7 --- /dev/null +++ b/src/main/java/zone/moddev/mc/basepower/recipe/MaterialTags.java @@ -0,0 +1,58 @@ +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() {} + + public static void registerPack(net.minecraftforge.event.AddPackFindersEvent event) { + if (event.getPackType() != net.minecraft.server.packs.PackType.SERVER_DATA) return; + event.addRepositorySource((consumer, constructor) -> { + net.minecraft.server.packs.repository.Pack pack = net.minecraft.server.packs.repository.Pack.create( + "basepower:material_policy", true, MaterialPolicyPack::new, constructor, + net.minecraft.server.packs.repository.Pack.Position.BOTTOM, + net.minecraft.server.packs.repository.PackSource.BUILT_IN); + if (pack != null) consumer.accept(pack); + }); + } + + 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/poweradvantage/recipe/PartsLoot.java b/src/main/java/zone/moddev/mc/basepower/recipe/PartsLoot.java similarity index 64% rename from src/main/java/zone/moddev/mc/poweradvantage/recipe/PartsLoot.java rename to src/main/java/zone/moddev/mc/basepower/recipe/PartsLoot.java index 78b7c0b3..cc1bef70 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/recipe/PartsLoot.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/PartsLoot.java @@ -1,15 +1,15 @@ -package zone.moddev.mc.poweradvantage.recipe; +package zone.moddev.mc.basepower.recipe; import java.util.Arrays; import java.util.HashSet; import java.util.Set; -import net.minecraft.util.ResourceLocation; -import net.minecraft.loot.ConstantRange; -import net.minecraft.loot.LootPool; -import net.minecraft.loot.TableLootEntry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.storage.loot.providers.number.ConstantValue; +import net.minecraft.world.level.storage.loot.LootPool; +import net.minecraft.world.level.storage.loot.entries.LootTableReference; import net.minecraftforge.event.LootTableLoadEvent; import net.minecraftforge.fml.ModList; -import zone.moddev.mc.poweradvantage.PowerAdvantage; +import zone.moddev.mc.basepower.PowerAdvantage; /** Forge emits this event for bundled tables, leaving user data-pack tables untouched. */ public final class PartsLoot { @@ -24,17 +24,17 @@ public static void onLoad(LootTableLoadEvent event) { mod -> ModList.get().isLoaded(mod)); } - static void addPools(net.minecraft.loot.LootTable table, ResourceLocation name, + static void addPools(net.minecraft.world.level.storage.loot.LootTable table, ResourceLocation name, boolean enabled, java.util.function.Predicate loaded) { if (!"minecraft".equals(name.getNamespace()) || !CHESTS.contains(name.getPath()) || !enabled) return; - for (String mod : new String[] {"poweradvantage", "steamadvantage", "electricadvantage"}) { + for (String mod : new String[] {"basepower", "basesteam", "baseelectric"}) { if (!loaded.test(mod) - || (mod.equals("poweradvantage") && name.getPath().equals("chests/end_city_treasure"))) continue; + || (mod.equals("basepower") && name.getPath().equals("chests/end_city_treasure"))) continue; String poolName = mod + ":legacy_parts"; if (table.getPool(poolName) != null) continue; - table.addPool(LootPool.lootPool().name(poolName) .setRolls(ConstantRange.exactly(1)) - .add(TableLootEntry.lootTableReference(new ResourceLocation(mod, "chests/advantage_parts"))).build()); + table.addPool(LootPool.lootPool().name(poolName) .setRolls(ConstantValue.exactly(1)) + .add(LootTableReference.lootTableReference(new ResourceLocation(mod, "chests/advantage_parts"))).build()); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeConditions.java b/src/main/java/zone/moddev/mc/basepower/recipe/RecipeConditions.java similarity index 77% rename from src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeConditions.java rename to src/main/java/zone/moddev/mc/basepower/recipe/RecipeConditions.java index bb129c3f..515af461 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeConditions.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/RecipeConditions.java @@ -1,15 +1,15 @@ -package zone.moddev.mc.poweradvantage.recipe; +package zone.moddev.mc.basepower.recipe; import com.google.gson.JsonObject; import java.util.Locale; import net.minecraft.tags.ItemTags; -import net.minecraft.util.JSONUtils; -import net.minecraft.util.ResourceLocation; +import net.minecraft.util.GsonHelper; +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.common.crafting.CraftingHelper; import net.minecraftforge.common.crafting.conditions.ICondition; import net.minecraftforge.common.crafting.conditions.IConditionSerializer; -import zone.moddev.mc.poweradvantage.PowerAdvantage; -import zone.moddev.mc.poweradvantage.RecipeMode; +import zone.moddev.mc.basepower.PowerAdvantage; +import zone.moddev.mc.basepower.RecipeMode; /** Registers the data-pack conditions used by PowerAdvantage recipe modes. */ public final class RecipeConditions { @@ -40,8 +40,8 @@ private Condition(ResourceLocation id, String value) { 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.TagCollectionManager.getInstance().getItems().getTagOrEmpty(new ResourceLocation(value)).getValues().isEmpty(); - case "tag_not_empty": return !net.minecraft.tags.TagCollectionManager.getInstance().getItems().getTagOrEmpty(new ResourceLocation(value)).getValues().isEmpty(); + 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(); default: throw new IllegalStateException("Unknown Advantage condition " + id); } } @@ -57,7 +57,7 @@ private Serializer(String name) { } @Override public ResourceLocation getID() { return id; } - @Override public Condition read(JsonObject json) { return new Condition(id, JSONUtils.getAsString(json, key)); } + @Override public Condition read(JsonObject json) { return new Condition(id, GsonHelper.getAsString(json, key)); } @Override public void write(JsonObject json, Condition condition) { json.addProperty(key, condition.value); } } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeDiscovery.java b/src/main/java/zone/moddev/mc/basepower/recipe/RecipeDiscovery.java similarity index 72% rename from src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeDiscovery.java rename to src/main/java/zone/moddev/mc/basepower/recipe/RecipeDiscovery.java index 7215be3d..08d0401f 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeDiscovery.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/RecipeDiscovery.java @@ -1,28 +1,28 @@ -package zone.moddev.mc.poweradvantage.recipe; +package zone.moddev.mc.basepower.recipe; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; import net.minecraft.advancements.Advancement; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.item.crafting.IRecipe; -import net.minecraft.util.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.event.entity.player.PlayerEvent; /** Retains earned discoveries when recipe modes or material providers change. */ public final class RecipeDiscovery { private static final Set NAMESPACES = new HashSet<>(Arrays.asList( - "poweradvantage", "steamadvantage", "electricadvantage")); + "basepower", "basesteam", "baseelectric")); private RecipeDiscovery() {} public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { - if (!(event.getPlayer() instanceof ServerPlayerEntity)) return; - ServerPlayerEntity player = (ServerPlayerEntity) event.getPlayer(); + if (!(event.getPlayer() instanceof ServerPlayer)) return; + ServerPlayer player = (ServerPlayer) event.getPlayer(); // Disabled recipes have no reward to grant. Replay only already-earned // discoveries when that exact recipe becomes available on a later login. - for (IRecipe recipe : player.getServer().getRecipeManager().getRecipes()) { + for (Recipe recipe : player.getServer().getRecipeManager().getRecipes()) { if (!NAMESPACES.contains(recipe.getId().getNamespace()) || recipe.isSpecial() || player .getRecipeBook().contains(recipe)) continue; Advancement advancement = player.getServer().getAdvancements().getAdvancement( diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/FuelRegistry.java b/src/main/java/zone/moddev/mc/basepower/registry/FuelRegistry.java similarity index 93% rename from src/main/java/zone/moddev/mc/poweradvantage/registry/FuelRegistry.java rename to src/main/java/zone/moddev/mc/basepower/registry/FuelRegistry.java index b510345e..6b73b291 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/FuelRegistry.java +++ b/src/main/java/zone/moddev/mc/basepower/registry/FuelRegistry.java @@ -1,9 +1,9 @@ -package zone.moddev.mc.poweradvantage.registry; +package zone.moddev.mc.basepower.registry; -import net.minecraft.block.Block; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.tileentity.AbstractFurnaceTileEntity; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; import net.minecraftforge.event.ForgeEventFactory; import java.util.*; @@ -13,7 +13,7 @@ /** * The fuel registry keeps track of the cooking time for new burnables added by PowerAdvantage. The - * actual registration of these burnables occurs in zone.moddev.mc.poweradvantage.init.Fuels + * actual registration of these burnables occurs in zone.moddev.mc.basepower.init.Fuels * @author DrCyano * */ @@ -142,8 +142,8 @@ public static short getActualBurntimeForItem(ItemStack fuel){ if(fuel == null || fuel.isEmpty()) return 0; int custom = getInstance().getBurnTime(fuel); if (custom > 0) return (short) Math.min(Short.MAX_VALUE, custom); - Integer vanilla = AbstractFurnaceTileEntity.getFuel().get(fuel.getItem()); - int burnTime = ForgeEventFactory.getItemBurnTime(fuel, vanilla == null ? 0 : vanilla.intValue()); + Integer vanilla = AbstractFurnaceBlockEntity.getFuel().get(fuel.getItem()); + int burnTime = ForgeEventFactory.getItemBurnTime(fuel, vanilla == null ? 0 : vanilla.intValue(), net.minecraft.world.item.crafting.RecipeType.SMELTING); return (short) Math.min(Short.MAX_VALUE, Math.max(0, burnTime)); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/ITileEntityGUI.java b/src/main/java/zone/moddev/mc/basepower/registry/ITileEntityGUI.java similarity index 72% rename from src/main/java/zone/moddev/mc/poweradvantage/registry/ITileEntityGUI.java rename to src/main/java/zone/moddev/mc/basepower/registry/ITileEntityGUI.java index b850b37c..ab581433 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/ITileEntityGUI.java +++ b/src/main/java/zone/moddev/mc/basepower/registry/ITileEntityGUI.java @@ -1,7 +1,7 @@ -package zone.moddev.mc.poweradvantage.registry; +package zone.moddev.mc.basepower.registry; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @@ -20,7 +20,7 @@ public interface ITileEntityGUI { * @param player The player that is going to use the GUI * @return An instance of net.minecraft.inventory.container.Container */ - public abstract net.minecraft.inventory.container.Container getContainer(TileEntity e, PlayerEntity player); + public abstract net.minecraft.world.inventory.AbstractContainerMenu getContainer(BlockEntity e, Player player); /** * Gets an instance of net.minecraft.client.gui.screen.inventory.ContainerScreen for @@ -31,5 +31,5 @@ public interface ITileEntityGUI { * @return An instance of net.minecraft.client.gui.screen.inventory.ContainerScreen */ @OnlyIn(Dist.CLIENT) - public abstract net.minecraft.client.gui.screen.inventory.ContainerScreen getContainerGUI(TileEntity e, PlayerEntity player); + public abstract net.minecraft.client.gui.screens.inventory.AbstractContainerScreen getContainerGUI(BlockEntity e, Player player); } \ No newline at end of file diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/MachineGUIRegistry.java b/src/main/java/zone/moddev/mc/basepower/registry/MachineGUIRegistry.java similarity index 54% rename from src/main/java/zone/moddev/mc/poweradvantage/registry/MachineGUIRegistry.java rename to src/main/java/zone/moddev/mc/basepower/registry/MachineGUIRegistry.java index 2ca207ce..7843c737 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/MachineGUIRegistry.java +++ b/src/main/java/zone/moddev/mc/basepower/registry/MachineGUIRegistry.java @@ -1,30 +1,30 @@ -package zone.moddev.mc.poweradvantage.registry; +package zone.moddev.mc.basepower.registry; import cyano.poweradvantage.api.GUIBlock; import cyano.poweradvantage.api.simple.SimpleMachineGUI; -import net.minecraft.block.Block; -import net.minecraft.client.gui.ScreenManager; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.entity.player.PlayerInventory; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.container.Container; -import net.minecraft.inventory.container.ContainerType; -import net.minecraft.inventory.container.INamedContainerProvider; -import net.minecraft.network.PacketBuffer; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.text.ITextComponent; -import net.minecraft.util.text.TranslationTextComponent; -import net.minecraft.world.World; +import net.minecraft.world.level.block.Block; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.MenuProvider; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TranslatableComponent; +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.event.RegistryEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.network.NetworkHooks; -import zone.moddev.mc.poweradvantage.PowerAdvantage; +import net.minecraftforge.fmllegacy.network.NetworkHooks; +import zone.moddev.mc.basepower.PowerAdvantage; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -36,7 +36,7 @@ public final class MachineGUIRegistry { private static final MachineGUIRegistry INSTANCE = new MachineGUIRegistry(); private static final AtomicInteger NEXT_ID = new AtomicInteger(1); private static final Map DEFINITIONS = new ConcurrentHashMap<>(); - public static ContainerType TYPE; + public static MenuType TYPE; private MachineGUIRegistry() {} @@ -51,23 +51,23 @@ public static int addGUI(ITileEntityGUI gui) { public void removeGUI(int id) { DEFINITIONS.remove(id); } @SubscribeEvent - public static void registerTypes(RegistryEvent.Register> event) { + public static void registerTypes(RegistryEvent.Register> event) { TYPE = IForgeContainerType.create(MachineGUIRegistry::createClientContainer); TYPE.setRegistryName(PowerAdvantage.MODID, "machine"); event.getRegistry().register(TYPE); } private static SimpleMachineGUI.Container createClientContainer( - int windowId, PlayerInventory inventory, PacketBuffer data) { + int windowId, Inventory inventory, FriendlyByteBuf data) { BlockPos pos = data.readBlockPos(); - World world = inventory.player.level; - TileEntity tile = world.getBlockEntity(pos); + Level world = inventory.player.level; + BlockEntity tile = world.getBlockEntity(pos); Block block = world.getBlockState(pos).getBlock(); - if (!(block instanceof GUIBlock) || !(tile instanceof IInventory)) { + if (!(block instanceof GUIBlock) || !(tile instanceof Container)) { throw new IllegalStateException("Missing machine for container at " + pos); } // Resolve the local block's definition; numeric registration order is not a network identity. - return definition(((GUIBlock) block).getGuiID()).createMenu(windowId, inventory, (IInventory) tile); + return definition(((GUIBlock) block).getGuiID()).createMenu(windowId, inventory, (Container) tile); } private static SimpleMachineGUI definition(int id) { @@ -80,42 +80,42 @@ private static SimpleMachineGUI definition(int id) { @OnlyIn(Dist.CLIENT) public static void registerScreens() { - ScreenManager.register(TYPE, + MenuScreens.register(TYPE, (container, inventory, title) -> container.createScreen(inventory, title)); } - public static void open(ServerPlayerEntity player, World world, BlockPos pos, int id) { - TileEntity tile = world.getBlockEntity(pos); - if (world.isClientSide || player.level != world || !(tile instanceof IInventory)) return; + public static void open(ServerPlayer player, Level world, BlockPos pos, int id) { + BlockEntity tile = world.getBlockEntity(pos); + if (world.isClientSide || player.level != world || !(tile instanceof Container)) return; SimpleMachineGUI gui = definition(id); - NetworkHooks.openGui(player, new INamedContainerProvider() { - @Override public Container createMenu(int windowId, PlayerInventory inventory, PlayerEntity owner) { - return gui.createMenu(windowId, inventory, (IInventory) tile); + NetworkHooks.openGui(player, new MenuProvider() { + @Override public AbstractContainerMenu createMenu(int windowId, Inventory inventory, Player owner) { + return gui.createMenu(windowId, inventory, (Container) tile); } - @Override public ITextComponent getDisplayName() { + @Override public Component getDisplayName() { return menuTitle(tile, world.getBlockState(pos).getBlock()); } }, pos); } - static ITextComponent menuTitle(TileEntity tile, Block block) { - if (tile instanceof net.minecraft.util.INameable) { - ITextComponent customName = ((net.minecraft.util.INameable) tile).getCustomName(); + static Component menuTitle(BlockEntity tile, Block block) { + if (tile instanceof net.minecraft.world.Nameable) { + Component customName = ((net.minecraft.world.Nameable) tile).getCustomName(); if (customName != null) return customName; } - return new TranslationTextComponent(block.getDescriptionId()); + return new TranslatableComponent(block.getDescriptionId()); } - public Object getServerGuiElement(int id, PlayerEntity player, World world, int x, int y, int z) { + public Object getServerGuiElement(int id, Player player, Level world, int x, int y, int z) { ITileEntityGUI gui = DEFINITIONS.get(id); - TileEntity tile = world.getBlockEntity(new BlockPos(x, y, z)); + BlockEntity tile = world.getBlockEntity(new BlockPos(x, y, z)); return gui == null || tile == null ? null : gui.getContainer(tile, player); } @OnlyIn(Dist.CLIENT) - public Object getClientGuiElement(int id, PlayerEntity player, World world, int x, int y, int z) { + public Object getClientGuiElement(int id, Player player, Level world, int x, int y, int z) { ITileEntityGUI gui = DEFINITIONS.get(id); - TileEntity tile = world.getBlockEntity(new BlockPos(x, y, z)); + BlockEntity tile = world.getBlockEntity(new BlockPos(x, y, z)); return gui == null || tile == null ? null : gui.getContainerGUI(tile, player); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/package-info.java b/src/main/java/zone/moddev/mc/basepower/registry/package-info.java similarity index 67% rename from src/main/java/zone/moddev/mc/poweradvantage/registry/package-info.java rename to src/main/java/zone/moddev/mc/basepower/registry/package-info.java index d71055ea..d22177c1 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/package-info.java +++ b/src/main/java/zone/moddev/mc/basepower/registry/package-info.java @@ -2,4 +2,4 @@ This package contains registry classes for registering content related to PowerAdvantage. */ -package zone.moddev.mc.poweradvantage.registry; \ No newline at end of file +package zone.moddev.mc.basepower.registry; \ No newline at end of file diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipe.java b/src/main/java/zone/moddev/mc/basepower/registry/still/recipe/DistillationRecipe.java similarity index 97% rename from src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipe.java rename to src/main/java/zone/moddev/mc/basepower/registry/still/recipe/DistillationRecipe.java index 5762008b..c0f848cb 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipe.java +++ b/src/main/java/zone/moddev/mc/basepower/registry/still/recipe/DistillationRecipe.java @@ -1,10 +1,10 @@ -package zone.moddev.mc.poweradvantage.registry.still.recipe; +package zone.moddev.mc.basepower.registry.still.recipe; import java.util.Collection; import java.util.LinkedList; -import net.minecraft.fluid.Fluid; -import net.minecraft.util.registry.Registry; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.core.Registry; import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.FluidTank; diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipeRegistry.java b/src/main/java/zone/moddev/mc/basepower/registry/still/recipe/DistillationRecipeRegistry.java similarity index 95% rename from src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipeRegistry.java rename to src/main/java/zone/moddev/mc/basepower/registry/still/recipe/DistillationRecipeRegistry.java index 61b4b6a1..5015b2a6 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipeRegistry.java +++ b/src/main/java/zone/moddev/mc/basepower/registry/still/recipe/DistillationRecipeRegistry.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.registry.still.recipe; +package zone.moddev.mc.basepower.registry.still.recipe; import java.util.ArrayList; import java.util.HashMap; @@ -7,12 +7,12 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import zone.moddev.mc.poweradvantage.PowerAdvantage; -import net.minecraft.fluid.Fluid; +import zone.moddev.mc.basepower.PowerAdvantage; +import net.minecraft.world.level.material.Fluid; import cyano.poweradvantage.api.fluid.FluidStack; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.Registry; -import zone.moddev.mc.poweradvantage.init.Fluids; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.Registry; +import zone.moddev.mc.basepower.init.Fluids; /** * This class handles all of the recipes for machines that "smelt" fluids. diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/ExplicitFluidReference.java b/src/main/java/zone/moddev/mc/basepower/registry/still/recipe/ExplicitFluidReference.java similarity index 90% rename from src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/ExplicitFluidReference.java rename to src/main/java/zone/moddev/mc/basepower/registry/still/recipe/ExplicitFluidReference.java index d3d6e5e2..716b7ffa 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/ExplicitFluidReference.java +++ b/src/main/java/zone/moddev/mc/basepower/registry/still/recipe/ExplicitFluidReference.java @@ -1,8 +1,8 @@ -package zone.moddev.mc.poweradvantage.registry.still.recipe; +package zone.moddev.mc.basepower.registry.still.recipe; -import net.minecraft.fluid.Fluid; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.Registry; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.Registry; /** * Implementation of FluidReference that compares specific fluids (fluids of same name but different diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/FluidReference.java b/src/main/java/zone/moddev/mc/basepower/registry/still/recipe/FluidReference.java similarity index 95% rename from src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/FluidReference.java rename to src/main/java/zone/moddev/mc/basepower/registry/still/recipe/FluidReference.java index ac62fc97..56c2c05b 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/FluidReference.java +++ b/src/main/java/zone/moddev/mc/basepower/registry/still/recipe/FluidReference.java @@ -1,6 +1,6 @@ -package zone.moddev.mc.poweradvantage.registry.still.recipe; +package zone.moddev.mc.basepower.registry.still.recipe; -import net.minecraft.fluid.Fluid; +import net.minecraft.world.level.material.Fluid; import cyano.poweradvantage.api.fluid.FluidStack; /** * This is a simple class used to identify fluids by either registry name or explicit object diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/RegistryNameFluidReference.java b/src/main/java/zone/moddev/mc/basepower/registry/still/recipe/RegistryNameFluidReference.java similarity index 87% rename from src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/RegistryNameFluidReference.java rename to src/main/java/zone/moddev/mc/basepower/registry/still/recipe/RegistryNameFluidReference.java index 447161d3..e539c931 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/RegistryNameFluidReference.java +++ b/src/main/java/zone/moddev/mc/basepower/registry/still/recipe/RegistryNameFluidReference.java @@ -1,9 +1,9 @@ -package zone.moddev.mc.poweradvantage.registry.still.recipe; +package zone.moddev.mc.basepower.registry.still.recipe; -import net.minecraft.fluid.Fluid; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.Registry; -import zone.moddev.mc.poweradvantage.init.Fluids; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.Registry; +import zone.moddev.mc.basepower.init.Fluids; public class RegistryNameFluidReference extends FluidReference { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/AdvantageFluidAdapter.java b/src/main/java/zone/moddev/mc/basepower/util/AdvantageFluidAdapter.java similarity index 91% rename from src/main/java/zone/moddev/mc/poweradvantage/util/AdvantageFluidAdapter.java rename to src/main/java/zone/moddev/mc/basepower/util/AdvantageFluidAdapter.java index cfd65bca..e7faf21d 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/AdvantageFluidAdapter.java +++ b/src/main/java/zone/moddev/mc/basepower/util/AdvantageFluidAdapter.java @@ -1,21 +1,21 @@ -package zone.moddev.mc.poweradvantage.util; +package zone.moddev.mc.basepower.util; import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.FluidTankInfo; import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; -import net.minecraft.fluid.Fluid; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; import net.minecraftforge.fluids.capability.CapabilityFluidHandler; import net.minecraftforge.fluids.capability.IFluidHandler; import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction; /** A side-bound foreign capability; reacquire it after removal or capability invalidation. */ final class AdvantageFluidAdapter implements IAdvantageFluidHandler { - private final TileEntity tile; + private final BlockEntity tile; private final Direction side; - AdvantageFluidAdapter(TileEntity tile, Direction side) { + AdvantageFluidAdapter(BlockEntity tile, Direction side) { this.tile = tile; this.side = side; } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidContainerHelper.java b/src/main/java/zone/moddev/mc/basepower/util/FluidContainerHelper.java similarity index 58% rename from src/main/java/zone/moddev/mc/poweradvantage/util/FluidContainerHelper.java rename to src/main/java/zone/moddev/mc/basepower/util/FluidContainerHelper.java index c6b37316..5c9360b8 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidContainerHelper.java +++ b/src/main/java/zone/moddev/mc/basepower/util/FluidContainerHelper.java @@ -1,28 +1,28 @@ -package zone.moddev.mc.poweradvantage.util; +package zone.moddev.mc.basepower.util; import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.FluidTank; import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.fluid.Fluid; -import net.minecraft.item.Items; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.util.Direction; -import net.minecraft.util.Hand; -import net.minecraft.util.registry.Registry; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.core.Registry; /** Uses Forge's container handling for buckets and capability-based fluid containers. */ public final class FluidContainerHelper { private FluidContainerHelper() {} - public static boolean interact(PlayerEntity player, Hand hand, + public static boolean interact(Player player, InteractionHand hand, IAdvantageFluidHandler handler, Direction face) { if (player.level.isClientSide) return false; return net.minecraftforge.fluids.FluidUtil.interactWithFluidHandler(player, hand, new ForgeFluidAdapter(handler, face, () -> { - if (handler instanceof net.minecraft.tileentity.TileEntity) - ((net.minecraft.tileentity.TileEntity) handler).setChanged(); + if (handler instanceof net.minecraft.world.level.block.entity.BlockEntity) + ((net.minecraft.world.level.block.entity.BlockEntity) handler).setChanged(); })); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidHandlerHelper.java b/src/main/java/zone/moddev/mc/basepower/util/FluidHandlerHelper.java similarity index 68% rename from src/main/java/zone/moddev/mc/poweradvantage/util/FluidHandlerHelper.java rename to src/main/java/zone/moddev/mc/basepower/util/FluidHandlerHelper.java index 27aeb4c9..e904588d 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidHandlerHelper.java +++ b/src/main/java/zone/moddev/mc/basepower/util/FluidHandlerHelper.java @@ -1,21 +1,21 @@ -package zone.moddev.mc.poweradvantage.util; +package zone.moddev.mc.basepower.util; import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; /** Looks up the stable Advantage fluid handler exposed by local machines. */ public final class FluidHandlerHelper { private FluidHandlerHelper() {} - public static IAdvantageFluidHandler getHandler(TileEntity tileEntity, Direction face) { + public static IAdvantageFluidHandler getHandler(BlockEntity tileEntity, Direction face) { if (tileEntity == null || tileEntity.isRemoved()) return null; if (tileEntity instanceof IAdvantageFluidHandler) return (IAdvantageFluidHandler) tileEntity; return tileEntity.getCapability(net.minecraftforge.fluids.capability.CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, face).isPresent() ? new AdvantageFluidAdapter(tileEntity, face) : null; } - public static boolean hasHandler(TileEntity tileEntity, Direction face) { + public static boolean hasHandler(BlockEntity tileEntity, Direction face) { return getHandler(tileEntity, face) != null; } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidIdHelper.java b/src/main/java/zone/moddev/mc/basepower/util/FluidIdHelper.java similarity index 80% rename from src/main/java/zone/moddev/mc/poweradvantage/util/FluidIdHelper.java rename to src/main/java/zone/moddev/mc/basepower/util/FluidIdHelper.java index b0429f59..45f122c2 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidIdHelper.java +++ b/src/main/java/zone/moddev/mc/basepower/util/FluidIdHelper.java @@ -1,8 +1,8 @@ -package zone.moddev.mc.poweradvantage.util; +package zone.moddev.mc.basepower.util; -import net.minecraft.fluid.Fluid; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.Registry; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.Registry; public final class FluidIdHelper { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/ForgeFluidAdapter.java b/src/main/java/zone/moddev/mc/basepower/util/ForgeFluidAdapter.java similarity index 98% rename from src/main/java/zone/moddev/mc/poweradvantage/util/ForgeFluidAdapter.java rename to src/main/java/zone/moddev/mc/basepower/util/ForgeFluidAdapter.java index 91ecbaad..a6418031 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/ForgeFluidAdapter.java +++ b/src/main/java/zone/moddev/mc/basepower/util/ForgeFluidAdapter.java @@ -1,9 +1,9 @@ -package zone.moddev.mc.poweradvantage.util; +package zone.moddev.mc.basepower.util; import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.FluidTankInfo; import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; -import net.minecraft.util.Direction; +import net.minecraft.core.Direction; import net.minecraftforge.fluids.capability.IFluidHandler; /** Exposes the stable Advantage contract through Forge's native fluid capability. */ diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/HashCodeHelper.java b/src/main/java/zone/moddev/mc/basepower/util/HashCodeHelper.java similarity index 96% rename from src/main/java/zone/moddev/mc/poweradvantage/util/HashCodeHelper.java rename to src/main/java/zone/moddev/mc/basepower/util/HashCodeHelper.java index efed6360..c7b3a122 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/HashCodeHelper.java +++ b/src/main/java/zone/moddev/mc/basepower/util/HashCodeHelper.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.util; +package zone.moddev.mc.basepower.util; public abstract class HashCodeHelper { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/InventoryLocks.java b/src/main/java/zone/moddev/mc/basepower/util/InventoryLocks.java similarity index 65% rename from src/main/java/zone/moddev/mc/poweradvantage/util/InventoryLocks.java rename to src/main/java/zone/moddev/mc/basepower/util/InventoryLocks.java index 2e66edf8..081a5be8 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/InventoryLocks.java +++ b/src/main/java/zone/moddev/mc/basepower/util/InventoryLocks.java @@ -1,20 +1,20 @@ -package zone.moddev.mc.poweradvantage.util; +package zone.moddev.mc.basepower.util; import java.lang.reflect.Field; -import net.minecraft.item.ItemStack; -import net.minecraft.tileentity.LockableTileEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; import net.minecraft.world.LockCode; -import net.minecraftforge.fml.common.ObfuscationReflectionHelper; +import net.minecraftforge.fml.util.ObfuscationReflectionHelper; /** Keeps automation from bypassing vanilla container locks. */ public final class InventoryLocks { // Ordinary inventories need no vanilla field access or Forge name remapper. private static final class Fields { - static final Field CODE = ObfuscationReflectionHelper.findField(LockableTileEntity.class, "field_174901_a"); + static final Field CODE = ObfuscationReflectionHelper.findField(BaseContainerBlockEntity.class, "f_58621_"); static final Field FIRST = ObfuscationReflectionHelper.findField( - net.minecraft.inventory.DoubleSidedInventory.class, "field_70477_b"); + net.minecraft.world.CompoundContainer.class, "f_18910_"); static final Field SECOND = ObfuscationReflectionHelper.findField( - net.minecraft.inventory.DoubleSidedInventory.class, "field_70478_c"); + net.minecraft.world.CompoundContainer.class, "f_18911_"); } private InventoryLocks() {} @@ -22,9 +22,9 @@ private InventoryLocks() {} public static boolean isLocked(Object inventory) { if (inventory instanceof InventoryWrapper) return ((InventoryWrapper) inventory).isLocked(); try { - if (inventory instanceof net.minecraft.inventory.DoubleSidedInventory) + if (inventory instanceof net.minecraft.world.CompoundContainer) return isLocked(Fields.FIRST.get(inventory)) || isLocked(Fields.SECOND.get(inventory)); - return inventory instanceof LockableTileEntity + return inventory instanceof BaseContainerBlockEntity && !((LockCode) Fields.CODE.get(inventory)) .unlocksWith(ItemStack.EMPTY); } catch (IllegalAccessException failure) { throw new IllegalStateException("Cannot inspect vanilla inventory lock", failure); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/InventoryWrapper.java b/src/main/java/zone/moddev/mc/basepower/util/InventoryWrapper.java similarity index 57% rename from src/main/java/zone/moddev/mc/poweradvantage/util/InventoryWrapper.java rename to src/main/java/zone/moddev/mc/basepower/util/InventoryWrapper.java index 2957a9ca..d0aef060 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/InventoryWrapper.java +++ b/src/main/java/zone/moddev/mc/basepower/util/InventoryWrapper.java @@ -1,41 +1,41 @@ -package zone.moddev.mc.poweradvantage.util; +package zone.moddev.mc.basepower.util; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.ISidedInventory; -import net.minecraft.inventory.Inventory; -import net.minecraft.item.ItemStack; -import net.minecraft.util.Direction; -import net.minecraft.util.INameable; -import net.minecraft.util.text.ITextComponent; -import net.minecraft.util.text.StringTextComponent; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.Container; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.core.Direction; +import net.minecraft.world.Nameable; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TextComponent; /** Gives ordinary inventories the sided interface used by conveyors. */ -public class InventoryWrapper implements ISidedInventory, INameable { +public class InventoryWrapper implements WorldlyContainer, Nameable { private static final int[] NO_SLOTS = new int[0]; private static final InventoryWrapper NULL_INVENTORY = new InventoryWrapper(); - private final IInventory inventory; + private final Container inventory; private final int[] slots; - protected InventoryWrapper() { this(new Inventory(0)); } + protected InventoryWrapper() { this(new SimpleContainer(0)); } - protected InventoryWrapper(IInventory inventory) { + protected InventoryWrapper(Container inventory) { this.inventory = inventory; slots = new int[inventory.getContainerSize()]; for (int i = 0; i < slots.length; i++) slots[i] = i; } - public static ISidedInventory wrap(IInventory inventory) { + public static WorldlyContainer wrap(Container inventory) { if (inventory == null) return NULL_INVENTORY; - return inventory instanceof ISidedInventory ? (ISidedInventory) inventory : new InventoryWrapper(inventory); + return inventory instanceof WorldlyContainer ? (WorldlyContainer) inventory : new InventoryWrapper(inventory); } public boolean isLocked() { return InventoryLocks.isLocked(inventory); } - @Override public ITextComponent getName() { - return inventory instanceof INameable ? ((INameable) inventory).getName() : new StringTextComponent("Inventory"); + @Override public Component getName() { + return inventory instanceof Nameable ? ((Nameable) inventory).getName() : new TextComponent("Inventory"); } - @Override public ITextComponent getCustomName() { - return inventory instanceof INameable ? ((INameable) inventory).getCustomName() : null; + @Override public Component getCustomName() { + return inventory instanceof Nameable ? ((Nameable) inventory).getCustomName() : null; } @Override public void clearContent() { inventory.clearContent(); } @Override public boolean isEmpty() { return inventory.isEmpty(); } @@ -46,11 +46,11 @@ public static ISidedInventory wrap(IInventory inventory) { @Override public ItemStack removeItemNoUpdate(int slot) { return inventory.removeItemNoUpdate(slot); } @Override public void setItem(int slot, ItemStack stack) { inventory.setItem(slot, stack); } @Override public boolean canPlaceItem(int slot, ItemStack stack) { return inventory.canPlaceItem(slot, stack); } - @Override public boolean stillValid(PlayerEntity player) { + @Override public boolean stillValid(Player player) { return slots.length != 0 && inventory.stillValid(player); } - @Override public void startOpen(PlayerEntity player) { inventory.startOpen(player); } - @Override public void stopOpen(PlayerEntity player) { inventory.stopOpen(player); } + @Override public void startOpen(Player player) { inventory.startOpen(player); } + @Override public void stopOpen(Player player) { inventory.stopOpen(player); } @Override public void setChanged() { inventory.setChanged(); } @Override public int[] getSlotsForFace(Direction face) { return isLocked() ? NO_SLOTS : slots; } @Override public boolean canPlaceItemThroughFace(int slot, ItemStack stack, Direction face) { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/MachineCapabilities.java b/src/main/java/zone/moddev/mc/basepower/util/MachineCapabilities.java similarity index 75% rename from src/main/java/zone/moddev/mc/poweradvantage/util/MachineCapabilities.java rename to src/main/java/zone/moddev/mc/basepower/util/MachineCapabilities.java index 4ae0301b..6551a015 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/MachineCapabilities.java +++ b/src/main/java/zone/moddev/mc/basepower/util/MachineCapabilities.java @@ -1,12 +1,12 @@ -package zone.moddev.mc.poweradvantage.util; +package zone.moddev.mc.basepower.util; import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; import java.util.HashMap; import java.util.Map; -import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.ISidedInventory; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; +import net.minecraft.world.Container; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.fluids.capability.CapabilityFluidHandler; @@ -16,12 +16,12 @@ /** Lazily built sided adapters, discarded when a machine is removed or unloaded. */ public final class MachineCapabilities { - private final TileEntity owner; + private final BlockEntity owner; private final Map> fluids = new HashMap<>(); private final Map> items = new HashMap<>(); private boolean valid = true; - public MachineCapabilities(TileEntity owner) { this.owner = owner; } + public MachineCapabilities(BlockEntity owner) { this.owner = owner; } public LazyOptional get(Capability capability, Direction face) { if (!valid || owner.isRemoved()) return LazyOptional.empty(); @@ -29,9 +29,9 @@ public LazyOptional get(Capability capability, Direction face) { return fluids.computeIfAbsent(face, side -> LazyOptional.of(() -> new ForgeFluidAdapter((IAdvantageFluidHandler) owner, side, owner::setChanged))).cast(); } - if (capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY && owner instanceof IInventory) { - return items.computeIfAbsent(face, side -> LazyOptional.of(() -> side != null && owner instanceof ISidedInventory - ? new SidedInvWrapper((ISidedInventory) owner, side) : new InvWrapper((IInventory) owner))).cast(); + if (capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY && owner instanceof Container) { + return items.computeIfAbsent(face, side -> LazyOptional.of(() -> side != null && owner instanceof WorldlyContainer + ? new SidedInvWrapper((WorldlyContainer) owner, side) : new InvWrapper((Container) owner))).cast(); } return LazyOptional.empty(); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/PowerHelper.java b/src/main/java/zone/moddev/mc/basepower/util/PowerHelper.java similarity index 76% rename from src/main/java/zone/moddev/mc/poweradvantage/util/PowerHelper.java rename to src/main/java/zone/moddev/mc/basepower/util/PowerHelper.java index 0b1b170e..5b6b7db8 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/PowerHelper.java +++ b/src/main/java/zone/moddev/mc/basepower/util/PowerHelper.java @@ -1,11 +1,11 @@ -package zone.moddev.mc.poweradvantage.util; +package zone.moddev.mc.basepower.util; -import zone.moddev.mc.poweradvantage.PowerAdvantage; +import zone.moddev.mc.basepower.PowerAdvantage; import cyano.poweradvantage.api.ITypedConduit; import cyano.poweradvantage.api.PowerConnectorContext; -import net.minecraft.block.ITileEntityProvider; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; import net.minecraftforge.energy.CapabilityEnergy; /** @@ -27,9 +27,9 @@ public static boolean areConnectable(PowerConnectorContext connection) { // both blocks are Power Advantage conductors return ((ITypedConduit) connection.thisBlock.getBlock()).canAcceptConnection(connection) && ((ITypedConduit) connection.otherBlock.getBlock()).canAcceptConnection(connection.reverse()); - } else if (connection.otherBlock.getBlock() instanceof ITileEntityProvider + } else if (connection.otherBlock.getBlock() instanceof EntityBlock && PowerAdvantage.forgeEnergyConversionTable.containsKey(connection.powerType)) { - TileEntity tile = connection.world.getBlockEntity(connection.otherBlockPosition); + BlockEntity tile = connection.world.getBlockEntity(connection.otherBlockPosition); if (tile == null) return false; for (Direction side : Direction.values()) { if (tile.getCapability(CapabilityEnergy.ENERGY, side).isPresent()) return true; diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/ReversibleHashMap.java b/src/main/java/zone/moddev/mc/basepower/util/ReversibleHashMap.java similarity index 97% rename from src/main/java/zone/moddev/mc/poweradvantage/util/ReversibleHashMap.java rename to src/main/java/zone/moddev/mc/basepower/util/ReversibleHashMap.java index f9df4fe0..0ff74bf7 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/ReversibleHashMap.java +++ b/src/main/java/zone/moddev/mc/basepower/util/ReversibleHashMap.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.util; +package zone.moddev.mc.basepower.util; import java.util.Collection; import java.util.Map; diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/ReversibleMap.java b/src/main/java/zone/moddev/mc/basepower/util/ReversibleMap.java similarity index 81% rename from src/main/java/zone/moddev/mc/poweradvantage/util/ReversibleMap.java rename to src/main/java/zone/moddev/mc/basepower/util/ReversibleMap.java index d8b39b04..fce0f4ca 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/ReversibleMap.java +++ b/src/main/java/zone/moddev/mc/basepower/util/ReversibleMap.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.util; +package zone.moddev.mc.basepower.util; public interface ReversibleMap extends java.util.Map { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/package-info.java b/src/main/java/zone/moddev/mc/basepower/util/package-info.java similarity index 55% rename from src/main/java/zone/moddev/mc/poweradvantage/util/package-info.java rename to src/main/java/zone/moddev/mc/basepower/util/package-info.java index 8346da0c..e2fb4a05 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/package-info.java +++ b/src/main/java/zone/moddev/mc/basepower/util/package-info.java @@ -3,4 +3,4 @@ This package contains helper classes.

*/ -package zone.moddev.mc.poweradvantage.util; \ No newline at end of file +package zone.moddev.mc.basepower.util; \ No newline at end of file diff --git a/src/main/java/zone/moddev/mc/poweradvantage/client/ClientOilRenderer.java b/src/main/java/zone/moddev/mc/poweradvantage/client/ClientOilRenderer.java deleted file mode 100644 index 71680232..00000000 --- a/src/main/java/zone/moddev/mc/poweradvantage/client/ClientOilRenderer.java +++ /dev/null @@ -1,44 +0,0 @@ -package zone.moddev.mc.poweradvantage.client; - -import net.minecraft.client.renderer.texture.AtlasTexture; -import net.minecraft.client.renderer.RenderType; -import net.minecraft.client.renderer.RenderTypeLookup; -import net.minecraft.util.ResourceLocation; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; -import net.minecraftforge.client.event.TextureStitchEvent; -import net.minecraftforge.common.MinecraftForge; -import zone.moddev.mc.poweradvantage.PowerAdvantage; - -/** Forge FluidAttributes handle rendering; only the custom atlas sprites need stitching. */ -@OnlyIn(Dist.CLIENT) -public final class ClientOilRenderer { - private ClientOilRenderer() {} - - public static void register() { - MinecraftForge.EVENT_BUS.addListener(ClientOilRenderer::stitch); - } - - public static void configureRenderLayers() { - RenderTypeLookup.setRenderLayer(zone.moddev.mc.poweradvantage.init.Blocks.steel_frame, - RenderType.cutout()); - for (net.minecraft.fluid.Fluid fluid : new net.minecraft.fluid.Fluid[] { - zone.moddev.mc.poweradvantage.init.Fluids.crude_oil, - zone.moddev.mc.poweradvantage.init.Fluids.flowing_crude_oil, - zone.moddev.mc.poweradvantage.init.Fluids.refined_oil, - zone.moddev.mc.poweradvantage.init.Fluids.flowing_refined_oil}) { - RenderTypeLookup.setRenderLayer(fluid, RenderType.translucent()); - } - RenderTypeLookup.setRenderLayer(zone.moddev.mc.poweradvantage.init.Blocks.crude_oil_block, - RenderType.translucent()); - RenderTypeLookup.setRenderLayer(zone.moddev.mc.poweradvantage.init.Blocks.refined_oil_block, - RenderType.translucent()); - } - - private static void stitch(TextureStitchEvent.Pre event) { - if (!AtlasTexture.LOCATION_BLOCKS.equals(event.getMap().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/poweradvantage/machines/conveyors/TileEntityBlockFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityBlockFilter.java deleted file mode 100644 index addda744..00000000 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityBlockFilter.java +++ /dev/null @@ -1,16 +0,0 @@ -package zone.moddev.mc.poweradvantage.machines.conveyors; - -import net.minecraft.item.BlockItem; -import net.minecraft.item.ItemStack; -import zone.moddev.mc.poweradvantage.init.Entities; - -public class TileEntityBlockFilter extends TileEntityConveyorFilter { - public TileEntityBlockFilter() { super(Entities.FILTER_BLOCK, 1); } - - @Override - public boolean matchesFilter(ItemStack item) { - if (item == null) return false; - return item.getItem() instanceof BlockItem; - } - -} diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFoodFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFoodFilter.java deleted file mode 100644 index 727d2b78..00000000 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFoodFilter.java +++ /dev/null @@ -1,15 +0,0 @@ -package zone.moddev.mc.poweradvantage.machines.conveyors; - -import net.minecraft.item.ItemStack; -import zone.moddev.mc.poweradvantage.init.Entities; - -public class TileEntityFoodFilter extends TileEntityConveyorFilter { - public TileEntityFoodFilter() { super(Entities.FILTER_FOOD, 1); } - - @Override - public boolean matchesFilter(ItemStack item) { - if (item == null) return false; - return !item.isEmpty() && item.getItem().isEdible(); - } - -} diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFuelFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFuelFilter.java deleted file mode 100644 index 821382e1..00000000 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFuelFilter.java +++ /dev/null @@ -1,17 +0,0 @@ -package zone.moddev.mc.poweradvantage.machines.conveyors; - - -import net.minecraft.item.ItemStack; -import net.minecraft.tileentity.AbstractFurnaceTileEntity; -import zone.moddev.mc.poweradvantage.init.Entities; - -public class TileEntityFuelFilter extends TileEntityConveyorFilter { - public TileEntityFuelFilter() { super(Entities.FILTER_FUEL, 1); } - - @Override - public boolean matchesFilter(ItemStack item) { - if (item == null) return false; - return AbstractFurnaceTileEntity.isFuel(item); - } - -} diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOverflowFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOverflowFilter.java deleted file mode 100644 index 59d11d0e..00000000 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOverflowFilter.java +++ /dev/null @@ -1,40 +0,0 @@ -package zone.moddev.mc.poweradvantage.machines.conveyors; - -import zone.moddev.mc.poweradvantage.util.InventoryWrapper; -import net.minecraft.inventory.IInventory; -import net.minecraft.item.ItemStack; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; -import zone.moddev.mc.poweradvantage.init.Entities; - -public class TileEntityOverflowFilter extends TileEntityConveyorFilter { - public TileEntityOverflowFilter() { super(Entities.FILTER_OVERFLOW, 1); } - - @Override - public boolean matchesFilter(ItemStack item) { - if (item == null || item.getItem() == null) return false; - boolean canDrop = canInsert(item, Direction.DOWN); - boolean canPass = canInsert(item, this.getFacing()); - if (canPass) { - return canDrop; - } else { - return true; - } - } - - private boolean canInsert(ItemStack item, Direction f) { - TileEntity target = getLevel().getBlockEntity(getBlockPos().relative(f)); - if (target instanceof IInventory) { - return canInsertItemInto(item, InventoryWrapper.wrap((IInventory) target), f.getOpposite()); - } else { - return false; - } - } - - @Override - protected boolean isValidItemFor(ItemStack item, TileEntity target, Direction side) { - // do all of the logic in the matchesFilter method - return true; - } - -} diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityPlantFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityPlantFilter.java deleted file mode 100644 index 74fe9856..00000000 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityPlantFilter.java +++ /dev/null @@ -1,26 +0,0 @@ -package zone.moddev.mc.poweradvantage.machines.conveyors; - -import net.minecraft.block.Block; -import net.minecraft.block.IGrowable; -import net.minecraft.item.BlockItem; -import net.minecraft.item.ItemStack; -import net.minecraftforge.common.IPlantable; -import zone.moddev.mc.poweradvantage.init.Entities; - -public class TileEntityPlantFilter extends TileEntityConveyorFilter { - public TileEntityPlantFilter() { super(Entities.FILTER_PLANT, 1); } - - @Override - public boolean matchesFilter(ItemStack item) { - if (item == null) return false; - if (item.getItem() instanceof IPlantable) return true; - if (item.getItem() instanceof BlockItem) { - Block b = ((BlockItem) item.getItem()).getBlock(); - if (b instanceof IPlantable) return true; - if (b instanceof IGrowable) return true; - - } - return false; - } - -} diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntitySmeltableFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntitySmeltableFilter.java deleted file mode 100644 index 60cb6b68..00000000 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntitySmeltableFilter.java +++ /dev/null @@ -1,20 +0,0 @@ -package zone.moddev.mc.poweradvantage.machines.conveyors; - -import net.minecraft.inventory.Inventory; -import net.minecraft.item.ItemStack; -import net.minecraft.util.text.StringTextComponent; -import net.minecraft.item.crafting.IRecipeType; -import zone.moddev.mc.poweradvantage.init.Entities; - -public class TileEntitySmeltableFilter extends TileEntityConveyorFilter { - public TileEntitySmeltableFilter() { super(Entities.FILTER_SMELT, 1); } - - @Override - public boolean matchesFilter(ItemStack item) { - if (item == null || item.isEmpty() || level == null) return false; - Inventory probe = new Inventory(1); - probe.setItem(0, item.copy()); - return level.getRecipeManager().getRecipeFor(IRecipeType.SMELTING, probe, level).isPresent(); - } - -} diff --git a/src/main/java/zone/moddev/mc/poweradvantage/recipe/MaterialTags.java b/src/main/java/zone/moddev/mc/poweradvantage/recipe/MaterialTags.java deleted file mode 100644 index 09fc4cc6..00000000 --- a/src/main/java/zone/moddev/mc/poweradvantage/recipe/MaterialTags.java +++ /dev/null @@ -1,64 +0,0 @@ -package zone.moddev.mc.poweradvantage.recipe; - -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; -import net.minecraft.item.Item; -import net.minecraft.tags.ITag; -import net.minecraft.tags.ITagCollection; -import net.minecraft.tags.TagCollectionReader; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.Registry; -import net.minecraftforge.event.AddReloadListenerEvent; -import net.minecraftforge.fml.ModList; -import zone.moddev.mc.poweradvantage.PowerAdvantage; -import zone.moddev.mc.poweradvantage.RecipeMode; -import zone.moddev.mc.poweradvantage.init.Items; - -/** Config-dependent aliases resolved by the server's native tag reload. */ -public final class MaterialTags { - private MaterialTags() {} - - public static void installReader(AddReloadListenerEvent event) { - // The event runs during construction, before the first reload starts. - // Clients receive the server's resolved tags and never apply local policy. - event.getDataPackRegistries().tagManager.items = new TagCollectionReader( - Registry.ITEM::getOptional, "tags/items", "item") { - @Override public ITagCollection load(Map builders) { - ITagCollection tags = super.load(builders); - if (ModList.get() == null || Items.bioplastic_ingot == null) return tags; - return applyPolicy(tags, Items.bioplastic_ingot, PowerAdvantage.plasticIsAlsoRubber, - PowerAdvantage.recipeMode == RecipeMode.NORMAL - && ModList.get().isLoaded("electricadvantage")); - } - }; - } - - static ITagCollection applyPolicy(ITagCollection 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 ITagCollection.of(result); - } - - private static void append(Map> tags, String name, - java.util.Collection additions) { - ResourceLocation id = new ResourceLocation(name); - Set values = new LinkedHashSet<>(); - ITag existing = tags.get(id); - if (existing != null) values.addAll(existing.getValues()); - for (Item item : additions) if (item != null) values.add(item); - tags.put(id, ITag.fromSet(values)); - } -} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index 01812572..b133ff28 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -1,3 +1,2 @@ -# Install the config-dependent item tag reader before the native reload begins. -public net.minecraft.resources.DataPackRegistries tagManager -public net.minecraft.tags.NetworkTagManager items +# Install legacy numeric IDs before vanilla flattens pre-1.13 block states. +public-f net.minecraft.util.datafix.fixes.BlockStateData f_14934_ diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index 0438a0c6..d8b89c00 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -1,38 +1,38 @@ modLoader="javafml" loaderVersion="${loader_version_range}" license="LGPL-3.0-only" -issueTrackerURL="https://github.com/MinecraftModDevelopmentMods/PowerAdvantage/issues" +issueTrackerURL="https://github.com/MinecraftModDevelopmentMods/basePower/issues" [[mods]] -modId="poweradvantage" +modId="basepower" version="${version}" -displayName="Power Advantage" -displayURL="https://www.curseforge.com/minecraft/mc-mods/power-advantage" +displayName="basePower" +displayURL="https://www.curseforge.com/minecraft/mc-mods/basepower" authors="DrCyano (aka Cyanobacterium), the MMD Team" description='''API and shared machines for pressure, electricity, quantum power, fluids, and item transport.''' -[[dependencies.poweradvantage]] +[[dependencies.basepower]] modId="basemetals" mandatory=false -versionRange="[3.1.1.116051,4.0.0)" +versionRange="[3.1.1.117011,4.0.0)" ordering="AFTER" side="BOTH" -[[dependencies.poweradvantage]] +[[dependencies.basepower]] modId="orespawn" mandatory=true -versionRange="[4.1.0.116051,5.0.0)" +versionRange="[4.1.0.117011,5.0.0)" ordering="AFTER" side="BOTH" -[[dependencies.poweradvantage]] +[[dependencies.basepower]] modId="forge" mandatory=true versionRange="${forge_version_range}" ordering="NONE" side="BOTH" -[[dependencies.poweradvantage]] +[[dependencies.basepower]] modId="minecraft" mandatory=true versionRange="${minecraft_version_range}" diff --git a/src/main/resources/assets/basepower/blockstates/converter_rf_electricity.json b/src/main/resources/assets/basepower/blockstates/converter_rf_electricity.json new file mode 100644 index 00000000..1ee2e9b8 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/converter_rf_electricity.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "basepower:block/converter_rf_electricity", + "y": 90 + }, + "facing=north": { + "model": "basepower:block/converter_rf_electricity" + }, + "facing=south": { + "model": "basepower:block/converter_rf_electricity", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/converter_rf_electricity", + "y": 270 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/converter_rf_quantum.json b/src/main/resources/assets/basepower/blockstates/converter_rf_quantum.json new file mode 100644 index 00000000..77622a74 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/converter_rf_quantum.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "basepower:block/converter_rf_quantum", + "y": 90 + }, + "facing=north": { + "model": "basepower:block/converter_rf_quantum" + }, + "facing=south": { + "model": "basepower:block/converter_rf_quantum", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/converter_rf_quantum", + "y": 270 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/converter_rf_steam.json b/src/main/resources/assets/basepower/blockstates/converter_rf_steam.json new file mode 100644 index 00000000..d78155d1 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/converter_rf_steam.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "basepower:block/converter_rf_steam", + "y": 90 + }, + "facing=north": { + "model": "basepower:block/converter_rf_steam" + }, + "facing=south": { + "model": "basepower:block/converter_rf_steam", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/converter_rf_steam", + "y": 270 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/converter_tr_electricity.json b/src/main/resources/assets/basepower/blockstates/converter_tr_electricity.json new file mode 100644 index 00000000..05a2a458 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/converter_tr_electricity.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "basepower:block/converter_tr_electricity", + "y": 90 + }, + "facing=north": { + "model": "basepower:block/converter_tr_electricity" + }, + "facing=south": { + "model": "basepower:block/converter_tr_electricity", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/converter_tr_electricity", + "y": 270 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/crude_oil.json b/src/main/resources/assets/basepower/blockstates/crude_oil.json new file mode 100644 index 00000000..5a992550 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/crude_oil.json @@ -0,0 +1,5 @@ +{ + "variants": { + "": { "model": "basepower:block/crude_oil" } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/fluid_discharge.json b/src/main/resources/assets/basepower/blockstates/fluid_discharge.json new file mode 100644 index 00000000..6c37194a --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/fluid_discharge.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "basepower:block/fluid_discharge", + "y": 90 + }, + "facing=north": { + "model": "basepower:block/fluid_discharge" + }, + "facing=south": { + "model": "basepower:block/fluid_discharge", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/fluid_discharge", + "y": 270 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/fluid_drain.json b/src/main/resources/assets/basepower/blockstates/fluid_drain.json new file mode 100644 index 00000000..ddd09e1a --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/fluid_drain.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "basepower:block/fluid_drain", + "y": 90 + }, + "facing=north": { + "model": "basepower:block/fluid_drain" + }, + "facing=south": { + "model": "basepower:block/fluid_drain", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/fluid_drain", + "y": 270 + } + } +} diff --git a/src/main/resources/assets/poweradvantage/blockstates/fluid_metal_tank.json b/src/main/resources/assets/basepower/blockstates/fluid_metal_tank.json similarity index 54% rename from src/main/resources/assets/poweradvantage/blockstates/fluid_metal_tank.json rename to src/main/resources/assets/basepower/blockstates/fluid_metal_tank.json index 5f1ee90e..2458ad8d 100644 --- a/src/main/resources/assets/poweradvantage/blockstates/fluid_metal_tank.json +++ b/src/main/resources/assets/basepower/blockstates/fluid_metal_tank.json @@ -1,21 +1,21 @@ { "variants": { "facing=east": { - "model": "poweradvantage:block/fluid_metal_tank", + "model": "basepower:block/fluid_metal_tank", "y": 90, "uvlock": true }, "facing=north": { - "model": "poweradvantage:block/fluid_metal_tank", + "model": "basepower:block/fluid_metal_tank", "uvlock": true }, "facing=south": { - "model": "poweradvantage:block/fluid_metal_tank", + "model": "basepower:block/fluid_metal_tank", "y": 180, "uvlock": true }, "facing=west": { - "model": "poweradvantage:block/fluid_metal_tank", + "model": "basepower:block/fluid_metal_tank", "y": 270, "uvlock": true } diff --git a/src/main/resources/assets/poweradvantage/blockstates/fluid_pipe.json b/src/main/resources/assets/basepower/blockstates/fluid_pipe.json similarity index 70% rename from src/main/resources/assets/poweradvantage/blockstates/fluid_pipe.json rename to src/main/resources/assets/basepower/blockstates/fluid_pipe.json index 74b70ea6..6299d392 100644 --- a/src/main/resources/assets/poweradvantage/blockstates/fluid_pipe.json +++ b/src/main/resources/assets/basepower/blockstates/fluid_pipe.json @@ -2,385 +2,385 @@ "__comment": "blockstate parameters MUST be in alphabetical order!", "variants": { "down=false,east=false,north=false,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_ensw", + "model": "basepower:block/fluid_pipe_ensw", "x": 0, "y": 0, "uvlock": false }, "down=true,east=false,north=false,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_n", + "model": "basepower:block/fluid_pipe_n", "x": 90, "y": 0, "uvlock": false }, "down=false,east=true,north=false,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_n", + "model": "basepower:block/fluid_pipe_n", "x": 0, "y": 90, "uvlock": false }, "down=false,east=false,north=true,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_n", + "model": "basepower:block/fluid_pipe_n", "x": 0, "y": 0, "uvlock": false }, "down=false,east=false,north=false,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_n", + "model": "basepower:block/fluid_pipe_n", "x": 0, "y": 180, "uvlock": false }, "down=false,east=false,north=false,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_n", + "model": "basepower:block/fluid_pipe_n", "x": 270, "y": 0, "uvlock": false }, "down=false,east=false,north=false,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_n", + "model": "basepower:block/fluid_pipe_n", "x": 0, "y": 270, "uvlock": false }, "down=true,east=true,north=false,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 90, "y": 180, "uvlock": false }, "down=true,east=false,north=true,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 90, "y": 90, "uvlock": false }, "down=true,east=false,north=false,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 90, "y": 270, "uvlock": false }, "down=true,east=false,north=false,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_ns", + "model": "basepower:block/fluid_pipe_ns", "x": 90, "y": 0, "uvlock": false }, "down=true,east=false,north=false,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 90, "y": 0, "uvlock": false }, "down=false,east=true,north=true,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 0, "y": 90, "uvlock": false }, "down=false,east=true,north=false,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 0, "y": 180, "uvlock": false }, "down=false,east=true,north=false,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 270, "y": 180, "uvlock": false }, "down=false,east=true,north=false,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_ns", + "model": "basepower:block/fluid_pipe_ns", "x": 0, "y": 90, "uvlock": false }, "down=false,east=false,north=true,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_ns", + "model": "basepower:block/fluid_pipe_ns", "x": 0, "y": 0, "uvlock": false }, "down=false,east=false,north=true,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 270, "y": 90, "uvlock": false }, "down=false,east=false,north=true,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 0, "y": 0, "uvlock": false }, "down=false,east=false,north=false,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 270, "y": 270, "uvlock": false }, "down=false,east=false,north=false,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 0, "y": 270, "uvlock": false }, "down=false,east=false,north=false,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 270, "y": 0, "uvlock": false }, "down=true,east=true,north=true,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 90, "y": 90, "uvlock": false }, "down=true,east=true,north=false,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 90, "y": 180, "uvlock": false }, "down=true,east=true,north=false,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 0, "y": 90, "uvlock": false }, "down=true,east=true,north=false,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 90, "y": 90, "uvlock": false }, "down=true,east=false,north=true,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 90, "y": 0, "uvlock": false }, "down=true,east=false,north=true,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 0, "y": 0, "uvlock": false }, "down=true,east=false,north=true,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 90, "y": 0, "uvlock": false }, "down=true,east=false,north=false,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 0, "y": 180, "uvlock": false }, "down=true,east=false,north=false,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 90, "y": 270, "uvlock": false }, "down=true,east=false,north=false,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 0, "y": 270, "uvlock": false }, "down=false,east=true,north=true,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nsw", + "model": "basepower:block/fluid_pipe_nsw", "x": 0, "y": 180, "uvlock": false }, "down=false,east=true,north=true,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 0, "y": 90, "uvlock": false }, "down=false,east=true,north=true,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsw", + "model": "basepower:block/fluid_pipe_nsw", "x": 0, "y": 90, "uvlock": false }, "down=false,east=true,north=false,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 0, "y": 180, "uvlock": false }, "down=false,east=true,north=false,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsw", + "model": "basepower:block/fluid_pipe_nsw", "x": 0, "y": 270, "uvlock": false }, "down=false,east=true,north=false,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 270, "y": 90, "uvlock": false }, "down=false,east=false,north=true,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 270, "y": 0, "uvlock": false }, "down=false,east=false,north=true,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsw", + "model": "basepower:block/fluid_pipe_nsw", "x": 0, "y": 0, "uvlock": false }, "down=false,east=false,north=true,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 0, "y": 0, "uvlock": false }, "down=false,east=false,north=false,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 0, "y": 270, "uvlock": false }, "down=true,east=true,north=true,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 180, "y": 180, "uvlock": false }, "down=true,east=true,north=true,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 90, "y": 90, "uvlock": false }, "down=true,east=true,north=true,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 180, "y": 90, "uvlock": false }, "down=true,east=true,north=false,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 90, "y": 180, "uvlock": false }, "down=true,east=true,north=false,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 180, "y": 270, "uvlock": false }, "down=true,east=true,north=false,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_ensw", + "model": "basepower:block/fluid_pipe_ensw", "x": 90, "y": 0, "uvlock": false }, "down=true,east=false,north=true,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_ensw", + "model": "basepower:block/fluid_pipe_ensw", "x": 90, "y": 90, "uvlock": false }, "down=true,east=false,north=true,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 180, "y": 0, "uvlock": false }, "down=true,east=false,north=true,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 90, "y": 0, "uvlock": false }, "down=true,east=false,north=false,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 90, "y": 270, "uvlock": false }, "down=false,east=true,north=true,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 0, "y": 180, "uvlock": false }, "down=false,east=true,north=true,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_ensw", + "model": "basepower:block/fluid_pipe_ensw", "x": 0, "y": 0, "uvlock": false }, "down=false,east=true,north=true,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 0, "y": 90, "uvlock": false }, "down=false,east=true,north=false,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 0, "y": 270, "uvlock": false }, "down=false,east=false,north=true,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 0, "y": 0, "uvlock": false }, "down=true,east=true,north=true,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_desuw", + "model": "basepower:block/fluid_pipe_desuw", "x": 0, "y": 270, "uvlock": false }, "down=true,east=true,north=true,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_desuw", + "model": "basepower:block/fluid_pipe_desuw", "x": 270, "y": 0, "uvlock": false }, "down=true,east=true,north=true,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_desuw", + "model": "basepower:block/fluid_pipe_desuw", "x": 0, "y": 180, "uvlock": false }, "down=true,east=true,north=false,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_desuw", + "model": "basepower:block/fluid_pipe_desuw", "x": 0, "y": 0, "uvlock": false }, "down=true,east=false,north=true,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_desuw", + "model": "basepower:block/fluid_pipe_desuw", "x": 0, "y": 90, "uvlock": false }, "down=false,east=true,north=true,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_desuw", + "model": "basepower:block/fluid_pipe_desuw", "x": 90, "y": 0, "uvlock": false }, "down=true,east=true,north=true,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_densuw", + "model": "basepower:block/fluid_pipe_densuw", "x": 0, "y": 0, "uvlock": false diff --git a/src/main/resources/assets/poweradvantage/blockstates/fluid_pipe_terminal.json b/src/main/resources/assets/basepower/blockstates/fluid_pipe_terminal.json similarity index 70% rename from src/main/resources/assets/poweradvantage/blockstates/fluid_pipe_terminal.json rename to src/main/resources/assets/basepower/blockstates/fluid_pipe_terminal.json index e5b42d6d..470261cb 100644 --- a/src/main/resources/assets/poweradvantage/blockstates/fluid_pipe_terminal.json +++ b/src/main/resources/assets/basepower/blockstates/fluid_pipe_terminal.json @@ -2,385 +2,385 @@ "__comment": "blockstate parameters MUST be in alphabetical order!", "variants": { "down=false,east=false,north=false,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_0", + "model": "basepower:block/fluid_pipe_0", "x": 0, "y": 0, "uvlock": false }, "down=true,east=false,north=false,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_n", + "model": "basepower:block/fluid_pipe_n", "x": 90, "y": 0, "uvlock": false }, "down=false,east=true,north=false,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_n", + "model": "basepower:block/fluid_pipe_n", "x": 0, "y": 90, "uvlock": false }, "down=false,east=false,north=true,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_n", + "model": "basepower:block/fluid_pipe_n", "x": 0, "y": 0, "uvlock": false }, "down=false,east=false,north=false,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_n", + "model": "basepower:block/fluid_pipe_n", "x": 0, "y": 180, "uvlock": false }, "down=false,east=false,north=false,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_n", + "model": "basepower:block/fluid_pipe_n", "x": 270, "y": 0, "uvlock": false }, "down=false,east=false,north=false,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_n", + "model": "basepower:block/fluid_pipe_n", "x": 0, "y": 270, "uvlock": false }, "down=true,east=true,north=false,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 90, "y": 180, "uvlock": false }, "down=true,east=false,north=true,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 90, "y": 90, "uvlock": false }, "down=true,east=false,north=false,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 90, "y": 270, "uvlock": false }, "down=true,east=false,north=false,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_ns", + "model": "basepower:block/fluid_pipe_ns", "x": 90, "y": 0, "uvlock": false }, "down=true,east=false,north=false,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 90, "y": 0, "uvlock": false }, "down=false,east=true,north=true,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 0, "y": 90, "uvlock": false }, "down=false,east=true,north=false,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 0, "y": 180, "uvlock": false }, "down=false,east=true,north=false,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 270, "y": 180, "uvlock": false }, "down=false,east=true,north=false,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_ns", + "model": "basepower:block/fluid_pipe_ns", "x": 0, "y": 90, "uvlock": false }, "down=false,east=false,north=true,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_ns", + "model": "basepower:block/fluid_pipe_ns", "x": 0, "y": 0, "uvlock": false }, "down=false,east=false,north=true,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 270, "y": 90, "uvlock": false }, "down=false,east=false,north=true,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 0, "y": 0, "uvlock": false }, "down=false,east=false,north=false,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 270, "y": 270, "uvlock": false }, "down=false,east=false,north=false,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 0, "y": 270, "uvlock": false }, "down=false,east=false,north=false,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nw", + "model": "basepower:block/fluid_pipe_nw", "x": 270, "y": 0, "uvlock": false }, "down=true,east=true,north=true,south=false,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 90, "y": 90, "uvlock": false }, "down=true,east=true,north=false,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 90, "y": 180, "uvlock": false }, "down=true,east=true,north=false,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 0, "y": 90, "uvlock": false }, "down=true,east=true,north=false,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 90, "y": 90, "uvlock": false }, "down=true,east=false,north=true,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 90, "y": 0, "uvlock": false }, "down=true,east=false,north=true,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 0, "y": 0, "uvlock": false }, "down=true,east=false,north=true,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 90, "y": 0, "uvlock": false }, "down=true,east=false,north=false,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 0, "y": 180, "uvlock": false }, "down=true,east=false,north=false,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 90, "y": 270, "uvlock": false }, "down=true,east=false,north=false,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 0, "y": 270, "uvlock": false }, "down=false,east=true,north=true,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nsw", + "model": "basepower:block/fluid_pipe_nsw", "x": 0, "y": 180, "uvlock": false }, "down=false,east=true,north=true,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 0, "y": 90, "uvlock": false }, "down=false,east=true,north=true,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsw", + "model": "basepower:block/fluid_pipe_nsw", "x": 0, "y": 90, "uvlock": false }, "down=false,east=true,north=false,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 0, "y": 180, "uvlock": false }, "down=false,east=true,north=false,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsw", + "model": "basepower:block/fluid_pipe_nsw", "x": 0, "y": 270, "uvlock": false }, "down=false,east=true,north=false,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 270, "y": 90, "uvlock": false }, "down=false,east=false,north=true,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_dnu", + "model": "basepower:block/fluid_pipe_dnu", "x": 270, "y": 0, "uvlock": false }, "down=false,east=false,north=true,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsw", + "model": "basepower:block/fluid_pipe_nsw", "x": 0, "y": 0, "uvlock": false }, "down=false,east=false,north=true,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 0, "y": 0, "uvlock": false }, "down=false,east=false,north=false,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nuw", + "model": "basepower:block/fluid_pipe_nuw", "x": 0, "y": 270, "uvlock": false }, "down=true,east=true,north=true,south=true,up=false,west=false": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 180, "y": 180, "uvlock": false }, "down=true,east=true,north=true,south=false,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 90, "y": 90, "uvlock": false }, "down=true,east=true,north=true,south=false,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 180, "y": 90, "uvlock": false }, "down=true,east=true,north=false,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 90, "y": 180, "uvlock": false }, "down=true,east=true,north=false,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 180, "y": 270, "uvlock": false }, "down=true,east=true,north=false,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_ensw", + "model": "basepower:block/fluid_pipe_ensw", "x": 90, "y": 0, "uvlock": false }, "down=true,east=false,north=true,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_ensw", + "model": "basepower:block/fluid_pipe_ensw", "x": 90, "y": 90, "uvlock": false }, "down=true,east=false,north=true,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 180, "y": 0, "uvlock": false }, "down=true,east=false,north=true,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 90, "y": 0, "uvlock": false }, "down=true,east=false,north=false,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 90, "y": 270, "uvlock": false }, "down=false,east=true,north=true,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 0, "y": 180, "uvlock": false }, "down=false,east=true,north=true,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_ensw", + "model": "basepower:block/fluid_pipe_ensw", "x": 0, "y": 0, "uvlock": false }, "down=false,east=true,north=true,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 0, "y": 90, "uvlock": false }, "down=false,east=true,north=false,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 0, "y": 270, "uvlock": false }, "down=false,east=false,north=true,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_nsuw", + "model": "basepower:block/fluid_pipe_nsuw", "x": 0, "y": 0, "uvlock": false }, "down=true,east=true,north=true,south=true,up=true,west=false": { - "model": "poweradvantage:block/fluid_pipe_desuw", + "model": "basepower:block/fluid_pipe_desuw", "x": 0, "y": 270, "uvlock": false }, "down=true,east=true,north=true,south=true,up=false,west=true": { - "model": "poweradvantage:block/fluid_pipe_desuw", + "model": "basepower:block/fluid_pipe_desuw", "x": 270, "y": 0, "uvlock": false }, "down=true,east=true,north=true,south=false,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_desuw", + "model": "basepower:block/fluid_pipe_desuw", "x": 0, "y": 180, "uvlock": false }, "down=true,east=true,north=false,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_desuw", + "model": "basepower:block/fluid_pipe_desuw", "x": 0, "y": 0, "uvlock": false }, "down=true,east=false,north=true,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_desuw", + "model": "basepower:block/fluid_pipe_desuw", "x": 0, "y": 90, "uvlock": false }, "down=false,east=true,north=true,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_desuw", + "model": "basepower:block/fluid_pipe_desuw", "x": 90, "y": 0, "uvlock": false }, "down=true,east=true,north=true,south=true,up=true,west=true": { - "model": "poweradvantage:block/fluid_pipe_densuw", + "model": "basepower:block/fluid_pipe_densuw", "x": 0, "y": 0, "uvlock": false diff --git a/src/main/resources/assets/basepower/blockstates/fluid_storage_tank.json b/src/main/resources/assets/basepower/blockstates/fluid_storage_tank.json new file mode 100644 index 00000000..6273f11f --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/fluid_storage_tank.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "basepower:block/fluid_storage_tank", + "y": 90 + }, + "facing=north": { + "model": "basepower:block/fluid_storage_tank" + }, + "facing=south": { + "model": "basepower:block/fluid_storage_tank", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/fluid_storage_tank", + "y": 270 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/fluid_switch.json b/src/main/resources/assets/basepower/blockstates/fluid_switch.json new file mode 100644 index 00000000..53aed9b5 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/fluid_switch.json @@ -0,0 +1,10 @@ +{ + "variants": { + "active=false": { + "model": "basepower:block/fluid_switch_off" + }, + "active=true": { + "model": "basepower:block/fluid_switch_on" + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/infinite_electricity.json b/src/main/resources/assets/basepower/blockstates/infinite_electricity.json new file mode 100644 index 00000000..a3dd78ee --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/infinite_electricity.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "basepower:block/infinite_electricity", + "y": 90 + }, + "facing=north": { + "model": "basepower:block/infinite_electricity" + }, + "facing=south": { + "model": "basepower:block/infinite_electricity", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/infinite_electricity", + "y": 270 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/infinite_quantum.json b/src/main/resources/assets/basepower/blockstates/infinite_quantum.json new file mode 100644 index 00000000..6f7a8622 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/infinite_quantum.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "basepower:block/infinite_quantum", + "y": 90 + }, + "facing=north": { + "model": "basepower:block/infinite_quantum" + }, + "facing=south": { + "model": "basepower:block/infinite_quantum", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/infinite_quantum", + "y": 270 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/infinite_steam.json b/src/main/resources/assets/basepower/blockstates/infinite_steam.json new file mode 100644 index 00000000..1c0c9758 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/infinite_steam.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "basepower:block/infinite_steam", + "y": 90 + }, + "facing=north": { + "model": "basepower:block/infinite_steam" + }, + "facing=south": { + "model": "basepower:block/infinite_steam", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/infinite_steam", + "y": 270 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/item_conveyor.json b/src/main/resources/assets/basepower/blockstates/item_conveyor.json new file mode 100644 index 00000000..81bb97be --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/item_conveyor.json @@ -0,0 +1,27 @@ +{ + "variants": { + "facing=down": { + "model": "basepower:block/item_conveyor", + "x": 90 + }, + "facing=up": { + "model": "basepower:block/item_conveyor", + "x": 270 + }, + "facing=north": { + "model": "basepower:block/item_conveyor" + }, + "facing=south": { + "model": "basepower:block/item_conveyor", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/item_conveyor", + "y": 270 + }, + "facing=east": { + "model": "basepower:block/item_conveyor", + "y": 90 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/item_filter_block.json b/src/main/resources/assets/basepower/blockstates/item_filter_block.json new file mode 100644 index 00000000..e5da2ad6 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/item_filter_block.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "basepower:block/item_filter_block" + }, + "facing=south": { + "model": "basepower:block/item_filter_block", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/item_filter_block", + "y": 270 + }, + "facing=east": { + "model": "basepower:block/item_filter_block", + "y": 90 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/item_filter_food.json b/src/main/resources/assets/basepower/blockstates/item_filter_food.json new file mode 100644 index 00000000..aa06924e --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/item_filter_food.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "basepower:block/item_filter_food" + }, + "facing=south": { + "model": "basepower:block/item_filter_food", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/item_filter_food", + "y": 270 + }, + "facing=east": { + "model": "basepower:block/item_filter_food", + "y": 90 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/item_filter_fuel.json b/src/main/resources/assets/basepower/blockstates/item_filter_fuel.json new file mode 100644 index 00000000..9d4493e6 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/item_filter_fuel.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "basepower:block/item_filter_fuel" + }, + "facing=south": { + "model": "basepower:block/item_filter_fuel", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/item_filter_fuel", + "y": 270 + }, + "facing=east": { + "model": "basepower:block/item_filter_fuel", + "y": 90 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/item_filter_inventory.json b/src/main/resources/assets/basepower/blockstates/item_filter_inventory.json new file mode 100644 index 00000000..9c5c81fe --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/item_filter_inventory.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "basepower:block/item_filter_inventory" + }, + "facing=south": { + "model": "basepower:block/item_filter_inventory", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/item_filter_inventory", + "y": 270 + }, + "facing=east": { + "model": "basepower:block/item_filter_inventory", + "y": 90 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/item_filter_ore.json b/src/main/resources/assets/basepower/blockstates/item_filter_ore.json new file mode 100644 index 00000000..5b639c64 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/item_filter_ore.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "basepower:block/item_filter_ore" + }, + "facing=south": { + "model": "basepower:block/item_filter_ore", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/item_filter_ore", + "y": 270 + }, + "facing=east": { + "model": "basepower:block/item_filter_ore", + "y": 90 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/item_filter_overflow.json b/src/main/resources/assets/basepower/blockstates/item_filter_overflow.json new file mode 100644 index 00000000..454345e7 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/item_filter_overflow.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "basepower:block/item_filter_overflow" + }, + "facing=south": { + "model": "basepower:block/item_filter_overflow", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/item_filter_overflow", + "y": 270 + }, + "facing=east": { + "model": "basepower:block/item_filter_overflow", + "y": 90 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/item_filter_plant.json b/src/main/resources/assets/basepower/blockstates/item_filter_plant.json new file mode 100644 index 00000000..2731b955 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/item_filter_plant.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "basepower:block/item_filter_plant" + }, + "facing=south": { + "model": "basepower:block/item_filter_plant", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/item_filter_plant", + "y": 270 + }, + "facing=east": { + "model": "basepower:block/item_filter_plant", + "y": 90 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/item_filter_smelt.json b/src/main/resources/assets/basepower/blockstates/item_filter_smelt.json new file mode 100644 index 00000000..bbd7b4a7 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/item_filter_smelt.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "basepower:block/item_filter_smelt" + }, + "facing=south": { + "model": "basepower:block/item_filter_smelt", + "y": 180 + }, + "facing=west": { + "model": "basepower:block/item_filter_smelt", + "y": 270 + }, + "facing=east": { + "model": "basepower:block/item_filter_smelt", + "y": 90 + } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/refined_oil.json b/src/main/resources/assets/basepower/blockstates/refined_oil.json new file mode 100644 index 00000000..d29bb008 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/refined_oil.json @@ -0,0 +1,5 @@ +{ + "variants": { + "": { "model": "basepower:block/refined_oil" } + } +} diff --git a/src/main/resources/assets/basepower/blockstates/steel_frame.json b/src/main/resources/assets/basepower/blockstates/steel_frame.json new file mode 100644 index 00000000..96d49f38 --- /dev/null +++ b/src/main/resources/assets/basepower/blockstates/steel_frame.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "basepower:block/steel_frame" + } + } +} diff --git a/src/main/resources/assets/poweradvantage/blockstates/still.json b/src/main/resources/assets/basepower/blockstates/still.json similarity index 53% rename from src/main/resources/assets/poweradvantage/blockstates/still.json rename to src/main/resources/assets/basepower/blockstates/still.json index 6fe4921b..4d20039f 100644 --- a/src/main/resources/assets/poweradvantage/blockstates/still.json +++ b/src/main/resources/assets/basepower/blockstates/still.json @@ -1,33 +1,33 @@ { "variants": { "active=false,facing=east": { - "model": "poweradvantage:block/still_off", + "model": "basepower:block/still_off", "y": 90 }, "active=false,facing=north": { - "model": "poweradvantage:block/still_off" + "model": "basepower:block/still_off" }, "active=false,facing=south": { - "model": "poweradvantage:block/still_off", + "model": "basepower:block/still_off", "y": 180 }, "active=false,facing=west": { - "model": "poweradvantage:block/still_off", + "model": "basepower:block/still_off", "y": 270 }, "active=true,facing=east": { - "model": "poweradvantage:block/still_on", + "model": "basepower:block/still_on", "y": 90 }, "active=true,facing=north": { - "model": "poweradvantage:block/still_on" + "model": "basepower:block/still_on" }, "active=true,facing=south": { - "model": "poweradvantage:block/still_on", + "model": "basepower:block/still_on", "y": 180 }, "active=true,facing=west": { - "model": "poweradvantage:block/still_on", + "model": "basepower:block/still_on", "y": 270 } } diff --git a/src/main/resources/assets/basepower/lang/de_de.json b/src/main/resources/assets/basepower/lang/de_de.json new file mode 100644 index 00000000..8b91480c --- /dev/null +++ b/src/main/resources/assets/basepower/lang/de_de.json @@ -0,0 +1,77 @@ +{ + "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", + "nei.basepower.recipehandler.shapelessfurnace.name": "Große Ofen (formlos)", + "nei.basepower.recipehandler.shapedfurnace.name": "Große Ofen (geformt)", + "tooltip.basepower.empty": "Leer", + "tooltip.basepower.buckets_of": "%x Eimer %y", + "itemGroup.basepower": "Leistungsvorteil", + "tile.basepower.fluid_drain.name": "Ablauf", + "tile.basepower.fluid_discharge.name": "Austragsrost", + "tile.basepower.fluid_storage_tank.name": "Lagertank", + "item.basepower.fluid_storage_tank_item.name": "Speicher", + "tile.basepower.fluid_pipe.name": "Rohr", + "tile.basepower.item_conveyor.name": "Förderer", + "tile.basepower.item_filter_block.name": "Block Auswahl Förder", + "tile.basepower.item_filter_food.name": "Lebensmittel Auswahl Förder", + "tile.basepower.item_filter_fuel.name": "Brennstoff Auswahl Förder", + "tile.basepower.item_filter_inventory.name": "Anpassbare Auswahl Förder", + "tile.basepower.item_filter_ore.name": "Erz Auswahl Förder", + "tile.basepower.item_filter_plant.name": "Pflanze Auswahl Förder", + "tile.basepower.item_filter_smelt.name": "Kochbare Auswahl Förder", + "tile.basepower.item_filter_overflow.name": "Überlauf Auswahl Förder", + "tile.basepower.steel_frame.name": "Stahlrahmen", + "item.basepower.starch.name": "Kartoffelstärke", + "item.basepower.bioplastic_ingot.name": "Biokunststoff-Barren", + "item.basepower.sprocket.name": "Kettenrad", + "tile.basepower.fluid_metal_tank.name": "Metall-Lagertank", + "item.basepower.rotator_tool.name": "Drehwerkzeug", + "tile.basepower.infinite_steam.name": "Unendlichen Dampfkraft", + "tile.basepower.infinite_electricity.name": "Unendliche Elektrische Leistung", + "tile.basepower.infinite_quantum.name": "Unendliche Quanten Leistung", + "tile.basepower.crude_oil.name": "Rohöl", + "fluid.basepower.crude_oil": "Rohöl", + "item.bucket.crude_oil": "Eimer Rohöl", + "tile.basepower.still.name": "Brennerei", + "tile.basepower.refined_oil.name": "Raffiniertes Öl", + "fluid.basepower.refined_oil": "Raffiniertes Öl", + "item.bucket.refined_oil": "Eimer für Raffiniertes Öl", + "tile.basepower.fluid_switch.name": "Ventil", + "tile.basepower.converter_rf_steam.name": "Dampf-RF Leistungstransformator", + "tile.basepower.converter_rf_electricity.name": "Elektrizität-RF Leistungstransformator", + "tile.basepower.converter_rf_quantum.name": "Quant-RF Leistungstransformator", + "tile.basepower.converter_tr_electricity.name": "Elektrizität-EU Leistungstransformator", + "block.basepower.fluid_drain": "Ablauf", + "block.basepower.fluid_discharge": "Austragsrost", + "block.basepower.fluid_storage_tank": "Lagertank", + "item.basepower.fluid_storage_tank_item": "Speicher", + "block.basepower.fluid_pipe": "Rohr", + "block.basepower.item_conveyor": "Förderer", + "block.basepower.item_filter_block": "Block Auswahl Förder", + "block.basepower.item_filter_food": "Lebensmittel Auswahl Förder", + "block.basepower.item_filter_fuel": "Brennstoff Auswahl Förder", + "block.basepower.item_filter_inventory": "Anpassbare Auswahl Förder", + "block.basepower.item_filter_ore": "Erz Auswahl Förder", + "block.basepower.item_filter_plant": "Pflanze Auswahl Förder", + "block.basepower.item_filter_smelt": "Kochbare Auswahl Förder", + "block.basepower.item_filter_overflow": "Überlauf Auswahl Förder", + "block.basepower.steel_frame": "Stahlrahmen", + "item.basepower.starch": "Kartoffelstärke", + "item.basepower.bioplastic_ingot": "Biokunststoff-Barren", + "item.basepower.sprocket": "Kettenrad", + "block.basepower.fluid_metal_tank": "Metall-Lagertank", + "item.basepower.rotator_tool": "Drehwerkzeug", + "block.basepower.infinite_steam": "Unendlichen Dampfkraft", + "block.basepower.infinite_electricity": "Unendliche Elektrische Leistung", + "block.basepower.infinite_quantum": "Unendliche Quanten Leistung", + "block.basepower.crude_oil": "Rohöl", + "block.basepower.still": "Brennerei", + "block.basepower.refined_oil": "Raffiniertes Öl", + "block.basepower.fluid_switch": "Ventil", + "block.basepower.converter_rf_steam": "Dampf-RF Leistungstransformator", + "block.basepower.converter_rf_electricity": "Elektrizität-RF Leistungstransformator", + "block.basepower.converter_rf_quantum": "Quant-RF Leistungstransformator", + "block.basepower.converter_tr_electricity": "Elektrizität-EU Leistungstransformator", + "item.basepower.crude_oil_bucket": "Eimer Rohöl", + "item.basepower.refined_oil_bucket": "Eimer für Raffiniertes Öl", + "block.basepower.fluid_pipe_terminal": "Rohr" +} diff --git a/src/main/resources/assets/basepower/lang/en_au.json b/src/main/resources/assets/basepower/lang/en_au.json new file mode 100644 index 00000000..6db16d71 --- /dev/null +++ b/src/main/resources/assets/basepower/lang/en_au.json @@ -0,0 +1,77 @@ +{ + "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", + "nei.basepower.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)", + "nei.basepower.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)", + "tooltip.basepower.empty": "Empty", + "tooltip.basepower.buckets_of": "%x buckets of %y", + "itemGroup.basepower": "Power Advantage", + "tile.basepower.fluid_drain.name": "Drain", + "tile.basepower.fluid_discharge.name": "Discharge Grate", + "tile.basepower.fluid_storage_tank.name": "Portable Fluid Tank", + "item.basepower.fluid_storage_tank_item.name": "Storage Tank", + "tile.basepower.fluid_pipe.name": "Pipe", + "tile.basepower.item_conveyor.name": "Conveyor", + "tile.basepower.item_filter_block.name": "Block Filter Conveyor", + "tile.basepower.item_filter_food.name": "Food Filter Conveyor", + "tile.basepower.item_filter_fuel.name": "Fuel Filter Conveyor", + "tile.basepower.item_filter_inventory.name": "Filter Conveyor", + "tile.basepower.item_filter_ore.name": "Ore Filter Conveyor", + "tile.basepower.item_filter_plant.name": "Plantable Filter Conveyor", + "tile.basepower.item_filter_smelt.name": "Furnace Filter Conveyor", + "tile.basepower.item_filter_overflow.name": "Overflow Filter Conveyor", + "tile.basepower.steel_frame.name": "Steel Frame", + "item.basepower.starch.name": "Starch", + "item.basepower.bioplastic_ingot.name": "Bioplastic Ingot", + "item.basepower.sprocket.name": "Sprocket", + "tile.basepower.fluid_metal_tank.name": "Metal Fluid Tank", + "item.basepower.rotator_tool.name": "Rotation Wrench", + "tile.basepower.infinite_steam.name": "Unlimited Steam Power", + "tile.basepower.infinite_electricity.name": "Unlimited Electrical Power", + "tile.basepower.infinite_quantum.name": "Unlimited Quantum Power", + "tile.basepower.crude_oil.name": "Crude Oil", + "fluid.basepower.crude_oil": "Crude Oil", + "item.bucket.crude_oil": "Bucket of Crude Oil", + "tile.basepower.still.name": "Distillation Furnace", + "tile.basepower.refined_oil.name": "Refined Oil", + "fluid.basepower.refined_oil": "Refined Oil", + "item.bucket.refined_oil": "Bucket of Refined Oil", + "tile.basepower.fluid_switch.name": "Fluid Switch Valve", + "tile.basepower.converter_rf_steam.name": "Steam-RF Transformer", + "tile.basepower.converter_rf_electricity.name": "Electricity-RF Transformer", + "tile.basepower.converter_rf_quantum.name": "Quantum-RF Transformer", + "tile.basepower.converter_tr_electricity.name": "Electricity-EU Transformer", + "block.basepower.fluid_drain": "Drain", + "block.basepower.fluid_discharge": "Discharge Grate", + "block.basepower.fluid_storage_tank": "Portable Fluid Tank", + "item.basepower.fluid_storage_tank_item": "Storage Tank", + "block.basepower.fluid_pipe": "Pipe", + "block.basepower.item_conveyor": "Conveyor", + "block.basepower.item_filter_block": "Block Filter Conveyor", + "block.basepower.item_filter_food": "Food Filter Conveyor", + "block.basepower.item_filter_fuel": "Fuel Filter Conveyor", + "block.basepower.item_filter_inventory": "Filter Conveyor", + "block.basepower.item_filter_ore": "Ore Filter Conveyor", + "block.basepower.item_filter_plant": "Plantable Filter Conveyor", + "block.basepower.item_filter_smelt": "Furnace Filter Conveyor", + "block.basepower.item_filter_overflow": "Overflow Filter Conveyor", + "block.basepower.steel_frame": "Steel Frame", + "item.basepower.starch": "Starch", + "item.basepower.bioplastic_ingot": "Bioplastic Ingot", + "item.basepower.sprocket": "Sprocket", + "block.basepower.fluid_metal_tank": "Metal Fluid Tank", + "item.basepower.rotator_tool": "Rotation Wrench", + "block.basepower.infinite_steam": "Unlimited Steam Power", + "block.basepower.infinite_electricity": "Unlimited Electrical Power", + "block.basepower.infinite_quantum": "Unlimited Quantum Power", + "block.basepower.crude_oil": "Crude Oil", + "block.basepower.still": "Distillation Furnace", + "block.basepower.refined_oil": "Refined Oil", + "block.basepower.fluid_switch": "Fluid Switch Valve", + "block.basepower.converter_rf_steam": "Steam-RF Transformer", + "block.basepower.converter_rf_electricity": "Electricity-RF Transformer", + "block.basepower.converter_rf_quantum": "Quantum-RF Transformer", + "block.basepower.converter_tr_electricity": "Electricity-EU Transformer", + "item.basepower.crude_oil_bucket": "Bucket of Crude Oil", + "item.basepower.refined_oil_bucket": "Bucket of Refined Oil", + "block.basepower.fluid_pipe_terminal": "Pipe" +} diff --git a/src/main/resources/assets/basepower/lang/en_ca.json b/src/main/resources/assets/basepower/lang/en_ca.json new file mode 100644 index 00000000..6db16d71 --- /dev/null +++ b/src/main/resources/assets/basepower/lang/en_ca.json @@ -0,0 +1,77 @@ +{ + "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", + "nei.basepower.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)", + "nei.basepower.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)", + "tooltip.basepower.empty": "Empty", + "tooltip.basepower.buckets_of": "%x buckets of %y", + "itemGroup.basepower": "Power Advantage", + "tile.basepower.fluid_drain.name": "Drain", + "tile.basepower.fluid_discharge.name": "Discharge Grate", + "tile.basepower.fluid_storage_tank.name": "Portable Fluid Tank", + "item.basepower.fluid_storage_tank_item.name": "Storage Tank", + "tile.basepower.fluid_pipe.name": "Pipe", + "tile.basepower.item_conveyor.name": "Conveyor", + "tile.basepower.item_filter_block.name": "Block Filter Conveyor", + "tile.basepower.item_filter_food.name": "Food Filter Conveyor", + "tile.basepower.item_filter_fuel.name": "Fuel Filter Conveyor", + "tile.basepower.item_filter_inventory.name": "Filter Conveyor", + "tile.basepower.item_filter_ore.name": "Ore Filter Conveyor", + "tile.basepower.item_filter_plant.name": "Plantable Filter Conveyor", + "tile.basepower.item_filter_smelt.name": "Furnace Filter Conveyor", + "tile.basepower.item_filter_overflow.name": "Overflow Filter Conveyor", + "tile.basepower.steel_frame.name": "Steel Frame", + "item.basepower.starch.name": "Starch", + "item.basepower.bioplastic_ingot.name": "Bioplastic Ingot", + "item.basepower.sprocket.name": "Sprocket", + "tile.basepower.fluid_metal_tank.name": "Metal Fluid Tank", + "item.basepower.rotator_tool.name": "Rotation Wrench", + "tile.basepower.infinite_steam.name": "Unlimited Steam Power", + "tile.basepower.infinite_electricity.name": "Unlimited Electrical Power", + "tile.basepower.infinite_quantum.name": "Unlimited Quantum Power", + "tile.basepower.crude_oil.name": "Crude Oil", + "fluid.basepower.crude_oil": "Crude Oil", + "item.bucket.crude_oil": "Bucket of Crude Oil", + "tile.basepower.still.name": "Distillation Furnace", + "tile.basepower.refined_oil.name": "Refined Oil", + "fluid.basepower.refined_oil": "Refined Oil", + "item.bucket.refined_oil": "Bucket of Refined Oil", + "tile.basepower.fluid_switch.name": "Fluid Switch Valve", + "tile.basepower.converter_rf_steam.name": "Steam-RF Transformer", + "tile.basepower.converter_rf_electricity.name": "Electricity-RF Transformer", + "tile.basepower.converter_rf_quantum.name": "Quantum-RF Transformer", + "tile.basepower.converter_tr_electricity.name": "Electricity-EU Transformer", + "block.basepower.fluid_drain": "Drain", + "block.basepower.fluid_discharge": "Discharge Grate", + "block.basepower.fluid_storage_tank": "Portable Fluid Tank", + "item.basepower.fluid_storage_tank_item": "Storage Tank", + "block.basepower.fluid_pipe": "Pipe", + "block.basepower.item_conveyor": "Conveyor", + "block.basepower.item_filter_block": "Block Filter Conveyor", + "block.basepower.item_filter_food": "Food Filter Conveyor", + "block.basepower.item_filter_fuel": "Fuel Filter Conveyor", + "block.basepower.item_filter_inventory": "Filter Conveyor", + "block.basepower.item_filter_ore": "Ore Filter Conveyor", + "block.basepower.item_filter_plant": "Plantable Filter Conveyor", + "block.basepower.item_filter_smelt": "Furnace Filter Conveyor", + "block.basepower.item_filter_overflow": "Overflow Filter Conveyor", + "block.basepower.steel_frame": "Steel Frame", + "item.basepower.starch": "Starch", + "item.basepower.bioplastic_ingot": "Bioplastic Ingot", + "item.basepower.sprocket": "Sprocket", + "block.basepower.fluid_metal_tank": "Metal Fluid Tank", + "item.basepower.rotator_tool": "Rotation Wrench", + "block.basepower.infinite_steam": "Unlimited Steam Power", + "block.basepower.infinite_electricity": "Unlimited Electrical Power", + "block.basepower.infinite_quantum": "Unlimited Quantum Power", + "block.basepower.crude_oil": "Crude Oil", + "block.basepower.still": "Distillation Furnace", + "block.basepower.refined_oil": "Refined Oil", + "block.basepower.fluid_switch": "Fluid Switch Valve", + "block.basepower.converter_rf_steam": "Steam-RF Transformer", + "block.basepower.converter_rf_electricity": "Electricity-RF Transformer", + "block.basepower.converter_rf_quantum": "Quantum-RF Transformer", + "block.basepower.converter_tr_electricity": "Electricity-EU Transformer", + "item.basepower.crude_oil_bucket": "Bucket of Crude Oil", + "item.basepower.refined_oil_bucket": "Bucket of Refined Oil", + "block.basepower.fluid_pipe_terminal": "Pipe" +} diff --git a/src/main/resources/assets/basepower/lang/en_en.json b/src/main/resources/assets/basepower/lang/en_en.json new file mode 100644 index 00000000..6db16d71 --- /dev/null +++ b/src/main/resources/assets/basepower/lang/en_en.json @@ -0,0 +1,77 @@ +{ + "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", + "nei.basepower.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)", + "nei.basepower.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)", + "tooltip.basepower.empty": "Empty", + "tooltip.basepower.buckets_of": "%x buckets of %y", + "itemGroup.basepower": "Power Advantage", + "tile.basepower.fluid_drain.name": "Drain", + "tile.basepower.fluid_discharge.name": "Discharge Grate", + "tile.basepower.fluid_storage_tank.name": "Portable Fluid Tank", + "item.basepower.fluid_storage_tank_item.name": "Storage Tank", + "tile.basepower.fluid_pipe.name": "Pipe", + "tile.basepower.item_conveyor.name": "Conveyor", + "tile.basepower.item_filter_block.name": "Block Filter Conveyor", + "tile.basepower.item_filter_food.name": "Food Filter Conveyor", + "tile.basepower.item_filter_fuel.name": "Fuel Filter Conveyor", + "tile.basepower.item_filter_inventory.name": "Filter Conveyor", + "tile.basepower.item_filter_ore.name": "Ore Filter Conveyor", + "tile.basepower.item_filter_plant.name": "Plantable Filter Conveyor", + "tile.basepower.item_filter_smelt.name": "Furnace Filter Conveyor", + "tile.basepower.item_filter_overflow.name": "Overflow Filter Conveyor", + "tile.basepower.steel_frame.name": "Steel Frame", + "item.basepower.starch.name": "Starch", + "item.basepower.bioplastic_ingot.name": "Bioplastic Ingot", + "item.basepower.sprocket.name": "Sprocket", + "tile.basepower.fluid_metal_tank.name": "Metal Fluid Tank", + "item.basepower.rotator_tool.name": "Rotation Wrench", + "tile.basepower.infinite_steam.name": "Unlimited Steam Power", + "tile.basepower.infinite_electricity.name": "Unlimited Electrical Power", + "tile.basepower.infinite_quantum.name": "Unlimited Quantum Power", + "tile.basepower.crude_oil.name": "Crude Oil", + "fluid.basepower.crude_oil": "Crude Oil", + "item.bucket.crude_oil": "Bucket of Crude Oil", + "tile.basepower.still.name": "Distillation Furnace", + "tile.basepower.refined_oil.name": "Refined Oil", + "fluid.basepower.refined_oil": "Refined Oil", + "item.bucket.refined_oil": "Bucket of Refined Oil", + "tile.basepower.fluid_switch.name": "Fluid Switch Valve", + "tile.basepower.converter_rf_steam.name": "Steam-RF Transformer", + "tile.basepower.converter_rf_electricity.name": "Electricity-RF Transformer", + "tile.basepower.converter_rf_quantum.name": "Quantum-RF Transformer", + "tile.basepower.converter_tr_electricity.name": "Electricity-EU Transformer", + "block.basepower.fluid_drain": "Drain", + "block.basepower.fluid_discharge": "Discharge Grate", + "block.basepower.fluid_storage_tank": "Portable Fluid Tank", + "item.basepower.fluid_storage_tank_item": "Storage Tank", + "block.basepower.fluid_pipe": "Pipe", + "block.basepower.item_conveyor": "Conveyor", + "block.basepower.item_filter_block": "Block Filter Conveyor", + "block.basepower.item_filter_food": "Food Filter Conveyor", + "block.basepower.item_filter_fuel": "Fuel Filter Conveyor", + "block.basepower.item_filter_inventory": "Filter Conveyor", + "block.basepower.item_filter_ore": "Ore Filter Conveyor", + "block.basepower.item_filter_plant": "Plantable Filter Conveyor", + "block.basepower.item_filter_smelt": "Furnace Filter Conveyor", + "block.basepower.item_filter_overflow": "Overflow Filter Conveyor", + "block.basepower.steel_frame": "Steel Frame", + "item.basepower.starch": "Starch", + "item.basepower.bioplastic_ingot": "Bioplastic Ingot", + "item.basepower.sprocket": "Sprocket", + "block.basepower.fluid_metal_tank": "Metal Fluid Tank", + "item.basepower.rotator_tool": "Rotation Wrench", + "block.basepower.infinite_steam": "Unlimited Steam Power", + "block.basepower.infinite_electricity": "Unlimited Electrical Power", + "block.basepower.infinite_quantum": "Unlimited Quantum Power", + "block.basepower.crude_oil": "Crude Oil", + "block.basepower.still": "Distillation Furnace", + "block.basepower.refined_oil": "Refined Oil", + "block.basepower.fluid_switch": "Fluid Switch Valve", + "block.basepower.converter_rf_steam": "Steam-RF Transformer", + "block.basepower.converter_rf_electricity": "Electricity-RF Transformer", + "block.basepower.converter_rf_quantum": "Quantum-RF Transformer", + "block.basepower.converter_tr_electricity": "Electricity-EU Transformer", + "item.basepower.crude_oil_bucket": "Bucket of Crude Oil", + "item.basepower.refined_oil_bucket": "Bucket of Refined Oil", + "block.basepower.fluid_pipe_terminal": "Pipe" +} diff --git a/src/main/resources/assets/basepower/lang/en_gb.json b/src/main/resources/assets/basepower/lang/en_gb.json new file mode 100644 index 00000000..6db16d71 --- /dev/null +++ b/src/main/resources/assets/basepower/lang/en_gb.json @@ -0,0 +1,77 @@ +{ + "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", + "nei.basepower.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)", + "nei.basepower.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)", + "tooltip.basepower.empty": "Empty", + "tooltip.basepower.buckets_of": "%x buckets of %y", + "itemGroup.basepower": "Power Advantage", + "tile.basepower.fluid_drain.name": "Drain", + "tile.basepower.fluid_discharge.name": "Discharge Grate", + "tile.basepower.fluid_storage_tank.name": "Portable Fluid Tank", + "item.basepower.fluid_storage_tank_item.name": "Storage Tank", + "tile.basepower.fluid_pipe.name": "Pipe", + "tile.basepower.item_conveyor.name": "Conveyor", + "tile.basepower.item_filter_block.name": "Block Filter Conveyor", + "tile.basepower.item_filter_food.name": "Food Filter Conveyor", + "tile.basepower.item_filter_fuel.name": "Fuel Filter Conveyor", + "tile.basepower.item_filter_inventory.name": "Filter Conveyor", + "tile.basepower.item_filter_ore.name": "Ore Filter Conveyor", + "tile.basepower.item_filter_plant.name": "Plantable Filter Conveyor", + "tile.basepower.item_filter_smelt.name": "Furnace Filter Conveyor", + "tile.basepower.item_filter_overflow.name": "Overflow Filter Conveyor", + "tile.basepower.steel_frame.name": "Steel Frame", + "item.basepower.starch.name": "Starch", + "item.basepower.bioplastic_ingot.name": "Bioplastic Ingot", + "item.basepower.sprocket.name": "Sprocket", + "tile.basepower.fluid_metal_tank.name": "Metal Fluid Tank", + "item.basepower.rotator_tool.name": "Rotation Wrench", + "tile.basepower.infinite_steam.name": "Unlimited Steam Power", + "tile.basepower.infinite_electricity.name": "Unlimited Electrical Power", + "tile.basepower.infinite_quantum.name": "Unlimited Quantum Power", + "tile.basepower.crude_oil.name": "Crude Oil", + "fluid.basepower.crude_oil": "Crude Oil", + "item.bucket.crude_oil": "Bucket of Crude Oil", + "tile.basepower.still.name": "Distillation Furnace", + "tile.basepower.refined_oil.name": "Refined Oil", + "fluid.basepower.refined_oil": "Refined Oil", + "item.bucket.refined_oil": "Bucket of Refined Oil", + "tile.basepower.fluid_switch.name": "Fluid Switch Valve", + "tile.basepower.converter_rf_steam.name": "Steam-RF Transformer", + "tile.basepower.converter_rf_electricity.name": "Electricity-RF Transformer", + "tile.basepower.converter_rf_quantum.name": "Quantum-RF Transformer", + "tile.basepower.converter_tr_electricity.name": "Electricity-EU Transformer", + "block.basepower.fluid_drain": "Drain", + "block.basepower.fluid_discharge": "Discharge Grate", + "block.basepower.fluid_storage_tank": "Portable Fluid Tank", + "item.basepower.fluid_storage_tank_item": "Storage Tank", + "block.basepower.fluid_pipe": "Pipe", + "block.basepower.item_conveyor": "Conveyor", + "block.basepower.item_filter_block": "Block Filter Conveyor", + "block.basepower.item_filter_food": "Food Filter Conveyor", + "block.basepower.item_filter_fuel": "Fuel Filter Conveyor", + "block.basepower.item_filter_inventory": "Filter Conveyor", + "block.basepower.item_filter_ore": "Ore Filter Conveyor", + "block.basepower.item_filter_plant": "Plantable Filter Conveyor", + "block.basepower.item_filter_smelt": "Furnace Filter Conveyor", + "block.basepower.item_filter_overflow": "Overflow Filter Conveyor", + "block.basepower.steel_frame": "Steel Frame", + "item.basepower.starch": "Starch", + "item.basepower.bioplastic_ingot": "Bioplastic Ingot", + "item.basepower.sprocket": "Sprocket", + "block.basepower.fluid_metal_tank": "Metal Fluid Tank", + "item.basepower.rotator_tool": "Rotation Wrench", + "block.basepower.infinite_steam": "Unlimited Steam Power", + "block.basepower.infinite_electricity": "Unlimited Electrical Power", + "block.basepower.infinite_quantum": "Unlimited Quantum Power", + "block.basepower.crude_oil": "Crude Oil", + "block.basepower.still": "Distillation Furnace", + "block.basepower.refined_oil": "Refined Oil", + "block.basepower.fluid_switch": "Fluid Switch Valve", + "block.basepower.converter_rf_steam": "Steam-RF Transformer", + "block.basepower.converter_rf_electricity": "Electricity-RF Transformer", + "block.basepower.converter_rf_quantum": "Quantum-RF Transformer", + "block.basepower.converter_tr_electricity": "Electricity-EU Transformer", + "item.basepower.crude_oil_bucket": "Bucket of Crude Oil", + "item.basepower.refined_oil_bucket": "Bucket of Refined Oil", + "block.basepower.fluid_pipe_terminal": "Pipe" +} diff --git a/src/main/resources/assets/basepower/lang/en_pt.json b/src/main/resources/assets/basepower/lang/en_pt.json new file mode 100644 index 00000000..313bb19a --- /dev/null +++ b/src/main/resources/assets/basepower/lang/en_pt.json @@ -0,0 +1,77 @@ +{ + "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", + "nei.basepower.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)", + "nei.basepower.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)", + "tooltip.basepower.empty": "Empty", + "tooltip.basepower.buckets_of": "%x buckets of %y", + "itemGroup.basepower": "Power Advantage", + "tile.basepower.fluid_drain.name": "Drain", + "tile.basepower.fluid_discharge.name": "Discharge Grate", + "tile.basepower.fluid_storage_tank.name": "Portable Fluid Tank", + "item.basepower.fluid_storage_tank_item.name": "Storage Tank", + "tile.basepower.fluid_pipe.name": "Pipe", + "tile.basepower.item_conveyor.name": "Conveyor", + "tile.basepower.item_filter_block.name": "Block Filter Conveyor", + "tile.basepower.item_filter_food.name": "Food Filter Conveyor", + "tile.basepower.item_filter_fuel.name": "Fuel Filter Conveyor", + "tile.basepower.item_filter_inventory.name": "Filter Conveyor", + "tile.basepower.item_filter_ore.name": "Ore Filter Conveyor", + "tile.basepower.item_filter_plant.name": "Plantable Filter Conveyor", + "tile.basepower.item_filter_smelt.name": "Furnace Filter Conveyor", + "tile.basepower.item_filter_overflow.name": "Overflow Filter Conveyor", + "tile.basepower.steel_frame.name": "Steel Frame", + "item.basepower.starch.name": "Starch", + "item.basepower.bioplastic_ingot.name": "Bioplastic Ingot", + "item.basepower.sprocket.name": "Sprocket", + "tile.basepower.fluid_metal_tank.name": "Metal Fluid Tank", + "item.basepower.rotator_tool.name": "Rotation Wrench", + "tile.basepower.infinite_steam.name": "Unlimited Steam Power", + "tile.basepower.infinite_electricity.name": "Unlimited Electrical Power", + "tile.basepower.infinite_quantum.name": "Unlimited Quantum Power", + "tile.basepower.crude_oil.name": "Black Gold", + "fluid.basepower.crude_oil": "Black Gold", + "item.bucket.crude_oil": "Bucket o' Black Gold", + "tile.basepower.still.name": "'Stillery", + "tile.basepower.refined_oil.name": "Lamp Oil", + "fluid.basepower.refined_oil": "Lamp Oil", + "item.bucket.refined_oil": "Bucket o' Lamp Oil", + "tile.basepower.fluid_switch.name": "Big Red Valve", + "tile.basepower.converter_rf_steam.name": "Steam-RF Transformer", + "tile.basepower.converter_rf_electricity.name": "Electricity-RF Transformer", + "tile.basepower.converter_rf_quantum.name": "Quantum-RF Transformer", + "tile.basepower.converter_tr_electricity.name": "Electricity-EU Transformer", + "block.basepower.fluid_drain": "Drain", + "block.basepower.fluid_discharge": "Discharge Grate", + "block.basepower.fluid_storage_tank": "Portable Fluid Tank", + "item.basepower.fluid_storage_tank_item": "Storage Tank", + "block.basepower.fluid_pipe": "Pipe", + "block.basepower.item_conveyor": "Conveyor", + "block.basepower.item_filter_block": "Block Filter Conveyor", + "block.basepower.item_filter_food": "Food Filter Conveyor", + "block.basepower.item_filter_fuel": "Fuel Filter Conveyor", + "block.basepower.item_filter_inventory": "Filter Conveyor", + "block.basepower.item_filter_ore": "Ore Filter Conveyor", + "block.basepower.item_filter_plant": "Plantable Filter Conveyor", + "block.basepower.item_filter_smelt": "Furnace Filter Conveyor", + "block.basepower.item_filter_overflow": "Overflow Filter Conveyor", + "block.basepower.steel_frame": "Steel Frame", + "item.basepower.starch": "Starch", + "item.basepower.bioplastic_ingot": "Bioplastic Ingot", + "item.basepower.sprocket": "Sprocket", + "block.basepower.fluid_metal_tank": "Metal Fluid Tank", + "item.basepower.rotator_tool": "Rotation Wrench", + "block.basepower.infinite_steam": "Unlimited Steam Power", + "block.basepower.infinite_electricity": "Unlimited Electrical Power", + "block.basepower.infinite_quantum": "Unlimited Quantum Power", + "block.basepower.crude_oil": "Black Gold", + "block.basepower.still": "'Stillery", + "block.basepower.refined_oil": "Lamp Oil", + "block.basepower.fluid_switch": "Big Red Valve", + "block.basepower.converter_rf_steam": "Steam-RF Transformer", + "block.basepower.converter_rf_electricity": "Electricity-RF Transformer", + "block.basepower.converter_rf_quantum": "Quantum-RF Transformer", + "block.basepower.converter_tr_electricity": "Electricity-EU Transformer", + "item.basepower.crude_oil_bucket": "Bucket o' Black Gold", + "item.basepower.refined_oil_bucket": "Bucket o' Lamp Oil", + "block.basepower.fluid_pipe_terminal": "Pipe" +} diff --git a/src/main/resources/assets/basepower/lang/en_us.json b/src/main/resources/assets/basepower/lang/en_us.json new file mode 100644 index 00000000..497c2e5f --- /dev/null +++ b/src/main/resources/assets/basepower/lang/en_us.json @@ -0,0 +1,75 @@ +{ + "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", + "nei.basepower.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)", + "nei.basepower.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)", + "tooltip.basepower.empty": "Empty", + "tooltip.basepower.buckets_of": "%x buckets of %y", + "itemGroup.basepower": "basePower", + "tile.basepower.fluid_drain.name": "Drain", + "tile.basepower.fluid_discharge.name": "Discharge Grate", + "tile.basepower.fluid_storage_tank.name": "Portable Fluid Tank", + "tile.basepower.fluid_pipe.name": "Pipe", + "tile.basepower.item_conveyor.name": "Conveyor", + "tile.basepower.item_filter_block.name": "Block Filter Conveyor", + "tile.basepower.item_filter_food.name": "Food Filter Conveyor", + "tile.basepower.item_filter_fuel.name": "Fuel Filter Conveyor", + "tile.basepower.item_filter_inventory.name": "Filter Conveyor", + "tile.basepower.item_filter_ore.name": "Ore Filter Conveyor", + "tile.basepower.item_filter_plant.name": "Plantable Filter Conveyor", + "tile.basepower.item_filter_smelt.name": "Furnace Filter Conveyor", + "tile.basepower.item_filter_overflow.name": "Overflow Filter Conveyor", + "tile.basepower.steel_frame.name": "Steel Frame", + "item.basepower.starch.name": "Starch", + "item.basepower.bioplastic_ingot.name": "Bioplastic Ingot", + "item.basepower.sprocket.name": "Sprocket", + "tile.basepower.fluid_metal_tank.name": "Metal Fluid Tank", + "item.basepower.rotator_tool.name": "Rotation Wrench", + "tile.basepower.infinite_steam.name": "Unlimited Steam Power", + "tile.basepower.infinite_electricity.name": "Unlimited Electrical Power", + "tile.basepower.infinite_quantum.name": "Unlimited Quantum Power", + "tile.basepower.crude_oil.name": "Crude Oil", + "fluid.basepower.crude_oil": "Crude Oil", + "item.bucket.crude_oil": "Bucket of Crude Oil", + "tile.basepower.still.name": "Distillation Furnace", + "tile.basepower.refined_oil.name": "Refined Oil", + "fluid.basepower.refined_oil": "Refined Oil", + "item.bucket.refined_oil": "Bucket of Refined Oil", + "tile.basepower.fluid_switch.name": "Fluid Switch Valve", + "tile.basepower.converter_rf_steam.name": "Steam-RF Transformer", + "tile.basepower.converter_rf_electricity.name": "Electricity-RF Transformer", + "tile.basepower.converter_rf_quantum.name": "Quantum-RF Transformer", + "tile.basepower.converter_tr_electricity.name": "Electricity-EU Transformer", + "block.basepower.fluid_drain": "Drain", + "block.basepower.fluid_discharge": "Discharge Grate", + "block.basepower.fluid_storage_tank": "Portable Fluid Tank", + "block.basepower.fluid_pipe": "Pipe", + "block.basepower.item_conveyor": "Conveyor", + "block.basepower.item_filter_block": "Block Filter Conveyor", + "block.basepower.item_filter_food": "Food Filter Conveyor", + "block.basepower.item_filter_fuel": "Fuel Filter Conveyor", + "block.basepower.item_filter_inventory": "Filter Conveyor", + "block.basepower.item_filter_ore": "Ore Filter Conveyor", + "block.basepower.item_filter_plant": "Plantable Filter Conveyor", + "block.basepower.item_filter_smelt": "Furnace Filter Conveyor", + "block.basepower.item_filter_overflow": "Overflow Filter Conveyor", + "block.basepower.steel_frame": "Steel Frame", + "item.basepower.starch": "Starch", + "item.basepower.bioplastic_ingot": "Bioplastic Ingot", + "item.basepower.sprocket": "Sprocket", + "block.basepower.fluid_metal_tank": "Metal Fluid Tank", + "item.basepower.rotator_tool": "Rotation Wrench", + "block.basepower.infinite_steam": "Unlimited Steam Power", + "block.basepower.infinite_electricity": "Unlimited Electrical Power", + "block.basepower.infinite_quantum": "Unlimited Quantum Power", + "block.basepower.crude_oil": "Crude Oil", + "block.basepower.still": "Distillation Furnace", + "block.basepower.refined_oil": "Refined Oil", + "block.basepower.fluid_switch": "Fluid Switch Valve", + "block.basepower.converter_rf_steam": "Steam-RF Transformer", + "block.basepower.converter_rf_electricity": "Electricity-RF Transformer", + "block.basepower.converter_rf_quantum": "Quantum-RF Transformer", + "block.basepower.converter_tr_electricity": "Electricity-EU Transformer", + "item.basepower.crude_oil_bucket": "Bucket of Crude Oil", + "item.basepower.refined_oil_bucket": "Bucket of Refined Oil", + "block.basepower.fluid_pipe_terminal": "Pipe" +} diff --git a/src/main/resources/assets/basepower/lang/es_es.json b/src/main/resources/assets/basepower/lang/es_es.json new file mode 100644 index 00000000..38214908 --- /dev/null +++ b/src/main/resources/assets/basepower/lang/es_es.json @@ -0,0 +1,75 @@ +{ + "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", + "nei.basepower.recipehandler.shapelessfurnace.name": "Gran Horno (no forma)", + "nei.basepower.recipehandler.shapedfurnace.name": "Gran Horno (en forma)", + "tooltip.basepower.empty": "Vacío", + "tooltip.basepower.buckets_of ": "%x cubos de %y", + "itemGroup.basepower": "Ventaja de Energía", + "tile.basepower.fluid_drain.name": "Drenaje", + "tile.basepower.fluid_discharge.name": "Descarga Grate", + "tile.basepower.fluid_storage_tank.name": "Tanque de almacenamiento", + "item.basepower.fluid_storage_tank_item.name": "Tanque de almacenamiento", + "tile.basepower.fluid_pipe.name": "Tubo", + "tile.basepower.item_conveyor.name": "Transportadores", + "tile.basepower.item_filter_block.name": "Bloquear Filtro Transportadores", + "tile.basepower.item_filter_food.name": "Filtro Alimentos Transportadores", + "tile.basepower.item_filter_fuel.name": "Filtro de combustible Transportadores", + "tile.basepower.item_filter_inventory.name": "Filtro Transportadores", + "tile.basepower.item_filter_ore.name": "Ore Transportadores Filtro", + "tile.basepower.item_filter_plant.name": "Plantable Transportadores Filtro", + "tile.basepower.item_filter_smelt.name": "Horno Filtro Transportadores", + "tile.basepower.item_filter_overflow.name": "Desbordamiento Filtro Transportadores", + "tile.basepower.steel_frame.name": "Marco de Acero", + "item.basepower.starch.name": "Almidón", + "item.basepower.bioplastic_ingot.name": "Bioplástico Lingote", + "item.basepower.sprocket.name": "Piñón", + "tile.basepower.fluid_metal_tank.name": "Tanque de Almacenamiento de Acero", + "item.basepower.rotator_tool.name": "Herramienta de Rotación", + "tile.basepower.infinite_steam.name": "La Energía de Vapor Infinito", + "tile.basepower.infinite_electricity.name": "Energía Eléctrica Infinito", + "tile.basepower.infinite_quantum.name": "Cuántica Infinito", + "tile.basepower.crude_oil.name": "Petróleo Crudo", + "fluid.basepower.crude_oil": "Petróleo Crudo", + "item.bucket.crude_oil.name": "Cubo de Crudo", + "tile.basepower.still.name": "Destilería", + "tile.basepower.refined_oil.name": "Refinado de Petróleo", + "fluid.basepower.refined_oil": "Refinado de Petróleo", + "item.bucket.refined_oil.name": "Cubo de Refinado de Petróleo", + "tile.basepower.fluid_switch.name": "Válvula", + "tile.basepower.converter_rf_steam.name": "Vapor-RF Transformador", + "tile.basepower.converter_rf_electricity.name": "Electricidad-RF Transformador", + "tile.basepower.converter_rf_quantum.name": "Cuántico-RF Transformador", + "tile.basepower.converter_tr_electricity.name": "Electricidad-EU Transformador", + "block.basepower.fluid_drain": "Drenaje", + "block.basepower.fluid_discharge": "Descarga Grate", + "block.basepower.fluid_storage_tank": "Tanque de almacenamiento", + "item.basepower.fluid_storage_tank_item": "Tanque de almacenamiento", + "block.basepower.fluid_pipe": "Tubo", + "block.basepower.item_conveyor": "Transportadores", + "block.basepower.item_filter_block": "Bloquear Filtro Transportadores", + "block.basepower.item_filter_food": "Filtro Alimentos Transportadores", + "block.basepower.item_filter_fuel": "Filtro de combustible Transportadores", + "block.basepower.item_filter_inventory": "Filtro Transportadores", + "block.basepower.item_filter_ore": "Ore Transportadores Filtro", + "block.basepower.item_filter_plant": "Plantable Transportadores Filtro", + "block.basepower.item_filter_smelt": "Horno Filtro Transportadores", + "block.basepower.item_filter_overflow": "Desbordamiento Filtro Transportadores", + "block.basepower.steel_frame": "Marco de Acero", + "item.basepower.starch": "Almidón", + "item.basepower.bioplastic_ingot": "Bioplástico Lingote", + "item.basepower.sprocket": "Piñón", + "block.basepower.fluid_metal_tank": "Tanque de Almacenamiento de Acero", + "item.basepower.rotator_tool": "Herramienta de Rotación", + "block.basepower.infinite_steam": "La Energía de Vapor Infinito", + "block.basepower.infinite_electricity": "Energía Eléctrica Infinito", + "block.basepower.infinite_quantum": "Cuántica Infinito", + "block.basepower.crude_oil": "Petróleo Crudo", + "block.basepower.still": "Destilería", + "block.basepower.refined_oil": "Refinado de Petróleo", + "block.basepower.fluid_switch": "Válvula", + "block.basepower.converter_rf_steam": "Vapor-RF Transformador", + "block.basepower.converter_rf_electricity": "Electricidad-RF Transformador", + "block.basepower.converter_rf_quantum": "Cuántico-RF Transformador", + "block.basepower.converter_tr_electricity": "Electricidad-EU Transformador", + "block.basepower.fluid_pipe_terminal": "Tubo" +} diff --git a/src/main/resources/assets/basepower/lang/es_mx.json b/src/main/resources/assets/basepower/lang/es_mx.json new file mode 100644 index 00000000..bc2cf1af --- /dev/null +++ b/src/main/resources/assets/basepower/lang/es_mx.json @@ -0,0 +1,75 @@ +{ + "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", + "nei.basepower.recipehandler.shapelessfurnace.name": "Gran Horno (no forma)", + "nei.basepower.recipehandler.shapedfurnace.name": "Gran Horno (en forma)", + "tooltip.basepower.empty": "Vacío", + "tooltip.basepower.buckets_of": "%x cubos de %y", + "itemGroup.basepower": "Ventaja de Energía", + "tile.basepower.fluid_drain.name": "Drenaje", + "tile.basepower.fluid_discharge.name": "Descarga Grate", + "tile.basepower.fluid_storage_tank.name": "Tanque de almacenamiento", + "item.basepower.fluid_storage_tank_item.name": "Tanque de almacenamiento", + "tile.basepower.fluid_pipe.name": "Tubo", + "tile.basepower.item_conveyor.name": "Transportadores", + "tile.basepower.item_filter_block.name": "Bloquear Filtro Transportadores", + "tile.basepower.item_filter_food.name": "Filtro Alimentos Transportadores", + "tile.basepower.item_filter_fuel.name": "Filtro de combustible Transportadores", + "tile.basepower.item_filter_inventory.name": "Filtro Transportadores", + "tile.basepower.item_filter_ore.name": "Ore Transportadores Filtro", + "tile.basepower.item_filter_plant.name": "Plantable Transportadores Filtro", + "tile.basepower.item_filter_smelt.name": "Horno Filtro Transportadores", + "tile.basepower.item_filter_overflow.name": "Desbordamiento Filtro Transportadores", + "tile.basepower.steel_frame.name": "Marco de Acero", + "item.basepower.starch.name": "Almidón", + "item.basepower.bioplastic_ingot.name": "Bioplástico Lingote", + "item.basepower.sprocket.name": "Piñón", + "tile.basepower.fluid_metal_tank.name": "Tanque de Almacenamiento de Acero", + "item.basepower.rotator_tool.name": "Herramienta de Rotación", + "tile.basepower.infinite_steam.name": "La Energía de Vapor Infinito", + "tile.basepower.infinite_electricity.name": "Energía Eléctrica Infinito", + "tile.basepower.infinite_quantum.name": "Cuántica Infinito", + "tile.basepower.crude_oil.name": "Petróleo Crudo", + "fluid.basepower.crude_oil": "Petróleo Crudo", + "item.bucket.crude_oil.name": "Cubo de Crudo", + "tile.basepower.still.name": "Destilería", + "tile.basepower.refined_oil.name": "Refinado de Petróleo", + "fluid.basepower.refined_oil": "Refinado de Petróleo", + "item.bucket.refined_oil.name": "Cubo de Refinado de Petróleo", + "tile.basepower.fluid_switch.name": "Válvula", + "tile.basepower.converter_rf_steam.name": "Vapor-RF Transformador", + "tile.basepower.converter_rf_electricity.name": "Electricidad-RF Transformador", + "tile.basepower.converter_rf_quantum.name": "Cuántico-RF Transformador", + "tile.basepower.converter_tr_electricity.name": "Electricidad-EU Transformador", + "block.basepower.fluid_drain": "Drenaje", + "block.basepower.fluid_discharge": "Descarga Grate", + "block.basepower.fluid_storage_tank": "Tanque de almacenamiento", + "item.basepower.fluid_storage_tank_item": "Tanque de almacenamiento", + "block.basepower.fluid_pipe": "Tubo", + "block.basepower.item_conveyor": "Transportadores", + "block.basepower.item_filter_block": "Bloquear Filtro Transportadores", + "block.basepower.item_filter_food": "Filtro Alimentos Transportadores", + "block.basepower.item_filter_fuel": "Filtro de combustible Transportadores", + "block.basepower.item_filter_inventory": "Filtro Transportadores", + "block.basepower.item_filter_ore": "Ore Transportadores Filtro", + "block.basepower.item_filter_plant": "Plantable Transportadores Filtro", + "block.basepower.item_filter_smelt": "Horno Filtro Transportadores", + "block.basepower.item_filter_overflow": "Desbordamiento Filtro Transportadores", + "block.basepower.steel_frame": "Marco de Acero", + "item.basepower.starch": "Almidón", + "item.basepower.bioplastic_ingot": "Bioplástico Lingote", + "item.basepower.sprocket": "Piñón", + "block.basepower.fluid_metal_tank": "Tanque de Almacenamiento de Acero", + "item.basepower.rotator_tool": "Herramienta de Rotación", + "block.basepower.infinite_steam": "La Energía de Vapor Infinito", + "block.basepower.infinite_electricity": "Energía Eléctrica Infinito", + "block.basepower.infinite_quantum": "Cuántica Infinito", + "block.basepower.crude_oil": "Petróleo Crudo", + "block.basepower.still": "Destilería", + "block.basepower.refined_oil": "Refinado de Petróleo", + "block.basepower.fluid_switch": "Válvula", + "block.basepower.converter_rf_steam": "Vapor-RF Transformador", + "block.basepower.converter_rf_electricity": "Electricidad-RF Transformador", + "block.basepower.converter_rf_quantum": "Cuántico-RF Transformador", + "block.basepower.converter_tr_electricity": "Electricidad-EU Transformador", + "block.basepower.fluid_pipe_terminal": "Tubo" +} diff --git a/src/main/resources/assets/basepower/lang/it_it.json b/src/main/resources/assets/basepower/lang/it_it.json new file mode 100644 index 00000000..d96d2666 --- /dev/null +++ b/src/main/resources/assets/basepower/lang/it_it.json @@ -0,0 +1,75 @@ +{ + "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", + "nei.basepower.recipehandler.shapelessfurnace.name": "Grande Fornace (informe)", + "nei.basepower.recipehandler.shapedfurnace.name": "Grande Fornace (forma)", + "tooltip.basepower.empty": "vuoto", + "tooltip.basepower.buckets_of": "%x secchi d'%y", + "itemGroup.basepower": "Potenza Advantage", + "tile.basepower.fluid_drain.name": "Scarico", + "tile.basepower.fluid_discharge.name": "Griglia di scarico", + "tile.basepower.fluid_storage_tank.name": "Serbatoio", + "item.basepower.fluid_storage_tank_item.name": "Serbatoio", + "tile.basepower.fluid_pipe.name": "Tubo", + "tile.basepower.item_conveyor.name": "Trasportatore", + "tile.basepower.item_filter_block.name": "Filtro di blocco trasportatore", + "tile.basepower.item_filter_food.name": "Filtro alimentare Conveyor", + "tile.basepower.item_filter_fuel.name": "carburante Filtro Conveyor", + "tile.basepower.item_filter_inventory.name": "Filtro Conveyor", + "tile.basepower.item_filter_ore.name": "Ore Filtro Conveyor", + "tile.basepower.item_filter_plant.name": "Plantable Filtro Conveyor", + "tile.basepower.item_filter_smelt.name": "Forno Filtro Conveyor", + "tile.basepower.item_filter_overflow.name": "Straripamento Filtro Conveyor", + "tile.basepower.steel_frame.name": "Steel Frame", + "item.basepower.starch.name": "Amido", + "item.basepower.bioplastic_ingot.name": "Bioplastica Lingotto", + "item.basepower.sprocket.name": "Sprocket", + "tile.basepower.fluid_metal_tank.name": "Serbatoio in Metallo", + "item.basepower.rotator_tool.name": "Strumento di Rotazione", + "tile.basepower.infinite_steam.name": "Forza Vapore Infinite", + "tile.basepower.infinite_electricity.name": "Elettricità Infinite", + "tile.basepower.infinite_quantum.name": "Atomica Infinite", + "tile.basepower.crude_oil.name": "Olio a Crudo", + "fluid.basepower.crude_oil": "Olio a Crudo", + "item.bucket.crude_oil.name": "Secchio di Greggio", + "tile.basepower.still.name": "Distilleria", + "tile.basepower.refined_oil.name": "Olio Raffinato", + "fluid.basepower.refined_oil": "Olio Raffinato", + "item.bucket.refined_oil.name": "Secchio di Olio Raffinato", + "tile.basepower.fluid_switch.name": "Valvola", + "tile.basepower.converter_rf_steam.name": "Vapore-RF Trasformatore", + "tile.basepower.converter_rf_electricity.name": "Elettricità-RF Trasformatore", + "tile.basepower.converter_rf_quantum.name": "Quantistico-RF Trasformatore", + "tile.basepower.converter_tr_electricity.name": "Elettricità-EU Trasformatore", + "block.basepower.fluid_drain": "Scarico", + "block.basepower.fluid_discharge": "Griglia di scarico", + "block.basepower.fluid_storage_tank": "Serbatoio", + "item.basepower.fluid_storage_tank_item": "Serbatoio", + "block.basepower.fluid_pipe": "Tubo", + "block.basepower.item_conveyor": "Trasportatore", + "block.basepower.item_filter_block": "Filtro di blocco trasportatore", + "block.basepower.item_filter_food": "Filtro alimentare Conveyor", + "block.basepower.item_filter_fuel": "carburante Filtro Conveyor", + "block.basepower.item_filter_inventory": "Filtro Conveyor", + "block.basepower.item_filter_ore": "Ore Filtro Conveyor", + "block.basepower.item_filter_plant": "Plantable Filtro Conveyor", + "block.basepower.item_filter_smelt": "Forno Filtro Conveyor", + "block.basepower.item_filter_overflow": "Straripamento Filtro Conveyor", + "block.basepower.steel_frame": "Steel Frame", + "item.basepower.starch": "Amido", + "item.basepower.bioplastic_ingot": "Bioplastica Lingotto", + "item.basepower.sprocket": "Sprocket", + "block.basepower.fluid_metal_tank": "Serbatoio in Metallo", + "item.basepower.rotator_tool": "Strumento di Rotazione", + "block.basepower.infinite_steam": "Forza Vapore Infinite", + "block.basepower.infinite_electricity": "Elettricità Infinite", + "block.basepower.infinite_quantum": "Atomica Infinite", + "block.basepower.crude_oil": "Olio a Crudo", + "block.basepower.still": "Distilleria", + "block.basepower.refined_oil": "Olio Raffinato", + "block.basepower.fluid_switch": "Valvola", + "block.basepower.converter_rf_steam": "Vapore-RF Trasformatore", + "block.basepower.converter_rf_electricity": "Elettricità-RF Trasformatore", + "block.basepower.converter_rf_quantum": "Quantistico-RF Trasformatore", + "block.basepower.converter_tr_electricity": "Elettricità-EU Trasformatore", + "block.basepower.fluid_pipe_terminal": "Tubo" +} diff --git a/src/main/resources/assets/basepower/lang/ja_jp.json b/src/main/resources/assets/basepower/lang/ja_jp.json new file mode 100644 index 00000000..d6628493 --- /dev/null +++ b/src/main/resources/assets/basepower/lang/ja_jp.json @@ -0,0 +1,75 @@ +{ + "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", + "nei.basepower.recipehandler.shapelessfurnace.name": "大型炉(型崩れ)", + "nei.basepower.recipehandler.shapedfurnace.name": "大型炉(形)", + "tooltip.basepower.empty": "空", + "tooltip.basepower.buckets_of": "%yの%xバケット", + "itemGroup.basepower": "パワー·アドバンテージ", + "tile.basepower.fluid_drain.name": "ドレン", + "tile.basepower.fluid_discharge.name": "放電グレート", + "tile.basepower.fluid_storage_tank.name": "貯蔵タンク", + "item.basepower.fluid_storage_tank_item.name": "貯蔵タンク", + "tile.basepower.fluid_pipe.name": "パイプ", + "tile.basepower.item_conveyor.name": "コンベア", + "tile.basepower.item_filter_block.name": "ブロックフィルタコンベア", + "tile.basepower.item_filter_food.name": "フードフィルターコンベア", + "tile.basepower.item_filter_fuel.name": "燃料フィルターコンベア", + "tile.basepower.item_filter_inventory.name": "フィルタコンベヤー", + "tile.basepower.item_filter_ore.name": "鉱石フィルターコンベア", + "tile.basepower.item_filter_plant.name": "プラントフィルタコンベヤー", + "tile.basepower.item_filter_smelt.name": "炉フィルターコンベア", + "tile.basepower.item_filter_overflow.name": "オーバーフローフィルタコンベヤー", + "tile.basepower.steel_frame.name": "鉄骨", + "item.basepower.starch.name": "デンプン", + "item.basepower.bioplastic_ingot.name": "は、バイオプラスチックインゴットを", + "item.basepower.sprocket.name": "スプロケット", + "tile.basepower.infinite_steam.name": "無限の蒸気動力", + "tile.basepower.infinite_electricity.name": "無限の電力", + "tile.basepower.infinite_quantum.name": "無限の量子パワー", + "tile.basepower.fluid_metal_tank.name": "金属製貯蔵タンク", + "item.basepower.rotator_tool.name": "回転ツール", + "tile.basepower.crude_oil.name": "原油", + "fluid.basepower.crude_oil": "原油", + "item.bucket.crude_oil.name": "原油のバケット", + "tile.basepower.still.name": "蒸留酒製造所", + "tile.basepower.refined_oil.name": "精製油", + "fluid.basepower.refined_oil": "精製油", + "item.bucket.refined_oil.name": "精製油のバケツ", + "tile.basepower.fluid_switch.name": "バルブ", + "tile.basepower.converter_rf_steam.name": "蒸気-RFトランス", + "tile.basepower.converter_rf_electricity.name": "電気-RFトランス", + "tile.basepower.converter_rf_quantum.name": "量子-RFトランス", + "tile.basepower.converter_tr_electricity.name": "電気-EUトランス", + "block.basepower.fluid_drain": "ドレン", + "block.basepower.fluid_discharge": "放電グレート", + "block.basepower.fluid_storage_tank": "貯蔵タンク", + "item.basepower.fluid_storage_tank_item": "貯蔵タンク", + "block.basepower.fluid_pipe": "パイプ", + "block.basepower.item_conveyor": "コンベア", + "block.basepower.item_filter_block": "ブロックフィルタコンベア", + "block.basepower.item_filter_food": "フードフィルターコンベア", + "block.basepower.item_filter_fuel": "燃料フィルターコンベア", + "block.basepower.item_filter_inventory": "フィルタコンベヤー", + "block.basepower.item_filter_ore": "鉱石フィルターコンベア", + "block.basepower.item_filter_plant": "プラントフィルタコンベヤー", + "block.basepower.item_filter_smelt": "炉フィルターコンベア", + "block.basepower.item_filter_overflow": "オーバーフローフィルタコンベヤー", + "block.basepower.steel_frame": "鉄骨", + "item.basepower.starch": "デンプン", + "item.basepower.bioplastic_ingot": "は、バイオプラスチックインゴットを", + "item.basepower.sprocket": "スプロケット", + "block.basepower.infinite_steam": "無限の蒸気動力", + "block.basepower.infinite_electricity": "無限の電力", + "block.basepower.infinite_quantum": "無限の量子パワー", + "block.basepower.fluid_metal_tank": "金属製貯蔵タンク", + "item.basepower.rotator_tool": "回転ツール", + "block.basepower.crude_oil": "原油", + "block.basepower.still": "蒸留酒製造所", + "block.basepower.refined_oil": "精製油", + "block.basepower.fluid_switch": "バルブ", + "block.basepower.converter_rf_steam": "蒸気-RFトランス", + "block.basepower.converter_rf_electricity": "電気-RFトランス", + "block.basepower.converter_rf_quantum": "量子-RFトランス", + "block.basepower.converter_tr_electricity": "電気-EUトランス", + "block.basepower.fluid_pipe_terminal": "パイプ" +} diff --git a/src/main/resources/assets/basepower/lang/ko_kr.json b/src/main/resources/assets/basepower/lang/ko_kr.json new file mode 100644 index 00000000..a3e9b68c --- /dev/null +++ b/src/main/resources/assets/basepower/lang/ko_kr.json @@ -0,0 +1,75 @@ +{ + "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", + "nei.basepower.recipehandler.shapelessfurnace.name": "대형 전기로 (못생긴)", + "nei.basepower.recipehandler.shapedfurnace.name": "대형 전기로 (모양)", + "tooltip.basepower.empty": "빈", + "tooltip.tooltip.basepower.buckets_of": "%는 X 버킷", + "itemGroup.basepower": "전원의 장점을 ", + "tile.basepower.fluid_drain.name": "드레인", + "tile.basepower.fluid_discharge.name": "방전 화격자", + "tile.basepower.fluid_storage_tank.name": "저장 탱크", + "item.basepower.fluid_storage_tank_item.name": "는 저장 탱크를 ", + "tile.basepower.fluid_pipe.name": "파이프", + "tile.basepower.item_conveyor.name": "컨베이어", + "tile.basepower.item_filter_block.name": "블록 필터 컨베이어", + "tile.basepower.item_filter_food.name": "식품 필터 컨베이어", + "tile.basepower.item_filter_fuel.name": "연료 필터 컨베이어", + "tile.basepower.item_filter_inventory.name": "필터 컨베이어", + "tile.basepower.item_filter_ore.name": "광석 필터 컨베이어", + "tile.basepower.item_filter_plant.name": "물만 주면 식물이 필터 컨베이어", + "tile.basepower.item_filter_smelt.name": "노 필터 컨베이어", + "tile.basepower.item_filter_overflow.name": "오버 플로우필터 컨베이어", + "tile.basepower.steel_frame.name": "스틸 프레임", + "item.basepower.starch.name": "전분", + "item.basepower.bioplastic_ingot.name": "는 바이오 플라스틱 잉곳을 ", + "item.basepower.sprocket.name": "스프로킷", + "tile.basepower.fluid_metal_tank.name": "금속 저장조", + "item.basepower.rotator_tool.name": "회전 도구", + "tile.basepower.infinite_steam.name": "무한 증기 력", + "tile.basepower.infinite_electricity.name": "무한 전력", + "tile.basepower.infinite_quantum.name": "무한 양자 전력", + "tile.basepower.crude_oil.name": "원유", + "fluid.basepower.crude_oil": "원유", + "item.bucket.crude_oil.name": "원유의 버킷", + "tile.basepower.still.name": "증류소", + "tile.basepower.refined_oil.name": "정유", + "fluid.basepower.refined_oil": "정유", + "item.bucket.refined_oil.name": "정유의 통", + "tile.basepower.fluid_switch.name": "판막", + "tile.basepower.converter_rf_steam.name": "증기-RF 전원 트랜스", + "tile.basepower.converter_rf_electricity.name": "전기-RF 전원 트랜스", + "tile.basepower.converter_rf_quantum.name": "양자-RF 전원 트랜스", + "tile.basepower.converter_tr_electricity.name": "전기-EU 전원 트랜스", + "block.basepower.fluid_drain": "드레인", + "block.basepower.fluid_discharge": "방전 화격자", + "block.basepower.fluid_storage_tank": "저장 탱크", + "item.basepower.fluid_storage_tank_item": "는 저장 탱크를 ", + "block.basepower.fluid_pipe": "파이프", + "block.basepower.item_conveyor": "컨베이어", + "block.basepower.item_filter_block": "블록 필터 컨베이어", + "block.basepower.item_filter_food": "식품 필터 컨베이어", + "block.basepower.item_filter_fuel": "연료 필터 컨베이어", + "block.basepower.item_filter_inventory": "필터 컨베이어", + "block.basepower.item_filter_ore": "광석 필터 컨베이어", + "block.basepower.item_filter_plant": "물만 주면 식물이 필터 컨베이어", + "block.basepower.item_filter_smelt": "노 필터 컨베이어", + "block.basepower.item_filter_overflow": "오버 플로우필터 컨베이어", + "block.basepower.steel_frame": "스틸 프레임", + "item.basepower.starch": "전분", + "item.basepower.bioplastic_ingot": "는 바이오 플라스틱 잉곳을 ", + "item.basepower.sprocket": "스프로킷", + "block.basepower.fluid_metal_tank": "금속 저장조", + "item.basepower.rotator_tool": "회전 도구", + "block.basepower.infinite_steam": "무한 증기 력", + "block.basepower.infinite_electricity": "무한 전력", + "block.basepower.infinite_quantum": "무한 양자 전력", + "block.basepower.crude_oil": "원유", + "block.basepower.still": "증류소", + "block.basepower.refined_oil": "정유", + "block.basepower.fluid_switch": "판막", + "block.basepower.converter_rf_steam": "증기-RF 전원 트랜스", + "block.basepower.converter_rf_electricity": "전기-RF 전원 트랜스", + "block.basepower.converter_rf_quantum": "양자-RF 전원 트랜스", + "block.basepower.converter_tr_electricity": "전기-EU 전원 트랜스", + "block.basepower.fluid_pipe_terminal": "파이프" +} diff --git a/src/main/resources/assets/basepower/lang/pt_br.json b/src/main/resources/assets/basepower/lang/pt_br.json new file mode 100644 index 00000000..a2a48dfc --- /dev/null +++ b/src/main/resources/assets/basepower/lang/pt_br.json @@ -0,0 +1,75 @@ +{ + "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", + "nei.basepower.recipehandler.shapelessfurnace.name": "Forno Grande (no forma)", + "nei.basepower.recipehandler.shapedfurnace.name": "Forno Grande (em forma)", + "tooltip.basepower.empty": "vazio", + "tooltip.basepower.buckets_of": "%x baldes de %y", + "itemGroup.basepower": "Vantagem de Poder", + "tile.basepower.fluid_drain.name": "Dreno", + "tile.basepower.fluid_discharge.name": "Grate Discharge", + "tile.basepower.fluid_storage_tank.name": "Tanque de armazenamento", + "item.basepower.fluid_storage_tank_item.name": "Tanque de armazenamento", + "tile.basepower.fluid_pipe.name": "Tubulação", + "tile.basepower.item_conveyor.name": "Transportador", + "tile.basepower.item_filter_block.name": "Bloquear filtro transportador", + "tile.basepower.item_filter_food.name": "Transportador Filtro de Alimentos", + "tile.basepower.item_filter_fuel.name": "Filtro de combustível Transportador", + "tile.basepower.item_filter_inventory.name": "Transportador Filtro", + "tile.basepower.item_filter_ore.name": "Ore Transportadora Filtro", + "tile.basepower.item_filter_plant.name": "Plantable Transportadora Filtro", + "tile.basepower.item_filter_smelt.name": "Fornalha Filtro Transportador", + "tile.basepower.item_filter_overflow.name": "Transbordamento Transportador Filtro", + "tile.basepower.steel_frame.name": "Armação de Aço", + "item.basepower.starch.name": "Amido", + "item.basepower.bioplastic_ingot.name": "Bioplastic lingote", + "item.basepower.sprocket.name": "Roda Dentada", + "tile.basepower.fluid_metal_tank.name": "Tanque de Armazenamento de Aço", + "item.basepower.rotator_tool.name": "Ferramenta de Rotação", + "tile.basepower.infinite_steam.name": "Energia a Vapor Infinito", + "tile.basepower.infinite_electricity.name": "Energia Elétrica Infinito", + "tile.basepower.infinite_quantum.name": "Energia Quantum Infinito", + "tile.basepower.crude_oil.name": "Óleo Cru", + "fluid.basepower.crude_oil": "Óleo Cru", + "item.bucket.crude_oil.name": "Balde de Óleo Cru", + "tile.basepower.still.name": "Destilaria", + "tile.basepower.refined_oil.name": "Óleo Refinado", + "fluid.basepower.refined_oil": "Óleo Refinado", + "item.bucket.refined_oil.name": "Balde de Óleo Refinado", + "tile.basepower.fluid_switch.name": "Válvula", + "tile.basepower.converter_rf_steam.name": "Vapor-RF Transformador", + "tile.basepower.converter_rf_electricity.name": "Electricidade-RF Transformador", + "tile.basepower.converter_rf_quantum.name": "Quantidade-RF Transformador", + "tile.basepower.converter_tr_electricity.name": "Electricidade-EU Transformador", + "block.basepower.fluid_drain": "Dreno", + "block.basepower.fluid_discharge": "Grate Discharge", + "block.basepower.fluid_storage_tank": "Tanque de armazenamento", + "item.basepower.fluid_storage_tank_item": "Tanque de armazenamento", + "block.basepower.fluid_pipe": "Tubulação", + "block.basepower.item_conveyor": "Transportador", + "block.basepower.item_filter_block": "Bloquear filtro transportador", + "block.basepower.item_filter_food": "Transportador Filtro de Alimentos", + "block.basepower.item_filter_fuel": "Filtro de combustível Transportador", + "block.basepower.item_filter_inventory": "Transportador Filtro", + "block.basepower.item_filter_ore": "Ore Transportadora Filtro", + "block.basepower.item_filter_plant": "Plantable Transportadora Filtro", + "block.basepower.item_filter_smelt": "Fornalha Filtro Transportador", + "block.basepower.item_filter_overflow": "Transbordamento Transportador Filtro", + "block.basepower.steel_frame": "Armação de Aço", + "item.basepower.starch": "Amido", + "item.basepower.bioplastic_ingot": "Bioplastic lingote", + "item.basepower.sprocket": "Roda Dentada", + "block.basepower.fluid_metal_tank": "Tanque de Armazenamento de Aço", + "item.basepower.rotator_tool": "Ferramenta de Rotação", + "block.basepower.infinite_steam": "Energia a Vapor Infinito", + "block.basepower.infinite_electricity": "Energia Elétrica Infinito", + "block.basepower.infinite_quantum": "Energia Quantum Infinito", + "block.basepower.crude_oil": "Óleo Cru", + "block.basepower.still": "Destilaria", + "block.basepower.refined_oil": "Óleo Refinado", + "block.basepower.fluid_switch": "Válvula", + "block.basepower.converter_rf_steam": "Vapor-RF Transformador", + "block.basepower.converter_rf_electricity": "Electricidade-RF Transformador", + "block.basepower.converter_rf_quantum": "Quantidade-RF Transformador", + "block.basepower.converter_tr_electricity": "Electricidade-EU Transformador", + "block.basepower.fluid_pipe_terminal": "Tubulação" +} diff --git a/src/main/resources/assets/basepower/lang/zh_cn.json b/src/main/resources/assets/basepower/lang/zh_cn.json new file mode 100644 index 00000000..631c3180 --- /dev/null +++ b/src/main/resources/assets/basepower/lang/zh_cn.json @@ -0,0 +1,75 @@ +{ + "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", + "nei.basepower.recipehandler.shapelessfurnace.name": "大熔炉(无形的)", + "nei.basepower.recipehandler.shapedfurnace.name": "大熔炉(有形的)", + "tooltip.basepower.empty": "空", + "tooltip.basepower.buckets_of": "%x水桶的%y", + "itemGroup.basepower": "能量优势", + "tile.basepower.fluid_drain.name": "抽水机", + "tile.basepower.fluid_discharge.name": "排液器", + "tile.basepower.fluid_storage_tank.name": "便携式液体储罐", + "item.basepower.fluid_storage_tank_item.name": "便携式液体储罐", + "tile.basepower.fluid_pipe.name": "液体管", + "tile.basepower.item_conveyor.name": "输送机", + "tile.basepower.item_filter_block.name": "方块过滤输送机", + "tile.basepower.item_filter_food.name": "食品过滤输送机", + "tile.basepower.item_filter_fuel.name": "燃料过滤输送机", + "tile.basepower.item_filter_inventory.name": "选择过滤输送机", + "tile.basepower.item_filter_ore.name": "矿物过滤输送机", + "tile.basepower.item_filter_plant.name": "种子过滤输送机", + "tile.basepower.item_filter_smelt.name": "熔炉过滤输送机", + "tile.basepower.item_filter_overflow.name": "溢出过滤输送机", + "tile.basepower.steel_frame.name": "钢框架", + "item.basepower.starch.name": "淀粉", + "item.basepower.bioplastic_ingot.name": "生物塑料锭", + "item.basepower.sprocket.name": "链轮", + "tile.basepower.fluid_metal_tank.name": "金属液罐", + "item.basepower.rotator_tool.name": "旋转扳手", + "tile.basepower.infinite_steam.name": "无限蒸汽动力", + "tile.basepower.infinite_electricity.name": "无限电力", + "tile.basepower.infinite_quantum.name": "无限量子动力", + "tile.basepower.crude_oil.name": "原油", + "fluid.basepower.crude_oil": "原油", + "item.bucket.crude_oil.name": "原油桶", + "tile.basepower.still.name": "酒廠", + "tile.basepower.refined_oil.name": "成品油", + "fluid.basepower.refined_oil": "成品油", + "item.bucket.refined_oil.name": "成品油的桶", + "tile.basepower.fluid_switch.name": "閥", + "tile.basepower.converter_rf_steam.name": "蒸汽-RF 變壓器", + "tile.basepower.converter_rf_electricity.name": "電力-RF 變壓器", + "tile.basepower.converter_rf_quantum.name": "量子-RF 變壓器", + "tile.basepower.converter_tr_electricity.name": "電力-EU 變壓器", + "block.basepower.fluid_drain": "抽水机", + "block.basepower.fluid_discharge": "排液器", + "block.basepower.fluid_storage_tank": "便携式液体储罐", + "item.basepower.fluid_storage_tank_item": "便携式液体储罐", + "block.basepower.fluid_pipe": "液体管", + "block.basepower.item_conveyor": "输送机", + "block.basepower.item_filter_block": "方块过滤输送机", + "block.basepower.item_filter_food": "食品过滤输送机", + "block.basepower.item_filter_fuel": "燃料过滤输送机", + "block.basepower.item_filter_inventory": "选择过滤输送机", + "block.basepower.item_filter_ore": "矿物过滤输送机", + "block.basepower.item_filter_plant": "种子过滤输送机", + "block.basepower.item_filter_smelt": "熔炉过滤输送机", + "block.basepower.item_filter_overflow": "溢出过滤输送机", + "block.basepower.steel_frame": "钢框架", + "item.basepower.starch": "淀粉", + "item.basepower.bioplastic_ingot": "生物塑料锭", + "item.basepower.sprocket": "链轮", + "block.basepower.fluid_metal_tank": "金属液罐", + "item.basepower.rotator_tool": "旋转扳手", + "block.basepower.infinite_steam": "无限蒸汽动力", + "block.basepower.infinite_electricity": "无限电力", + "block.basepower.infinite_quantum": "无限量子动力", + "block.basepower.crude_oil": "原油", + "block.basepower.still": "酒廠", + "block.basepower.refined_oil": "成品油", + "block.basepower.fluid_switch": "閥", + "block.basepower.converter_rf_steam": "蒸汽-RF 變壓器", + "block.basepower.converter_rf_electricity": "電力-RF 變壓器", + "block.basepower.converter_rf_quantum": "量子-RF 變壓器", + "block.basepower.converter_tr_electricity": "電力-EU 變壓器", + "block.basepower.fluid_pipe_terminal": "液体管" +} diff --git a/src/main/resources/assets/poweradvantage/models/block/converter_rf_electricity.json b/src/main/resources/assets/basepower/models/block/converter_rf_electricity.json similarity index 94% rename from src/main/resources/assets/poweradvantage/models/block/converter_rf_electricity.json rename to src/main/resources/assets/basepower/models/block/converter_rf_electricity.json index 01a03f62..dbdf82ad 100644 --- a/src/main/resources/assets/poweradvantage/models/block/converter_rf_electricity.json +++ b/src/main/resources/assets/basepower/models/block/converter_rf_electricity.json @@ -1,10 +1,10 @@ { "parent": "block/block", "textures": { - "face": "poweradvantage:blocks/steel_frame", - "core": "poweradvantage:materials/plastic", - "plug": "poweradvantage:blocks/filter_bottom", - "particle": "poweradvantage:materials/steel" + "face": "basepower:blocks/steel_frame", + "core": "basepower:materials/plastic", + "plug": "basepower:blocks/filter_bottom", + "particle": "basepower:materials/steel" }, "elements": [ { diff --git a/src/main/resources/assets/poweradvantage/models/block/converter_rf_quantum.json b/src/main/resources/assets/basepower/models/block/converter_rf_quantum.json similarity index 96% rename from src/main/resources/assets/poweradvantage/models/block/converter_rf_quantum.json rename to src/main/resources/assets/basepower/models/block/converter_rf_quantum.json index 5733e1cb..bd4bfd9b 100644 --- a/src/main/resources/assets/poweradvantage/models/block/converter_rf_quantum.json +++ b/src/main/resources/assets/basepower/models/block/converter_rf_quantum.json @@ -1,10 +1,10 @@ { "parent": "block/block", "textures": { - "face": "poweradvantage:blocks/steel_frame", + "face": "basepower:blocks/steel_frame", "core": "minecraft:block/packed_ice", - "plug": "poweradvantage:blocks/filter_bottom", - "particle": "poweradvantage:materials/steel" + "plug": "basepower:blocks/filter_bottom", + "particle": "basepower:materials/steel" }, "elements": [ { diff --git a/src/main/resources/assets/poweradvantage/models/block/converter_rf_steam.json b/src/main/resources/assets/basepower/models/block/converter_rf_steam.json similarity index 95% rename from src/main/resources/assets/poweradvantage/models/block/converter_rf_steam.json rename to src/main/resources/assets/basepower/models/block/converter_rf_steam.json index a831250b..b1fe2c85 100644 --- a/src/main/resources/assets/poweradvantage/models/block/converter_rf_steam.json +++ b/src/main/resources/assets/basepower/models/block/converter_rf_steam.json @@ -1,10 +1,10 @@ { "parent": "block/block", "textures": { - "face": "poweradvantage:blocks/steel_frame", - "core": "poweradvantage:materials/brass", - "plug": "poweradvantage:blocks/filter_bottom", - "particle": "poweradvantage:materials/steel" + "face": "basepower:blocks/steel_frame", + "core": "basepower:materials/brass", + "plug": "basepower:blocks/filter_bottom", + "particle": "basepower:materials/steel" }, "elements": [ { diff --git a/src/main/resources/assets/basepower/models/block/converter_tr_electricity.json b/src/main/resources/assets/basepower/models/block/converter_tr_electricity.json new file mode 100644 index 00000000..102f1072 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/converter_tr_electricity.json @@ -0,0 +1,6 @@ +{ + "parent": "block/cube_all", + "textures": { + "all": "basepower:blocks/techreborn_converter" + } +} diff --git a/src/main/resources/assets/poweradvantage/models/block/conveyor_base.json b/src/main/resources/assets/basepower/models/block/conveyor_base.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/conveyor_base.json rename to src/main/resources/assets/basepower/models/block/conveyor_base.json diff --git a/src/main/resources/assets/poweradvantage/models/block/crude_oil.json b/src/main/resources/assets/basepower/models/block/crude_oil.json similarity index 52% rename from src/main/resources/assets/poweradvantage/models/block/crude_oil.json rename to src/main/resources/assets/basepower/models/block/crude_oil.json index 37af9662..7176ee6e 100644 --- a/src/main/resources/assets/poweradvantage/models/block/crude_oil.json +++ b/src/main/resources/assets/basepower/models/block/crude_oil.json @@ -1,6 +1,6 @@ { "parent": "minecraft:block/block", "textures": { - "particle": "poweradvantage:blocks/crude_oil_still" + "particle": "basepower:blocks/crude_oil_still" } } diff --git a/src/main/resources/assets/basepower/models/block/fluid_discharge.json b/src/main/resources/assets/basepower/models/block/fluid_discharge.json new file mode 100644 index 00000000..2ee79703 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_discharge.json @@ -0,0 +1,8 @@ +{ + "parent": "block/cube_bottom_top", + "textures": { + "top": "basepower:blocks/fluid_discharge_side", + "bottom": "basepower:blocks/fluid_discharge_face", + "side": "basepower:blocks/fluid_discharge_side" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/block/fluid_drain.json b/src/main/resources/assets/basepower/models/block/fluid_drain.json new file mode 100644 index 00000000..b076dd00 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_drain.json @@ -0,0 +1,8 @@ +{ + "parent": "block/cube_bottom_top", + "textures": { + "top": "basepower:blocks/fluid_drain_face", + "bottom": "basepower:blocks/fluid_drain_side", + "side": "basepower:blocks/fluid_drain_side" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_metal_tank.json b/src/main/resources/assets/basepower/models/block/fluid_metal_tank.json similarity index 94% rename from src/main/resources/assets/poweradvantage/models/block/fluid_metal_tank.json rename to src/main/resources/assets/basepower/models/block/fluid_metal_tank.json index 33428415..85fc4abf 100644 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_metal_tank.json +++ b/src/main/resources/assets/basepower/models/block/fluid_metal_tank.json @@ -1,10 +1,10 @@ { "parent": "block/block", "textures": { - "top": "poweradvantage:blocks/fluid_metal_tank_top", - "bottom": "poweradvantage:blocks/fluid_metal_tank_top", - "side": "poweradvantage:blocks/fluid_metal_tank_side", - "particle": "poweradvantage:blocks/fluid_metal_tank_side" + "top": "basepower:blocks/fluid_metal_tank_top", + "bottom": "basepower:blocks/fluid_metal_tank_top", + "side": "basepower:blocks/fluid_metal_tank_side", + "particle": "basepower:blocks/fluid_metal_tank_side" }, "elements": [ { diff --git a/src/main/resources/assets/basepower/models/block/fluid_pipe_0.json b/src/main/resources/assets/basepower/models/block/fluid_pipe_0.json new file mode 100644 index 00000000..65e3862c --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_pipe_0.json @@ -0,0 +1,7 @@ +{ + "parent": "basepower:block/pipe4_0", + "textures": { + "texture": "basepower:blocks/fluid_pipe", + "particle": "basepower:materials/iron" + } +} diff --git a/src/main/resources/assets/basepower/models/block/fluid_pipe_densuw.json b/src/main/resources/assets/basepower/models/block/fluid_pipe_densuw.json new file mode 100644 index 00000000..3de01b0f --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_pipe_densuw.json @@ -0,0 +1,7 @@ +{ + "parent": "basepower:block/pipe4_densuw", + "textures": { + "texture": "basepower:blocks/fluid_pipe", + "particle": "basepower:materials/iron" + } +} diff --git a/src/main/resources/assets/basepower/models/block/fluid_pipe_desuw.json b/src/main/resources/assets/basepower/models/block/fluid_pipe_desuw.json new file mode 100644 index 00000000..c61fadb9 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_pipe_desuw.json @@ -0,0 +1,7 @@ +{ + "parent": "basepower:block/pipe4_desuw", + "textures": { + "texture": "basepower:blocks/fluid_pipe", + "particle": "basepower:materials/iron" + } +} diff --git a/src/main/resources/assets/basepower/models/block/fluid_pipe_dnu.json b/src/main/resources/assets/basepower/models/block/fluid_pipe_dnu.json new file mode 100644 index 00000000..cfb26b68 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_pipe_dnu.json @@ -0,0 +1,7 @@ +{ + "parent": "basepower:block/pipe4_dnu", + "textures": { + "texture": "basepower:blocks/fluid_pipe", + "particle": "basepower:materials/iron" + } +} diff --git a/src/main/resources/assets/basepower/models/block/fluid_pipe_ensw.json b/src/main/resources/assets/basepower/models/block/fluid_pipe_ensw.json new file mode 100644 index 00000000..d0f8c0c8 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_pipe_ensw.json @@ -0,0 +1,7 @@ +{ + "parent": "basepower:block/pipe4_ensw", + "textures": { + "texture": "basepower:blocks/fluid_pipe", + "particle": "basepower:materials/iron" + } +} diff --git a/src/main/resources/assets/basepower/models/block/fluid_pipe_n.json b/src/main/resources/assets/basepower/models/block/fluid_pipe_n.json new file mode 100644 index 00000000..2401588e --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_pipe_n.json @@ -0,0 +1,7 @@ +{ + "parent": "basepower:block/pipe4_n", + "textures": { + "texture": "basepower:blocks/fluid_pipe", + "particle": "basepower:materials/iron" + } +} diff --git a/src/main/resources/assets/basepower/models/block/fluid_pipe_ns.json b/src/main/resources/assets/basepower/models/block/fluid_pipe_ns.json new file mode 100644 index 00000000..b2dc847d --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_pipe_ns.json @@ -0,0 +1,7 @@ +{ + "parent": "basepower:block/pipe4_ns", + "textures": { + "texture": "basepower:blocks/fluid_pipe", + "particle": "basepower:materials/iron" + } +} diff --git a/src/main/resources/assets/basepower/models/block/fluid_pipe_nsuw.json b/src/main/resources/assets/basepower/models/block/fluid_pipe_nsuw.json new file mode 100644 index 00000000..eeaf1cc9 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_pipe_nsuw.json @@ -0,0 +1,7 @@ +{ + "parent": "basepower:block/pipe4_nsuw", + "textures": { + "texture": "basepower:blocks/fluid_pipe", + "particle": "basepower:materials/iron" + } +} diff --git a/src/main/resources/assets/basepower/models/block/fluid_pipe_nsw.json b/src/main/resources/assets/basepower/models/block/fluid_pipe_nsw.json new file mode 100644 index 00000000..75c2bf60 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_pipe_nsw.json @@ -0,0 +1,7 @@ +{ + "parent": "basepower:block/pipe4_nsw", + "textures": { + "texture": "basepower:blocks/fluid_pipe", + "particle": "basepower:materials/iron" + } +} diff --git a/src/main/resources/assets/basepower/models/block/fluid_pipe_nuw.json b/src/main/resources/assets/basepower/models/block/fluid_pipe_nuw.json new file mode 100644 index 00000000..4dba871f --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_pipe_nuw.json @@ -0,0 +1,7 @@ +{ + "parent": "basepower:block/pipe4_nuw", + "textures": { + "texture": "basepower:blocks/fluid_pipe", + "particle": "basepower:materials/iron" + } +} diff --git a/src/main/resources/assets/basepower/models/block/fluid_pipe_nw.json b/src/main/resources/assets/basepower/models/block/fluid_pipe_nw.json new file mode 100644 index 00000000..a7aa067c --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_pipe_nw.json @@ -0,0 +1,7 @@ +{ + "parent": "basepower:block/pipe4_nw", + "textures": { + "texture": "basepower:blocks/fluid_pipe", + "particle": "basepower:materials/iron" + } +} diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_storage_tank.json b/src/main/resources/assets/basepower/models/block/fluid_storage_tank.json similarity index 97% rename from src/main/resources/assets/poweradvantage/models/block/fluid_storage_tank.json rename to src/main/resources/assets/basepower/models/block/fluid_storage_tank.json index c8daff39..70f1e281 100644 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_storage_tank.json +++ b/src/main/resources/assets/basepower/models/block/fluid_storage_tank.json @@ -1,9 +1,9 @@ { "parent": "block/block", "textures": { - "walls": "poweradvantage:materials/plastic", - "supports": "poweradvantage:materials/steel", - "particle": "poweradvantage:materials/plastic" + "walls": "basepower:materials/plastic", + "supports": "basepower:materials/steel", + "particle": "basepower:materials/plastic" }, "elements": [ { diff --git a/src/main/resources/assets/basepower/models/block/fluid_switch_off.json b/src/main/resources/assets/basepower/models/block/fluid_switch_off.json new file mode 100644 index 00000000..53991a62 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_switch_off.json @@ -0,0 +1,7 @@ +{ + "parent": "block/cube_all", + "textures": { + "all": "basepower:blocks/fluid_switch_off", + "particle": "basepower:materials/steel" + } +} diff --git a/src/main/resources/assets/basepower/models/block/fluid_switch_on.json b/src/main/resources/assets/basepower/models/block/fluid_switch_on.json new file mode 100644 index 00000000..b132b591 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/fluid_switch_on.json @@ -0,0 +1,7 @@ +{ + "parent": "block/cube_all", + "textures": { + "all": "basepower:blocks/fluid_switch_on", + "particle": "basepower:materials/steel" + } +} diff --git a/src/main/resources/assets/poweradvantage/models/block/infinite_electricity.json b/src/main/resources/assets/basepower/models/block/infinite_electricity.json similarity index 52% rename from src/main/resources/assets/poweradvantage/models/block/infinite_electricity.json rename to src/main/resources/assets/basepower/models/block/infinite_electricity.json index 40f79af8..83005105 100644 --- a/src/main/resources/assets/poweradvantage/models/block/infinite_electricity.json +++ b/src/main/resources/assets/basepower/models/block/infinite_electricity.json @@ -1,6 +1,6 @@ { "parent": "block/cube_all", "textures": { - "all": "poweradvantage:materials/metal_panel" + "all": "basepower:materials/metal_panel" } } \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/block/infinite_quantum.json b/src/main/resources/assets/basepower/models/block/infinite_quantum.json similarity index 52% rename from src/main/resources/assets/poweradvantage/models/block/infinite_quantum.json rename to src/main/resources/assets/basepower/models/block/infinite_quantum.json index 40f79af8..83005105 100644 --- a/src/main/resources/assets/poweradvantage/models/block/infinite_quantum.json +++ b/src/main/resources/assets/basepower/models/block/infinite_quantum.json @@ -1,6 +1,6 @@ { "parent": "block/cube_all", "textures": { - "all": "poweradvantage:materials/metal_panel" + "all": "basepower:materials/metal_panel" } } \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/block/infinite_steam.json b/src/main/resources/assets/basepower/models/block/infinite_steam.json similarity index 52% rename from src/main/resources/assets/poweradvantage/models/block/infinite_steam.json rename to src/main/resources/assets/basepower/models/block/infinite_steam.json index 40f79af8..83005105 100644 --- a/src/main/resources/assets/poweradvantage/models/block/infinite_steam.json +++ b/src/main/resources/assets/basepower/models/block/infinite_steam.json @@ -1,6 +1,6 @@ { "parent": "block/cube_all", "textures": { - "all": "poweradvantage:materials/metal_panel" + "all": "basepower:materials/metal_panel" } } \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/block/item_conveyor.json b/src/main/resources/assets/basepower/models/block/item_conveyor.json new file mode 100644 index 00000000..9da9824c --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/item_conveyor.json @@ -0,0 +1,10 @@ +{ + "parent": "basepower:block/conveyor_base", + "textures": { + "top": "basepower:blocks/item_conveyor_top", + "bottom": "basepower:blocks/item_conveyor_top", + "side": "basepower:blocks/item_conveyor_side", + "end": "basepower:blocks/item_conveyor_end", + "rim": "basepower:materials/steel" + } +} diff --git a/src/main/resources/assets/basepower/models/block/item_filter_block.json b/src/main/resources/assets/basepower/models/block/item_filter_block.json new file mode 100644 index 00000000..fadbf83d --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/item_filter_block.json @@ -0,0 +1,10 @@ +{ + "parent": "basepower:block/conveyor_base", + "textures": { + "top": "basepower:blocks/item_conveyor_top", + "bottom": "basepower:blocks/filter_bottom", + "side": "basepower:blocks/filter_side_block", + "end": "basepower:blocks/item_conveyor_end", + "rim": "basepower:materials/steel" + } +} diff --git a/src/main/resources/assets/basepower/models/block/item_filter_food.json b/src/main/resources/assets/basepower/models/block/item_filter_food.json new file mode 100644 index 00000000..67b14ad3 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/item_filter_food.json @@ -0,0 +1,10 @@ +{ + "parent": "basepower:block/conveyor_base", + "textures": { + "top": "basepower:blocks/item_conveyor_top", + "bottom": "basepower:blocks/filter_bottom", + "side": "basepower:blocks/filter_side_food", + "end": "basepower:blocks/item_conveyor_end", + "rim": "basepower:materials/steel" + } +} diff --git a/src/main/resources/assets/basepower/models/block/item_filter_fuel.json b/src/main/resources/assets/basepower/models/block/item_filter_fuel.json new file mode 100644 index 00000000..41c8e925 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/item_filter_fuel.json @@ -0,0 +1,10 @@ +{ + "parent": "basepower:block/conveyor_base", + "textures": { + "top": "basepower:blocks/item_conveyor_top", + "bottom": "basepower:blocks/filter_bottom", + "side": "basepower:blocks/filter_side_fuel", + "end": "basepower:blocks/item_conveyor_end", + "rim": "basepower:materials/steel" + } +} diff --git a/src/main/resources/assets/basepower/models/block/item_filter_inventory.json b/src/main/resources/assets/basepower/models/block/item_filter_inventory.json new file mode 100644 index 00000000..cbec1c3c --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/item_filter_inventory.json @@ -0,0 +1,10 @@ +{ + "parent": "basepower:block/conveyor_base", + "textures": { + "top": "basepower:blocks/item_conveyor_top", + "bottom": "basepower:blocks/filter_bottom", + "side": "basepower:blocks/filter_side_inventory", + "end": "basepower:blocks/item_conveyor_end", + "rim": "basepower:materials/steel" + } +} diff --git a/src/main/resources/assets/basepower/models/block/item_filter_ore.json b/src/main/resources/assets/basepower/models/block/item_filter_ore.json new file mode 100644 index 00000000..040a4fd8 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/item_filter_ore.json @@ -0,0 +1,10 @@ +{ + "parent": "basepower:block/conveyor_base", + "textures": { + "top": "basepower:blocks/item_conveyor_top", + "bottom": "basepower:blocks/filter_bottom", + "side": "basepower:blocks/filter_side_ore", + "end": "basepower:blocks/item_conveyor_end", + "rim": "basepower:materials/steel" + } +} diff --git a/src/main/resources/assets/basepower/models/block/item_filter_overflow.json b/src/main/resources/assets/basepower/models/block/item_filter_overflow.json new file mode 100644 index 00000000..4f71669f --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/item_filter_overflow.json @@ -0,0 +1,10 @@ +{ + "parent": "basepower:block/conveyor_base", + "textures": { + "top": "basepower:blocks/item_conveyor_top", + "bottom": "basepower:blocks/filter_bottom", + "side": "basepower:blocks/filter_side_overflow", + "end": "basepower:blocks/item_conveyor_end", + "rim": "basepower:materials/steel" + } +} diff --git a/src/main/resources/assets/basepower/models/block/item_filter_plant.json b/src/main/resources/assets/basepower/models/block/item_filter_plant.json new file mode 100644 index 00000000..1e097b16 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/item_filter_plant.json @@ -0,0 +1,10 @@ +{ + "parent": "basepower:block/conveyor_base", + "textures": { + "top": "basepower:blocks/item_conveyor_top", + "bottom": "basepower:blocks/filter_bottom", + "side": "basepower:blocks/filter_side_plantable", + "end": "basepower:blocks/item_conveyor_end", + "rim": "basepower:materials/steel" + } +} diff --git a/src/main/resources/assets/basepower/models/block/item_filter_smelt.json b/src/main/resources/assets/basepower/models/block/item_filter_smelt.json new file mode 100644 index 00000000..ee436335 --- /dev/null +++ b/src/main/resources/assets/basepower/models/block/item_filter_smelt.json @@ -0,0 +1,10 @@ +{ + "parent": "basepower:block/conveyor_base", + "textures": { + "top": "basepower:blocks/item_conveyor_top", + "bottom": "basepower:blocks/filter_bottom", + "side": "basepower:blocks/filter_side_smeltable", + "end": "basepower:blocks/item_conveyor_end", + "rim": "basepower:materials/steel" + } +} diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe1_0.json b/src/main/resources/assets/basepower/models/block/pipe1_0.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe1_0.json rename to src/main/resources/assets/basepower/models/block/pipe1_0.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe1_densuw.json b/src/main/resources/assets/basepower/models/block/pipe1_densuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe1_densuw.json rename to src/main/resources/assets/basepower/models/block/pipe1_densuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe1_desuw.json b/src/main/resources/assets/basepower/models/block/pipe1_desuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe1_desuw.json rename to src/main/resources/assets/basepower/models/block/pipe1_desuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe1_dnu.json b/src/main/resources/assets/basepower/models/block/pipe1_dnu.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe1_dnu.json rename to src/main/resources/assets/basepower/models/block/pipe1_dnu.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe1_ensw.json b/src/main/resources/assets/basepower/models/block/pipe1_ensw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe1_ensw.json rename to src/main/resources/assets/basepower/models/block/pipe1_ensw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe1_n.json b/src/main/resources/assets/basepower/models/block/pipe1_n.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe1_n.json rename to src/main/resources/assets/basepower/models/block/pipe1_n.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe1_ns.json b/src/main/resources/assets/basepower/models/block/pipe1_ns.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe1_ns.json rename to src/main/resources/assets/basepower/models/block/pipe1_ns.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe1_nsuw.json b/src/main/resources/assets/basepower/models/block/pipe1_nsuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe1_nsuw.json rename to src/main/resources/assets/basepower/models/block/pipe1_nsuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe1_nsw.json b/src/main/resources/assets/basepower/models/block/pipe1_nsw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe1_nsw.json rename to src/main/resources/assets/basepower/models/block/pipe1_nsw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe1_nuw.json b/src/main/resources/assets/basepower/models/block/pipe1_nuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe1_nuw.json rename to src/main/resources/assets/basepower/models/block/pipe1_nuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe1_nw.json b/src/main/resources/assets/basepower/models/block/pipe1_nw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe1_nw.json rename to src/main/resources/assets/basepower/models/block/pipe1_nw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe2_0.json b/src/main/resources/assets/basepower/models/block/pipe2_0.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe2_0.json rename to src/main/resources/assets/basepower/models/block/pipe2_0.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe2_densuw.json b/src/main/resources/assets/basepower/models/block/pipe2_densuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe2_densuw.json rename to src/main/resources/assets/basepower/models/block/pipe2_densuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe2_desuw.json b/src/main/resources/assets/basepower/models/block/pipe2_desuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe2_desuw.json rename to src/main/resources/assets/basepower/models/block/pipe2_desuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe2_dnu.json b/src/main/resources/assets/basepower/models/block/pipe2_dnu.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe2_dnu.json rename to src/main/resources/assets/basepower/models/block/pipe2_dnu.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe2_ensw.json b/src/main/resources/assets/basepower/models/block/pipe2_ensw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe2_ensw.json rename to src/main/resources/assets/basepower/models/block/pipe2_ensw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe2_n.json b/src/main/resources/assets/basepower/models/block/pipe2_n.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe2_n.json rename to src/main/resources/assets/basepower/models/block/pipe2_n.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe2_ns.json b/src/main/resources/assets/basepower/models/block/pipe2_ns.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe2_ns.json rename to src/main/resources/assets/basepower/models/block/pipe2_ns.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe2_nsuw.json b/src/main/resources/assets/basepower/models/block/pipe2_nsuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe2_nsuw.json rename to src/main/resources/assets/basepower/models/block/pipe2_nsuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe2_nsw.json b/src/main/resources/assets/basepower/models/block/pipe2_nsw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe2_nsw.json rename to src/main/resources/assets/basepower/models/block/pipe2_nsw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe2_nuw.json b/src/main/resources/assets/basepower/models/block/pipe2_nuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe2_nuw.json rename to src/main/resources/assets/basepower/models/block/pipe2_nuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe2_nw.json b/src/main/resources/assets/basepower/models/block/pipe2_nw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe2_nw.json rename to src/main/resources/assets/basepower/models/block/pipe2_nw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe3_0.json b/src/main/resources/assets/basepower/models/block/pipe3_0.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe3_0.json rename to src/main/resources/assets/basepower/models/block/pipe3_0.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe3_densuw.json b/src/main/resources/assets/basepower/models/block/pipe3_densuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe3_densuw.json rename to src/main/resources/assets/basepower/models/block/pipe3_densuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe3_desuw.json b/src/main/resources/assets/basepower/models/block/pipe3_desuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe3_desuw.json rename to src/main/resources/assets/basepower/models/block/pipe3_desuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe3_dnu.json b/src/main/resources/assets/basepower/models/block/pipe3_dnu.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe3_dnu.json rename to src/main/resources/assets/basepower/models/block/pipe3_dnu.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe3_ensw.json b/src/main/resources/assets/basepower/models/block/pipe3_ensw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe3_ensw.json rename to src/main/resources/assets/basepower/models/block/pipe3_ensw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe3_n.json b/src/main/resources/assets/basepower/models/block/pipe3_n.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe3_n.json rename to src/main/resources/assets/basepower/models/block/pipe3_n.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe3_ns.json b/src/main/resources/assets/basepower/models/block/pipe3_ns.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe3_ns.json rename to src/main/resources/assets/basepower/models/block/pipe3_ns.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe3_nsuw.json b/src/main/resources/assets/basepower/models/block/pipe3_nsuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe3_nsuw.json rename to src/main/resources/assets/basepower/models/block/pipe3_nsuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe3_nsw.json b/src/main/resources/assets/basepower/models/block/pipe3_nsw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe3_nsw.json rename to src/main/resources/assets/basepower/models/block/pipe3_nsw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe3_nuw.json b/src/main/resources/assets/basepower/models/block/pipe3_nuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe3_nuw.json rename to src/main/resources/assets/basepower/models/block/pipe3_nuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe3_nw.json b/src/main/resources/assets/basepower/models/block/pipe3_nw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe3_nw.json rename to src/main/resources/assets/basepower/models/block/pipe3_nw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe4_0.json b/src/main/resources/assets/basepower/models/block/pipe4_0.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe4_0.json rename to src/main/resources/assets/basepower/models/block/pipe4_0.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe4_densuw.json b/src/main/resources/assets/basepower/models/block/pipe4_densuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe4_densuw.json rename to src/main/resources/assets/basepower/models/block/pipe4_densuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe4_desuw.json b/src/main/resources/assets/basepower/models/block/pipe4_desuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe4_desuw.json rename to src/main/resources/assets/basepower/models/block/pipe4_desuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe4_dnu.json b/src/main/resources/assets/basepower/models/block/pipe4_dnu.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe4_dnu.json rename to src/main/resources/assets/basepower/models/block/pipe4_dnu.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe4_ensw.json b/src/main/resources/assets/basepower/models/block/pipe4_ensw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe4_ensw.json rename to src/main/resources/assets/basepower/models/block/pipe4_ensw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe4_n.json b/src/main/resources/assets/basepower/models/block/pipe4_n.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe4_n.json rename to src/main/resources/assets/basepower/models/block/pipe4_n.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe4_ns.json b/src/main/resources/assets/basepower/models/block/pipe4_ns.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe4_ns.json rename to src/main/resources/assets/basepower/models/block/pipe4_ns.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe4_nsuw.json b/src/main/resources/assets/basepower/models/block/pipe4_nsuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe4_nsuw.json rename to src/main/resources/assets/basepower/models/block/pipe4_nsuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe4_nsw.json b/src/main/resources/assets/basepower/models/block/pipe4_nsw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe4_nsw.json rename to src/main/resources/assets/basepower/models/block/pipe4_nsw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe4_nuw.json b/src/main/resources/assets/basepower/models/block/pipe4_nuw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe4_nuw.json rename to src/main/resources/assets/basepower/models/block/pipe4_nuw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/pipe4_nw.json b/src/main/resources/assets/basepower/models/block/pipe4_nw.json similarity index 100% rename from src/main/resources/assets/poweradvantage/models/block/pipe4_nw.json rename to src/main/resources/assets/basepower/models/block/pipe4_nw.json diff --git a/src/main/resources/assets/poweradvantage/models/block/refined_oil.json b/src/main/resources/assets/basepower/models/block/refined_oil.json similarity index 51% rename from src/main/resources/assets/poweradvantage/models/block/refined_oil.json rename to src/main/resources/assets/basepower/models/block/refined_oil.json index 81449e23..28f83b65 100644 --- a/src/main/resources/assets/poweradvantage/models/block/refined_oil.json +++ b/src/main/resources/assets/basepower/models/block/refined_oil.json @@ -1,6 +1,6 @@ { "parent": "minecraft:block/block", "textures": { - "particle": "poweradvantage:blocks/refined_oil_still" + "particle": "basepower:blocks/refined_oil_still" } } diff --git a/src/main/resources/assets/poweradvantage/models/block/steel_frame.json b/src/main/resources/assets/basepower/models/block/steel_frame.json similarity index 94% rename from src/main/resources/assets/poweradvantage/models/block/steel_frame.json rename to src/main/resources/assets/basepower/models/block/steel_frame.json index 6e5ef049..a423e34b 100644 --- a/src/main/resources/assets/poweradvantage/models/block/steel_frame.json +++ b/src/main/resources/assets/basepower/models/block/steel_frame.json @@ -1,8 +1,8 @@ { "parent": "block/block", "textures": { - "face": "poweradvantage:blocks/steel_frame", - "particle": "poweradvantage:materials/steel" + "face": "basepower:blocks/steel_frame", + "particle": "basepower:materials/steel" }, "elements": [ { diff --git a/src/main/resources/assets/poweradvantage/models/block/still_off.json b/src/main/resources/assets/basepower/models/block/still_off.json similarity index 96% rename from src/main/resources/assets/poweradvantage/models/block/still_off.json rename to src/main/resources/assets/basepower/models/block/still_off.json index 69edeb89..6b62bcb8 100644 --- a/src/main/resources/assets/poweradvantage/models/block/still_off.json +++ b/src/main/resources/assets/basepower/models/block/still_off.json @@ -4,8 +4,8 @@ "top": "minecraft:block/furnace_top", "front": "minecraft:block/furnace_front", "side": "minecraft:block/furnace_side", - "tank": "poweradvantage:materials/steel", - "particle": "poweradvantage:materials/steel" + "tank": "basepower:materials/steel", + "particle": "basepower:materials/steel" }, "elements": [ { diff --git a/src/main/resources/assets/poweradvantage/models/block/still_on.json b/src/main/resources/assets/basepower/models/block/still_on.json similarity index 96% rename from src/main/resources/assets/poweradvantage/models/block/still_on.json rename to src/main/resources/assets/basepower/models/block/still_on.json index ac5eb132..0296244d 100644 --- a/src/main/resources/assets/poweradvantage/models/block/still_on.json +++ b/src/main/resources/assets/basepower/models/block/still_on.json @@ -4,8 +4,8 @@ "top": "minecraft:block/furnace_top", "front": "minecraft:block/furnace_front_on", "side": "minecraft:block/furnace_side", - "tank": "poweradvantage:materials/steel", - "particle": "poweradvantage:materials/steel" + "tank": "basepower:materials/steel", + "particle": "basepower:materials/steel" }, "elements": [ { diff --git a/src/main/resources/assets/basepower/models/item/bioplastic_ingot.json b/src/main/resources/assets/basepower/models/item/bioplastic_ingot.json new file mode 100644 index 00000000..96b8cfd8 --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/bioplastic_ingot.json @@ -0,0 +1,6 @@ +{ + "parent": "item/generated", + "textures": { + "layer0": "basepower:items/bioplastic_ingot" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/bucket_crude_oil.json b/src/main/resources/assets/basepower/models/item/bucket_crude_oil.json new file mode 100644 index 00000000..bc0182ff --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/bucket_crude_oil.json @@ -0,0 +1,6 @@ +{ + "parent": "item/generated", + "textures": { + "layer0": "basepower:items/bucket_crude_oil" + } +} diff --git a/src/main/resources/assets/basepower/models/item/bucket_refined_oil.json b/src/main/resources/assets/basepower/models/item/bucket_refined_oil.json new file mode 100644 index 00000000..145160a4 --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/bucket_refined_oil.json @@ -0,0 +1,6 @@ +{ + "parent": "item/generated", + "textures": { + "layer0": "basepower:items/bucket_refined_oil" + } +} diff --git a/src/main/resources/assets/basepower/models/item/converter_rf_electricity.json b/src/main/resources/assets/basepower/models/item/converter_rf_electricity.json new file mode 100644 index 00000000..8016dd44 --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/converter_rf_electricity.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/converter_rf_electricity" +} diff --git a/src/main/resources/assets/basepower/models/item/converter_rf_quantum.json b/src/main/resources/assets/basepower/models/item/converter_rf_quantum.json new file mode 100644 index 00000000..20bffbeb --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/converter_rf_quantum.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/converter_rf_quantum" +} diff --git a/src/main/resources/assets/basepower/models/item/converter_rf_steam.json b/src/main/resources/assets/basepower/models/item/converter_rf_steam.json new file mode 100644 index 00000000..77e6147c --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/converter_rf_steam.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/converter_rf_steam" +} diff --git a/src/main/resources/assets/basepower/models/item/converter_tr_electricity.json b/src/main/resources/assets/basepower/models/item/converter_tr_electricity.json new file mode 100644 index 00000000..8c54bb8f --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/converter_tr_electricity.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/converter_tr_electricity" +} diff --git a/src/main/resources/assets/basepower/models/item/crude_oil.json b/src/main/resources/assets/basepower/models/item/crude_oil.json new file mode 100644 index 00000000..38fee3aa --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/crude_oil.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:item/crude_oil_bucket" +} diff --git a/src/main/resources/assets/poweradvantage/models/item/crude_oil_bucket.json b/src/main/resources/assets/basepower/models/item/crude_oil_bucket.json similarity index 54% rename from src/main/resources/assets/poweradvantage/models/item/crude_oil_bucket.json rename to src/main/resources/assets/basepower/models/item/crude_oil_bucket.json index 9e70d56f..e227a3d5 100644 --- a/src/main/resources/assets/poweradvantage/models/item/crude_oil_bucket.json +++ b/src/main/resources/assets/basepower/models/item/crude_oil_bucket.json @@ -1,6 +1,6 @@ { "parent": "minecraft:item/generated", "textures": { - "layer0": "poweradvantage:items/bucket_crude_oil" + "layer0": "basepower:items/bucket_crude_oil" } } diff --git a/src/main/resources/assets/basepower/models/item/fluid_discharge.json b/src/main/resources/assets/basepower/models/item/fluid_discharge.json new file mode 100644 index 00000000..d9af1467 --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/fluid_discharge.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/fluid_discharge" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/fluid_drain.json b/src/main/resources/assets/basepower/models/item/fluid_drain.json new file mode 100644 index 00000000..60de5a6c --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/fluid_drain.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/fluid_drain" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/fluid_metal_tank.json b/src/main/resources/assets/basepower/models/item/fluid_metal_tank.json new file mode 100644 index 00000000..cc8c6058 --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/fluid_metal_tank.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/fluid_metal_tank" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/fluid_pipe.json b/src/main/resources/assets/basepower/models/item/fluid_pipe.json new file mode 100644 index 00000000..2cfca627 --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/fluid_pipe.json @@ -0,0 +1,4 @@ +{ + "parent": "basepower:block/fluid_pipe_ns", + "ambientocclusion": false +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/fluid_pipe_terminal.json b/src/main/resources/assets/basepower/models/item/fluid_pipe_terminal.json new file mode 100644 index 00000000..2cfca627 --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/fluid_pipe_terminal.json @@ -0,0 +1,4 @@ +{ + "parent": "basepower:block/fluid_pipe_ns", + "ambientocclusion": false +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/fluid_storage_tank.json b/src/main/resources/assets/basepower/models/item/fluid_storage_tank.json new file mode 100644 index 00000000..33831e30 --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/fluid_storage_tank.json @@ -0,0 +1,4 @@ +{ + "parent": "basepower:block/fluid_storage_tank", + "ambientocclusion": false +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/fluid_switch.json b/src/main/resources/assets/basepower/models/item/fluid_switch.json new file mode 100644 index 00000000..e62bd15b --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/fluid_switch.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/fluid_switch_on" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/infinite_electricity.json b/src/main/resources/assets/basepower/models/item/infinite_electricity.json new file mode 100644 index 00000000..a1abf245 --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/infinite_electricity.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/infinite_electricity" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/infinite_quantum.json b/src/main/resources/assets/basepower/models/item/infinite_quantum.json new file mode 100644 index 00000000..53b56ddd --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/infinite_quantum.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/infinite_quantum" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/infinite_steam.json b/src/main/resources/assets/basepower/models/item/infinite_steam.json new file mode 100644 index 00000000..e8f9061e --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/infinite_steam.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/infinite_steam" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/item_conveyor.json b/src/main/resources/assets/basepower/models/item/item_conveyor.json new file mode 100644 index 00000000..f042d83c --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/item_conveyor.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/item_conveyor" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/item_filter_block.json b/src/main/resources/assets/basepower/models/item/item_filter_block.json new file mode 100644 index 00000000..e820da77 --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/item_filter_block.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/item_filter_block" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/item_filter_food.json b/src/main/resources/assets/basepower/models/item/item_filter_food.json new file mode 100644 index 00000000..96db616c --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/item_filter_food.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/item_filter_food" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/item_filter_fuel.json b/src/main/resources/assets/basepower/models/item/item_filter_fuel.json new file mode 100644 index 00000000..1399fb3c --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/item_filter_fuel.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/item_filter_fuel" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/item_filter_inventory.json b/src/main/resources/assets/basepower/models/item/item_filter_inventory.json new file mode 100644 index 00000000..03a135df --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/item_filter_inventory.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/item_filter_inventory" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/item_filter_ore.json b/src/main/resources/assets/basepower/models/item/item_filter_ore.json new file mode 100644 index 00000000..7a44c344 --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/item_filter_ore.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/item_filter_ore" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/item_filter_overflow.json b/src/main/resources/assets/basepower/models/item/item_filter_overflow.json new file mode 100644 index 00000000..caac6427 --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/item_filter_overflow.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/item_filter_overflow" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/item_filter_plant.json b/src/main/resources/assets/basepower/models/item/item_filter_plant.json new file mode 100644 index 00000000..40beb7ab --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/item_filter_plant.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/item_filter_plant" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/item_filter_smelt.json b/src/main/resources/assets/basepower/models/item/item_filter_smelt.json new file mode 100644 index 00000000..5386d841 --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/item_filter_smelt.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/item_filter_smelt" +} \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/refined_oil.json b/src/main/resources/assets/basepower/models/item/refined_oil.json new file mode 100644 index 00000000..a79e57e9 --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/refined_oil.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:item/refined_oil_bucket" +} diff --git a/src/main/resources/assets/poweradvantage/models/item/refined_oil_bucket.json b/src/main/resources/assets/basepower/models/item/refined_oil_bucket.json similarity index 53% rename from src/main/resources/assets/poweradvantage/models/item/refined_oil_bucket.json rename to src/main/resources/assets/basepower/models/item/refined_oil_bucket.json index 48c1456a..1b69ba5d 100644 --- a/src/main/resources/assets/poweradvantage/models/item/refined_oil_bucket.json +++ b/src/main/resources/assets/basepower/models/item/refined_oil_bucket.json @@ -1,6 +1,6 @@ { "parent": "minecraft:item/generated", "textures": { - "layer0": "poweradvantage:items/bucket_refined_oil" + "layer0": "basepower:items/bucket_refined_oil" } } diff --git a/src/main/resources/assets/poweradvantage/models/item/rotator_tool.json b/src/main/resources/assets/basepower/models/item/rotator_tool.json similarity index 50% rename from src/main/resources/assets/poweradvantage/models/item/rotator_tool.json rename to src/main/resources/assets/basepower/models/item/rotator_tool.json index 498abaf3..e096fa4c 100644 --- a/src/main/resources/assets/poweradvantage/models/item/rotator_tool.json +++ b/src/main/resources/assets/basepower/models/item/rotator_tool.json @@ -1,6 +1,6 @@ { "parent": "item/handheld", "textures": { - "layer0": "poweradvantage:items/rotator_tool" + "layer0": "basepower:items/rotator_tool" } } \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/sprocket.json b/src/main/resources/assets/basepower/models/item/sprocket.json similarity index 53% rename from src/main/resources/assets/poweradvantage/models/item/sprocket.json rename to src/main/resources/assets/basepower/models/item/sprocket.json index f26d4642..8555e9f9 100644 --- a/src/main/resources/assets/poweradvantage/models/item/sprocket.json +++ b/src/main/resources/assets/basepower/models/item/sprocket.json @@ -1,6 +1,6 @@ { "parent": "item/generated", "textures": { - "layer0": "poweradvantage:items/sprocket" + "layer0": "basepower:items/sprocket" } } \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/starch.json b/src/main/resources/assets/basepower/models/item/starch.json similarity index 54% rename from src/main/resources/assets/poweradvantage/models/item/starch.json rename to src/main/resources/assets/basepower/models/item/starch.json index ac3db87c..5e9dbcfe 100644 --- a/src/main/resources/assets/poweradvantage/models/item/starch.json +++ b/src/main/resources/assets/basepower/models/item/starch.json @@ -1,6 +1,6 @@ { "parent": "item/generated", "textures": { - "layer0": "poweradvantage:items/starch" + "layer0": "basepower:items/starch" } } \ No newline at end of file diff --git a/src/main/resources/assets/basepower/models/item/steel_frame.json b/src/main/resources/assets/basepower/models/item/steel_frame.json new file mode 100644 index 00000000..78e655ea --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/steel_frame.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/steel_frame" +} diff --git a/src/main/resources/assets/basepower/models/item/still.json b/src/main/resources/assets/basepower/models/item/still.json new file mode 100644 index 00000000..74f0491a --- /dev/null +++ b/src/main/resources/assets/basepower/models/item/still.json @@ -0,0 +1,3 @@ +{ + "parent": "basepower:block/still_off" +} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/crude_oil_flow.png b/src/main/resources/assets/basepower/textures/blocks/crude_oil_flow.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/crude_oil_flow.png rename to src/main/resources/assets/basepower/textures/blocks/crude_oil_flow.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/crude_oil_flow.png.mcmeta b/src/main/resources/assets/basepower/textures/blocks/crude_oil_flow.png.mcmeta similarity index 91% rename from src/main/resources/assets/poweradvantage/textures/blocks/crude_oil_flow.png.mcmeta rename to src/main/resources/assets/basepower/textures/blocks/crude_oil_flow.png.mcmeta index de3267f0..4f0718ac 100644 --- a/src/main/resources/assets/poweradvantage/textures/blocks/crude_oil_flow.png.mcmeta +++ b/src/main/resources/assets/basepower/textures/blocks/crude_oil_flow.png.mcmeta @@ -1,3 +1,3 @@ -{ - "animation": {} +{ + "animation": {} } \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/crude_oil_still.png b/src/main/resources/assets/basepower/textures/blocks/crude_oil_still.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/crude_oil_still.png rename to src/main/resources/assets/basepower/textures/blocks/crude_oil_still.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/crude_oil_still.png.mcmeta b/src/main/resources/assets/basepower/textures/blocks/crude_oil_still.png.mcmeta similarity index 89% rename from src/main/resources/assets/poweradvantage/textures/blocks/crude_oil_still.png.mcmeta rename to src/main/resources/assets/basepower/textures/blocks/crude_oil_still.png.mcmeta index 4b721e09..0645f48c 100644 --- a/src/main/resources/assets/poweradvantage/textures/blocks/crude_oil_still.png.mcmeta +++ b/src/main/resources/assets/basepower/textures/blocks/crude_oil_still.png.mcmeta @@ -1,5 +1,5 @@ -{ - "animation": { - "frametime": 2 - } -} +{ + "animation": { + "frametime": 2 + } +} diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/filter_bottom.png b/src/main/resources/assets/basepower/textures/blocks/filter_bottom.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/filter_bottom.png rename to src/main/resources/assets/basepower/textures/blocks/filter_bottom.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/filter_side_block.png b/src/main/resources/assets/basepower/textures/blocks/filter_side_block.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/filter_side_block.png rename to src/main/resources/assets/basepower/textures/blocks/filter_side_block.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/filter_side_food.png b/src/main/resources/assets/basepower/textures/blocks/filter_side_food.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/filter_side_food.png rename to src/main/resources/assets/basepower/textures/blocks/filter_side_food.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/filter_side_fuel.png b/src/main/resources/assets/basepower/textures/blocks/filter_side_fuel.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/filter_side_fuel.png rename to src/main/resources/assets/basepower/textures/blocks/filter_side_fuel.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/filter_side_inventory.png b/src/main/resources/assets/basepower/textures/blocks/filter_side_inventory.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/filter_side_inventory.png rename to src/main/resources/assets/basepower/textures/blocks/filter_side_inventory.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/filter_side_ore.png b/src/main/resources/assets/basepower/textures/blocks/filter_side_ore.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/filter_side_ore.png rename to src/main/resources/assets/basepower/textures/blocks/filter_side_ore.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/filter_side_overflow.png b/src/main/resources/assets/basepower/textures/blocks/filter_side_overflow.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/filter_side_overflow.png rename to src/main/resources/assets/basepower/textures/blocks/filter_side_overflow.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/filter_side_plantable.png b/src/main/resources/assets/basepower/textures/blocks/filter_side_plantable.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/filter_side_plantable.png rename to src/main/resources/assets/basepower/textures/blocks/filter_side_plantable.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/filter_side_smeltable.png b/src/main/resources/assets/basepower/textures/blocks/filter_side_smeltable.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/filter_side_smeltable.png rename to src/main/resources/assets/basepower/textures/blocks/filter_side_smeltable.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/fluid_discharge_face.png b/src/main/resources/assets/basepower/textures/blocks/fluid_discharge_face.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/fluid_discharge_face.png rename to src/main/resources/assets/basepower/textures/blocks/fluid_discharge_face.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/fluid_discharge_side.png b/src/main/resources/assets/basepower/textures/blocks/fluid_discharge_side.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/fluid_discharge_side.png rename to src/main/resources/assets/basepower/textures/blocks/fluid_discharge_side.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/fluid_drain_face.png b/src/main/resources/assets/basepower/textures/blocks/fluid_drain_face.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/fluid_drain_face.png rename to src/main/resources/assets/basepower/textures/blocks/fluid_drain_face.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/fluid_drain_side.png b/src/main/resources/assets/basepower/textures/blocks/fluid_drain_side.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/fluid_drain_side.png rename to src/main/resources/assets/basepower/textures/blocks/fluid_drain_side.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/fluid_metal_tank_side.png b/src/main/resources/assets/basepower/textures/blocks/fluid_metal_tank_side.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/fluid_metal_tank_side.png rename to src/main/resources/assets/basepower/textures/blocks/fluid_metal_tank_side.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/fluid_metal_tank_top.png b/src/main/resources/assets/basepower/textures/blocks/fluid_metal_tank_top.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/fluid_metal_tank_top.png rename to src/main/resources/assets/basepower/textures/blocks/fluid_metal_tank_top.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/fluid_pipe.png b/src/main/resources/assets/basepower/textures/blocks/fluid_pipe.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/fluid_pipe.png rename to src/main/resources/assets/basepower/textures/blocks/fluid_pipe.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/fluid_switch_off.png b/src/main/resources/assets/basepower/textures/blocks/fluid_switch_off.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/fluid_switch_off.png rename to src/main/resources/assets/basepower/textures/blocks/fluid_switch_off.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/fluid_switch_on.png b/src/main/resources/assets/basepower/textures/blocks/fluid_switch_on.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/fluid_switch_on.png rename to src/main/resources/assets/basepower/textures/blocks/fluid_switch_on.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/item_conveyor_end.png b/src/main/resources/assets/basepower/textures/blocks/item_conveyor_end.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/item_conveyor_end.png rename to src/main/resources/assets/basepower/textures/blocks/item_conveyor_end.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/item_conveyor_side.png b/src/main/resources/assets/basepower/textures/blocks/item_conveyor_side.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/item_conveyor_side.png rename to src/main/resources/assets/basepower/textures/blocks/item_conveyor_side.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/item_conveyor_top.png b/src/main/resources/assets/basepower/textures/blocks/item_conveyor_top.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/item_conveyor_top.png rename to src/main/resources/assets/basepower/textures/blocks/item_conveyor_top.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/metal_panel.png b/src/main/resources/assets/basepower/textures/blocks/metal_panel.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/metal_panel.png rename to src/main/resources/assets/basepower/textures/blocks/metal_panel.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/powercable.png b/src/main/resources/assets/basepower/textures/blocks/powercable.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/powercable.png rename to src/main/resources/assets/basepower/textures/blocks/powercable.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/refined_oil_flow.png b/src/main/resources/assets/basepower/textures/blocks/refined_oil_flow.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/refined_oil_flow.png rename to src/main/resources/assets/basepower/textures/blocks/refined_oil_flow.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/refined_oil_flow.png.mcmeta b/src/main/resources/assets/basepower/textures/blocks/refined_oil_flow.png.mcmeta similarity index 91% rename from src/main/resources/assets/poweradvantage/textures/blocks/refined_oil_flow.png.mcmeta rename to src/main/resources/assets/basepower/textures/blocks/refined_oil_flow.png.mcmeta index de3267f0..4f0718ac 100644 --- a/src/main/resources/assets/poweradvantage/textures/blocks/refined_oil_flow.png.mcmeta +++ b/src/main/resources/assets/basepower/textures/blocks/refined_oil_flow.png.mcmeta @@ -1,3 +1,3 @@ -{ - "animation": {} +{ + "animation": {} } \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/refined_oil_still.png b/src/main/resources/assets/basepower/textures/blocks/refined_oil_still.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/refined_oil_still.png rename to src/main/resources/assets/basepower/textures/blocks/refined_oil_still.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/refined_oil_still.png.mcmeta b/src/main/resources/assets/basepower/textures/blocks/refined_oil_still.png.mcmeta similarity index 89% rename from src/main/resources/assets/poweradvantage/textures/blocks/refined_oil_still.png.mcmeta rename to src/main/resources/assets/basepower/textures/blocks/refined_oil_still.png.mcmeta index 4b721e09..0645f48c 100644 --- a/src/main/resources/assets/poweradvantage/textures/blocks/refined_oil_still.png.mcmeta +++ b/src/main/resources/assets/basepower/textures/blocks/refined_oil_still.png.mcmeta @@ -1,5 +1,5 @@ -{ - "animation": { - "frametime": 2 - } -} +{ + "animation": { + "frametime": 2 + } +} diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/steel_frame.png b/src/main/resources/assets/basepower/textures/blocks/steel_frame.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/steel_frame.png rename to src/main/resources/assets/basepower/textures/blocks/steel_frame.png diff --git a/src/main/resources/assets/poweradvantage/textures/blocks/techreborn_converter.png b/src/main/resources/assets/basepower/textures/blocks/techreborn_converter.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/blocks/techreborn_converter.png rename to src/main/resources/assets/basepower/textures/blocks/techreborn_converter.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/fluid_discharge_gui.png b/src/main/resources/assets/basepower/textures/gui/container/fluid_discharge_gui.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/fluid_discharge_gui.png rename to src/main/resources/assets/basepower/textures/gui/container/fluid_discharge_gui.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/fluid_drain_gui.png b/src/main/resources/assets/basepower/textures/gui/container/fluid_drain_gui.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/fluid_drain_gui.png rename to src/main/resources/assets/basepower/textures/gui/container/fluid_drain_gui.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/fluid_metal_tank_gui.png b/src/main/resources/assets/basepower/textures/gui/container/fluid_metal_tank_gui.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/fluid_metal_tank_gui.png rename to src/main/resources/assets/basepower/textures/gui/container/fluid_metal_tank_gui.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/fluid_storage_tank_gui.png b/src/main/resources/assets/basepower/textures/gui/container/fluid_storage_tank_gui.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/fluid_storage_tank_gui.png rename to src/main/resources/assets/basepower/textures/gui/container/fluid_storage_tank_gui.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor.png b/src/main/resources/assets/basepower/textures/gui/container/item_conveyor.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor.png rename to src/main/resources/assets/basepower/textures/gui/container/item_conveyor.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_block.png b/src/main/resources/assets/basepower/textures/gui/container/item_conveyor_block.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_block.png rename to src/main/resources/assets/basepower/textures/gui/container/item_conveyor_block.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_food.png b/src/main/resources/assets/basepower/textures/gui/container/item_conveyor_food.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_food.png rename to src/main/resources/assets/basepower/textures/gui/container/item_conveyor_food.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_fuel.png b/src/main/resources/assets/basepower/textures/gui/container/item_conveyor_fuel.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_fuel.png rename to src/main/resources/assets/basepower/textures/gui/container/item_conveyor_fuel.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_inventory.png b/src/main/resources/assets/basepower/textures/gui/container/item_conveyor_inventory.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_inventory.png rename to src/main/resources/assets/basepower/textures/gui/container/item_conveyor_inventory.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_ore.png b/src/main/resources/assets/basepower/textures/gui/container/item_conveyor_ore.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_ore.png rename to src/main/resources/assets/basepower/textures/gui/container/item_conveyor_ore.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_overflow.png b/src/main/resources/assets/basepower/textures/gui/container/item_conveyor_overflow.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_overflow.png rename to src/main/resources/assets/basepower/textures/gui/container/item_conveyor_overflow.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_plant.png b/src/main/resources/assets/basepower/textures/gui/container/item_conveyor_plant.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_plant.png rename to src/main/resources/assets/basepower/textures/gui/container/item_conveyor_plant.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_smelt.png b/src/main/resources/assets/basepower/textures/gui/container/item_conveyor_smelt.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/item_conveyor_smelt.png rename to src/main/resources/assets/basepower/textures/gui/container/item_conveyor_smelt.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/machineGUI_template.png b/src/main/resources/assets/basepower/textures/gui/container/machineGUI_template.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/machineGUI_template.png rename to src/main/resources/assets/basepower/textures/gui/container/machineGUI_template.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/player_inventory.png b/src/main/resources/assets/basepower/textures/gui/container/player_inventory.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/player_inventory.png rename to src/main/resources/assets/basepower/textures/gui/container/player_inventory.png diff --git a/src/main/resources/assets/poweradvantage/textures/gui/container/still_gui.png b/src/main/resources/assets/basepower/textures/gui/container/still_gui.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/gui/container/still_gui.png rename to src/main/resources/assets/basepower/textures/gui/container/still_gui.png diff --git a/src/main/resources/assets/poweradvantage/textures/items/bioplastic_ingot.png b/src/main/resources/assets/basepower/textures/items/bioplastic_ingot.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/items/bioplastic_ingot.png rename to src/main/resources/assets/basepower/textures/items/bioplastic_ingot.png diff --git a/src/main/resources/assets/poweradvantage/textures/items/bucket_crude_oil.png b/src/main/resources/assets/basepower/textures/items/bucket_crude_oil.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/items/bucket_crude_oil.png rename to src/main/resources/assets/basepower/textures/items/bucket_crude_oil.png diff --git a/src/main/resources/assets/poweradvantage/textures/items/bucket_refined_oil.png b/src/main/resources/assets/basepower/textures/items/bucket_refined_oil.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/items/bucket_refined_oil.png rename to src/main/resources/assets/basepower/textures/items/bucket_refined_oil.png diff --git a/src/main/resources/assets/poweradvantage/textures/items/rotator_tool.png b/src/main/resources/assets/basepower/textures/items/rotator_tool.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/items/rotator_tool.png rename to src/main/resources/assets/basepower/textures/items/rotator_tool.png diff --git a/src/main/resources/assets/poweradvantage/textures/items/sprocket.png b/src/main/resources/assets/basepower/textures/items/sprocket.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/items/sprocket.png rename to src/main/resources/assets/basepower/textures/items/sprocket.png diff --git a/src/main/resources/assets/poweradvantage/textures/items/starch.png b/src/main/resources/assets/basepower/textures/items/starch.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/items/starch.png rename to src/main/resources/assets/basepower/textures/items/starch.png diff --git a/src/main/resources/assets/poweradvantage/textures/materials/brass.png b/src/main/resources/assets/basepower/textures/materials/brass.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/materials/brass.png rename to src/main/resources/assets/basepower/textures/materials/brass.png diff --git a/src/main/resources/assets/poweradvantage/textures/materials/copper.png b/src/main/resources/assets/basepower/textures/materials/copper.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/materials/copper.png rename to src/main/resources/assets/basepower/textures/materials/copper.png diff --git a/src/main/resources/assets/poweradvantage/textures/materials/invar.png b/src/main/resources/assets/basepower/textures/materials/invar.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/materials/invar.png rename to src/main/resources/assets/basepower/textures/materials/invar.png diff --git a/src/main/resources/assets/poweradvantage/textures/materials/iron.png b/src/main/resources/assets/basepower/textures/materials/iron.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/materials/iron.png rename to src/main/resources/assets/basepower/textures/materials/iron.png diff --git a/src/main/resources/assets/poweradvantage/textures/materials/metal_panel.png b/src/main/resources/assets/basepower/textures/materials/metal_panel.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/materials/metal_panel.png rename to src/main/resources/assets/basepower/textures/materials/metal_panel.png diff --git a/src/main/resources/assets/poweradvantage/textures/materials/plastic.png b/src/main/resources/assets/basepower/textures/materials/plastic.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/materials/plastic.png rename to src/main/resources/assets/basepower/textures/materials/plastic.png diff --git a/src/main/resources/assets/poweradvantage/textures/materials/steel.png b/src/main/resources/assets/basepower/textures/materials/steel.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/materials/steel.png rename to src/main/resources/assets/basepower/textures/materials/steel.png diff --git a/src/main/resources/assets/poweradvantage/textures/materials/steel_dark.png b/src/main/resources/assets/basepower/textures/materials/steel_dark.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/materials/steel_dark.png rename to src/main/resources/assets/basepower/textures/materials/steel_dark.png diff --git a/src/main/resources/assets/poweradvantage/textures/materials/stone.png b/src/main/resources/assets/basepower/textures/materials/stone.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/materials/stone.png rename to src/main/resources/assets/basepower/textures/materials/stone.png diff --git a/src/main/resources/assets/poweradvantage/textures/materials/stone_brick.png b/src/main/resources/assets/basepower/textures/materials/stone_brick.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/materials/stone_brick.png rename to src/main/resources/assets/basepower/textures/materials/stone_brick.png diff --git a/src/main/resources/assets/poweradvantage/textures/materials/wood.png b/src/main/resources/assets/basepower/textures/materials/wood.png similarity index 100% rename from src/main/resources/assets/poweradvantage/textures/materials/wood.png rename to src/main/resources/assets/basepower/textures/materials/wood.png diff --git a/src/main/resources/assets/poweradvantage/blockstates/converter_rf_electricity.json b/src/main/resources/assets/poweradvantage/blockstates/converter_rf_electricity.json deleted file mode 100644 index 35df5016..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/converter_rf_electricity.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=east": { - "model": "poweradvantage:block/converter_rf_electricity", - "y": 90 - }, - "facing=north": { - "model": "poweradvantage:block/converter_rf_electricity" - }, - "facing=south": { - "model": "poweradvantage:block/converter_rf_electricity", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/converter_rf_electricity", - "y": 270 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/converter_rf_quantum.json b/src/main/resources/assets/poweradvantage/blockstates/converter_rf_quantum.json deleted file mode 100644 index 257c03cf..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/converter_rf_quantum.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=east": { - "model": "poweradvantage:block/converter_rf_quantum", - "y": 90 - }, - "facing=north": { - "model": "poweradvantage:block/converter_rf_quantum" - }, - "facing=south": { - "model": "poweradvantage:block/converter_rf_quantum", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/converter_rf_quantum", - "y": 270 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/converter_rf_steam.json b/src/main/resources/assets/poweradvantage/blockstates/converter_rf_steam.json deleted file mode 100644 index d868d075..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/converter_rf_steam.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=east": { - "model": "poweradvantage:block/converter_rf_steam", - "y": 90 - }, - "facing=north": { - "model": "poweradvantage:block/converter_rf_steam" - }, - "facing=south": { - "model": "poweradvantage:block/converter_rf_steam", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/converter_rf_steam", - "y": 270 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/converter_tr_electricity.json b/src/main/resources/assets/poweradvantage/blockstates/converter_tr_electricity.json deleted file mode 100644 index d7730168..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/converter_tr_electricity.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=east": { - "model": "poweradvantage:block/converter_tr_electricity", - "y": 90 - }, - "facing=north": { - "model": "poweradvantage:block/converter_tr_electricity" - }, - "facing=south": { - "model": "poweradvantage:block/converter_tr_electricity", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/converter_tr_electricity", - "y": 270 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/crude_oil.json b/src/main/resources/assets/poweradvantage/blockstates/crude_oil.json deleted file mode 100644 index 9ff671aa..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/crude_oil.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "variants": { - "": { "model": "poweradvantage:block/crude_oil" } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/fluid_discharge.json b/src/main/resources/assets/poweradvantage/blockstates/fluid_discharge.json deleted file mode 100644 index 1266604e..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/fluid_discharge.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=east": { - "model": "poweradvantage:block/fluid_discharge", - "y": 90 - }, - "facing=north": { - "model": "poweradvantage:block/fluid_discharge" - }, - "facing=south": { - "model": "poweradvantage:block/fluid_discharge", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/fluid_discharge", - "y": 270 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/fluid_drain.json b/src/main/resources/assets/poweradvantage/blockstates/fluid_drain.json deleted file mode 100644 index 70980092..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/fluid_drain.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=east": { - "model": "poweradvantage:block/fluid_drain", - "y": 90 - }, - "facing=north": { - "model": "poweradvantage:block/fluid_drain" - }, - "facing=south": { - "model": "poweradvantage:block/fluid_drain", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/fluid_drain", - "y": 270 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/fluid_storage_tank.json b/src/main/resources/assets/poweradvantage/blockstates/fluid_storage_tank.json deleted file mode 100644 index c835f911..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/fluid_storage_tank.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=east": { - "model": "poweradvantage:block/fluid_storage_tank", - "y": 90 - }, - "facing=north": { - "model": "poweradvantage:block/fluid_storage_tank" - }, - "facing=south": { - "model": "poweradvantage:block/fluid_storage_tank", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/fluid_storage_tank", - "y": 270 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/fluid_switch.json b/src/main/resources/assets/poweradvantage/blockstates/fluid_switch.json deleted file mode 100644 index 5817285b..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/fluid_switch.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "variants": { - "active=false": { - "model": "poweradvantage:block/fluid_switch_off" - }, - "active=true": { - "model": "poweradvantage:block/fluid_switch_on" - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/infinite_electricity.json b/src/main/resources/assets/poweradvantage/blockstates/infinite_electricity.json deleted file mode 100644 index e06b9490..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/infinite_electricity.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=east": { - "model": "poweradvantage:block/infinite_electricity", - "y": 90 - }, - "facing=north": { - "model": "poweradvantage:block/infinite_electricity" - }, - "facing=south": { - "model": "poweradvantage:block/infinite_electricity", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/infinite_electricity", - "y": 270 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/infinite_quantum.json b/src/main/resources/assets/poweradvantage/blockstates/infinite_quantum.json deleted file mode 100644 index 1e7ad502..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/infinite_quantum.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=east": { - "model": "poweradvantage:block/infinite_quantum", - "y": 90 - }, - "facing=north": { - "model": "poweradvantage:block/infinite_quantum" - }, - "facing=south": { - "model": "poweradvantage:block/infinite_quantum", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/infinite_quantum", - "y": 270 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/infinite_steam.json b/src/main/resources/assets/poweradvantage/blockstates/infinite_steam.json deleted file mode 100644 index 3ad324b6..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/infinite_steam.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=east": { - "model": "poweradvantage:block/infinite_steam", - "y": 90 - }, - "facing=north": { - "model": "poweradvantage:block/infinite_steam" - }, - "facing=south": { - "model": "poweradvantage:block/infinite_steam", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/infinite_steam", - "y": 270 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_conveyor.json b/src/main/resources/assets/poweradvantage/blockstates/item_conveyor.json deleted file mode 100644 index 93c7ac9e..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/item_conveyor.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "variants": { - "facing=down": { - "model": "poweradvantage:block/item_conveyor", - "x": 90 - }, - "facing=up": { - "model": "poweradvantage:block/item_conveyor", - "x": 270 - }, - "facing=north": { - "model": "poweradvantage:block/item_conveyor" - }, - "facing=south": { - "model": "poweradvantage:block/item_conveyor", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/item_conveyor", - "y": 270 - }, - "facing=east": { - "model": "poweradvantage:block/item_conveyor", - "y": 90 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_block.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_block.json deleted file mode 100644 index 39140fb1..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_block.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=north": { - "model": "poweradvantage:block/item_filter_block" - }, - "facing=south": { - "model": "poweradvantage:block/item_filter_block", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/item_filter_block", - "y": 270 - }, - "facing=east": { - "model": "poweradvantage:block/item_filter_block", - "y": 90 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_food.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_food.json deleted file mode 100644 index 6cc97515..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_food.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=north": { - "model": "poweradvantage:block/item_filter_food" - }, - "facing=south": { - "model": "poweradvantage:block/item_filter_food", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/item_filter_food", - "y": 270 - }, - "facing=east": { - "model": "poweradvantage:block/item_filter_food", - "y": 90 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_fuel.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_fuel.json deleted file mode 100644 index 9bbcc350..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_fuel.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=north": { - "model": "poweradvantage:block/item_filter_fuel" - }, - "facing=south": { - "model": "poweradvantage:block/item_filter_fuel", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/item_filter_fuel", - "y": 270 - }, - "facing=east": { - "model": "poweradvantage:block/item_filter_fuel", - "y": 90 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_inventory.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_inventory.json deleted file mode 100644 index ce93778d..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_inventory.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=north": { - "model": "poweradvantage:block/item_filter_inventory" - }, - "facing=south": { - "model": "poweradvantage:block/item_filter_inventory", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/item_filter_inventory", - "y": 270 - }, - "facing=east": { - "model": "poweradvantage:block/item_filter_inventory", - "y": 90 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_ore.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_ore.json deleted file mode 100644 index 402f28c9..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_ore.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=north": { - "model": "poweradvantage:block/item_filter_ore" - }, - "facing=south": { - "model": "poweradvantage:block/item_filter_ore", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/item_filter_ore", - "y": 270 - }, - "facing=east": { - "model": "poweradvantage:block/item_filter_ore", - "y": 90 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_overflow.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_overflow.json deleted file mode 100644 index 83a02f1c..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_overflow.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=north": { - "model": "poweradvantage:block/item_filter_overflow" - }, - "facing=south": { - "model": "poweradvantage:block/item_filter_overflow", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/item_filter_overflow", - "y": 270 - }, - "facing=east": { - "model": "poweradvantage:block/item_filter_overflow", - "y": 90 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_plant.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_plant.json deleted file mode 100644 index 0527268e..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_plant.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=north": { - "model": "poweradvantage:block/item_filter_plant" - }, - "facing=south": { - "model": "poweradvantage:block/item_filter_plant", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/item_filter_plant", - "y": 270 - }, - "facing=east": { - "model": "poweradvantage:block/item_filter_plant", - "y": 90 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_smelt.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_smelt.json deleted file mode 100644 index eb7fb555..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_smelt.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "variants": { - "facing=north": { - "model": "poweradvantage:block/item_filter_smelt" - }, - "facing=south": { - "model": "poweradvantage:block/item_filter_smelt", - "y": 180 - }, - "facing=west": { - "model": "poweradvantage:block/item_filter_smelt", - "y": 270 - }, - "facing=east": { - "model": "poweradvantage:block/item_filter_smelt", - "y": 90 - } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/refined_oil.json b/src/main/resources/assets/poweradvantage/blockstates/refined_oil.json deleted file mode 100644 index 80e8102b..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/refined_oil.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "variants": { - "": { "model": "poweradvantage:block/refined_oil" } - } -} diff --git a/src/main/resources/assets/poweradvantage/blockstates/steel_frame.json b/src/main/resources/assets/poweradvantage/blockstates/steel_frame.json deleted file mode 100644 index 6adc29c6..00000000 --- a/src/main/resources/assets/poweradvantage/blockstates/steel_frame.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "variants": { - "": { - "model": "poweradvantage:block/steel_frame" - } - } -} diff --git a/src/main/resources/assets/poweradvantage/lang/de_de.json b/src/main/resources/assets/poweradvantage/lang/de_de.json deleted file mode 100644 index bdf24685..00000000 --- a/src/main/resources/assets/poweradvantage/lang/de_de.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", - "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Große Ofen (formlos)", - "nei.poweradvantage.recipehandler.shapedfurnace.name": "Große Ofen (geformt)", - "tooltip.poweradvantage.empty": "Leer", - "tooltip.poweradvantage.buckets_of": "%x Eimer %y", - "itemGroup.poweradvantage": "Leistungsvorteil", - "tile.poweradvantage.fluid_drain.name": "Ablauf", - "tile.poweradvantage.fluid_discharge.name": "Austragsrost", - "tile.poweradvantage.fluid_storage_tank.name": "Lagertank", - "item.poweradvantage.fluid_storage_tank_item.name": "Speicher", - "tile.poweradvantage.fluid_pipe.name": "Rohr", - "tile.poweradvantage.item_conveyor.name": "Förderer", - "tile.poweradvantage.item_filter_block.name": "Block Auswahl Förder", - "tile.poweradvantage.item_filter_food.name": "Lebensmittel Auswahl Förder", - "tile.poweradvantage.item_filter_fuel.name": "Brennstoff Auswahl Förder", - "tile.poweradvantage.item_filter_inventory.name": "Anpassbare Auswahl Förder", - "tile.poweradvantage.item_filter_ore.name": "Erz Auswahl Förder", - "tile.poweradvantage.item_filter_plant.name": "Pflanze Auswahl Förder", - "tile.poweradvantage.item_filter_smelt.name": "Kochbare Auswahl Förder", - "tile.poweradvantage.item_filter_overflow.name": "Überlauf Auswahl Förder", - "tile.poweradvantage.steel_frame.name": "Stahlrahmen", - "item.poweradvantage.starch.name": "Kartoffelstärke", - "item.poweradvantage.bioplastic_ingot.name": "Biokunststoff-Barren", - "item.poweradvantage.sprocket.name": "Kettenrad", - "tile.poweradvantage.fluid_metal_tank.name": "Metall-Lagertank", - "item.poweradvantage.rotator_tool.name": "Drehwerkzeug", - "tile.poweradvantage.infinite_steam.name": "Unendlichen Dampfkraft", - "tile.poweradvantage.infinite_electricity.name": "Unendliche Elektrische Leistung", - "tile.poweradvantage.infinite_quantum.name": "Unendliche Quanten Leistung", - "tile.poweradvantage.crude_oil.name": "Rohöl", - "fluid.poweradvantage.crude_oil": "Rohöl", - "item.bucket.crude_oil": "Eimer Rohöl", - "tile.poweradvantage.still.name": "Brennerei", - "tile.poweradvantage.refined_oil.name": "Raffiniertes Öl", - "fluid.poweradvantage.refined_oil": "Raffiniertes Öl", - "item.bucket.refined_oil": "Eimer für Raffiniertes Öl", - "tile.poweradvantage.fluid_switch.name": "Ventil", - "tile.poweradvantage.converter_rf_steam.name": "Dampf-RF Leistungstransformator", - "tile.poweradvantage.converter_rf_electricity.name": "Elektrizität-RF Leistungstransformator", - "tile.poweradvantage.converter_rf_quantum.name": "Quant-RF Leistungstransformator", - "tile.poweradvantage.converter_tr_electricity.name": "Elektrizität-EU Leistungstransformator", - "block.poweradvantage.fluid_drain": "Ablauf", - "block.poweradvantage.fluid_discharge": "Austragsrost", - "block.poweradvantage.fluid_storage_tank": "Lagertank", - "item.poweradvantage.fluid_storage_tank_item": "Speicher", - "block.poweradvantage.fluid_pipe": "Rohr", - "block.poweradvantage.item_conveyor": "Förderer", - "block.poweradvantage.item_filter_block": "Block Auswahl Förder", - "block.poweradvantage.item_filter_food": "Lebensmittel Auswahl Förder", - "block.poweradvantage.item_filter_fuel": "Brennstoff Auswahl Förder", - "block.poweradvantage.item_filter_inventory": "Anpassbare Auswahl Förder", - "block.poweradvantage.item_filter_ore": "Erz Auswahl Förder", - "block.poweradvantage.item_filter_plant": "Pflanze Auswahl Förder", - "block.poweradvantage.item_filter_smelt": "Kochbare Auswahl Förder", - "block.poweradvantage.item_filter_overflow": "Überlauf Auswahl Förder", - "block.poweradvantage.steel_frame": "Stahlrahmen", - "item.poweradvantage.starch": "Kartoffelstärke", - "item.poweradvantage.bioplastic_ingot": "Biokunststoff-Barren", - "item.poweradvantage.sprocket": "Kettenrad", - "block.poweradvantage.fluid_metal_tank": "Metall-Lagertank", - "item.poweradvantage.rotator_tool": "Drehwerkzeug", - "block.poweradvantage.infinite_steam": "Unendlichen Dampfkraft", - "block.poweradvantage.infinite_electricity": "Unendliche Elektrische Leistung", - "block.poweradvantage.infinite_quantum": "Unendliche Quanten Leistung", - "block.poweradvantage.crude_oil": "Rohöl", - "block.poweradvantage.still": "Brennerei", - "block.poweradvantage.refined_oil": "Raffiniertes Öl", - "block.poweradvantage.fluid_switch": "Ventil", - "block.poweradvantage.converter_rf_steam": "Dampf-RF Leistungstransformator", - "block.poweradvantage.converter_rf_electricity": "Elektrizität-RF Leistungstransformator", - "block.poweradvantage.converter_rf_quantum": "Quant-RF Leistungstransformator", - "block.poweradvantage.converter_tr_electricity": "Elektrizität-EU Leistungstransformator", - "item.poweradvantage.crude_oil_bucket": "Eimer Rohöl", - "item.poweradvantage.refined_oil_bucket": "Eimer für Raffiniertes Öl", - "block.poweradvantage.fluid_pipe_terminal": "Rohr" -} diff --git a/src/main/resources/assets/poweradvantage/lang/en_au.json b/src/main/resources/assets/poweradvantage/lang/en_au.json deleted file mode 100644 index 7b1619fe..00000000 --- a/src/main/resources/assets/poweradvantage/lang/en_au.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", - "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)", - "nei.poweradvantage.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)", - "tooltip.poweradvantage.empty": "Empty", - "tooltip.poweradvantage.buckets_of": "%x buckets of %y", - "itemGroup.poweradvantage": "Power Advantage", - "tile.poweradvantage.fluid_drain.name": "Drain", - "tile.poweradvantage.fluid_discharge.name": "Discharge Grate", - "tile.poweradvantage.fluid_storage_tank.name": "Portable Fluid Tank", - "item.poweradvantage.fluid_storage_tank_item.name": "Storage Tank", - "tile.poweradvantage.fluid_pipe.name": "Pipe", - "tile.poweradvantage.item_conveyor.name": "Conveyor", - "tile.poweradvantage.item_filter_block.name": "Block Filter Conveyor", - "tile.poweradvantage.item_filter_food.name": "Food Filter Conveyor", - "tile.poweradvantage.item_filter_fuel.name": "Fuel Filter Conveyor", - "tile.poweradvantage.item_filter_inventory.name": "Filter Conveyor", - "tile.poweradvantage.item_filter_ore.name": "Ore Filter Conveyor", - "tile.poweradvantage.item_filter_plant.name": "Plantable Filter Conveyor", - "tile.poweradvantage.item_filter_smelt.name": "Furnace Filter Conveyor", - "tile.poweradvantage.item_filter_overflow.name": "Overflow Filter Conveyor", - "tile.poweradvantage.steel_frame.name": "Steel Frame", - "item.poweradvantage.starch.name": "Starch", - "item.poweradvantage.bioplastic_ingot.name": "Bioplastic Ingot", - "item.poweradvantage.sprocket.name": "Sprocket", - "tile.poweradvantage.fluid_metal_tank.name": "Metal Fluid Tank", - "item.poweradvantage.rotator_tool.name": "Rotation Wrench", - "tile.poweradvantage.infinite_steam.name": "Unlimited Steam Power", - "tile.poweradvantage.infinite_electricity.name": "Unlimited Electrical Power", - "tile.poweradvantage.infinite_quantum.name": "Unlimited Quantum Power", - "tile.poweradvantage.crude_oil.name": "Crude Oil", - "fluid.poweradvantage.crude_oil": "Crude Oil", - "item.bucket.crude_oil": "Bucket of Crude Oil", - "tile.poweradvantage.still.name": "Distillation Furnace", - "tile.poweradvantage.refined_oil.name": "Refined Oil", - "fluid.poweradvantage.refined_oil": "Refined Oil", - "item.bucket.refined_oil": "Bucket of Refined Oil", - "tile.poweradvantage.fluid_switch.name": "Fluid Switch Valve", - "tile.poweradvantage.converter_rf_steam.name": "Steam-RF Transformer", - "tile.poweradvantage.converter_rf_electricity.name": "Electricity-RF Transformer", - "tile.poweradvantage.converter_rf_quantum.name": "Quantum-RF Transformer", - "tile.poweradvantage.converter_tr_electricity.name": "Electricity-EU Transformer", - "block.poweradvantage.fluid_drain": "Drain", - "block.poweradvantage.fluid_discharge": "Discharge Grate", - "block.poweradvantage.fluid_storage_tank": "Portable Fluid Tank", - "item.poweradvantage.fluid_storage_tank_item": "Storage Tank", - "block.poweradvantage.fluid_pipe": "Pipe", - "block.poweradvantage.item_conveyor": "Conveyor", - "block.poweradvantage.item_filter_block": "Block Filter Conveyor", - "block.poweradvantage.item_filter_food": "Food Filter Conveyor", - "block.poweradvantage.item_filter_fuel": "Fuel Filter Conveyor", - "block.poweradvantage.item_filter_inventory": "Filter Conveyor", - "block.poweradvantage.item_filter_ore": "Ore Filter Conveyor", - "block.poweradvantage.item_filter_plant": "Plantable Filter Conveyor", - "block.poweradvantage.item_filter_smelt": "Furnace Filter Conveyor", - "block.poweradvantage.item_filter_overflow": "Overflow Filter Conveyor", - "block.poweradvantage.steel_frame": "Steel Frame", - "item.poweradvantage.starch": "Starch", - "item.poweradvantage.bioplastic_ingot": "Bioplastic Ingot", - "item.poweradvantage.sprocket": "Sprocket", - "block.poweradvantage.fluid_metal_tank": "Metal Fluid Tank", - "item.poweradvantage.rotator_tool": "Rotation Wrench", - "block.poweradvantage.infinite_steam": "Unlimited Steam Power", - "block.poweradvantage.infinite_electricity": "Unlimited Electrical Power", - "block.poweradvantage.infinite_quantum": "Unlimited Quantum Power", - "block.poweradvantage.crude_oil": "Crude Oil", - "block.poweradvantage.still": "Distillation Furnace", - "block.poweradvantage.refined_oil": "Refined Oil", - "block.poweradvantage.fluid_switch": "Fluid Switch Valve", - "block.poweradvantage.converter_rf_steam": "Steam-RF Transformer", - "block.poweradvantage.converter_rf_electricity": "Electricity-RF Transformer", - "block.poweradvantage.converter_rf_quantum": "Quantum-RF Transformer", - "block.poweradvantage.converter_tr_electricity": "Electricity-EU Transformer", - "item.poweradvantage.crude_oil_bucket": "Bucket of Crude Oil", - "item.poweradvantage.refined_oil_bucket": "Bucket of Refined Oil", - "block.poweradvantage.fluid_pipe_terminal": "Pipe" -} diff --git a/src/main/resources/assets/poweradvantage/lang/en_ca.json b/src/main/resources/assets/poweradvantage/lang/en_ca.json deleted file mode 100644 index 7b1619fe..00000000 --- a/src/main/resources/assets/poweradvantage/lang/en_ca.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", - "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)", - "nei.poweradvantage.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)", - "tooltip.poweradvantage.empty": "Empty", - "tooltip.poweradvantage.buckets_of": "%x buckets of %y", - "itemGroup.poweradvantage": "Power Advantage", - "tile.poweradvantage.fluid_drain.name": "Drain", - "tile.poweradvantage.fluid_discharge.name": "Discharge Grate", - "tile.poweradvantage.fluid_storage_tank.name": "Portable Fluid Tank", - "item.poweradvantage.fluid_storage_tank_item.name": "Storage Tank", - "tile.poweradvantage.fluid_pipe.name": "Pipe", - "tile.poweradvantage.item_conveyor.name": "Conveyor", - "tile.poweradvantage.item_filter_block.name": "Block Filter Conveyor", - "tile.poweradvantage.item_filter_food.name": "Food Filter Conveyor", - "tile.poweradvantage.item_filter_fuel.name": "Fuel Filter Conveyor", - "tile.poweradvantage.item_filter_inventory.name": "Filter Conveyor", - "tile.poweradvantage.item_filter_ore.name": "Ore Filter Conveyor", - "tile.poweradvantage.item_filter_plant.name": "Plantable Filter Conveyor", - "tile.poweradvantage.item_filter_smelt.name": "Furnace Filter Conveyor", - "tile.poweradvantage.item_filter_overflow.name": "Overflow Filter Conveyor", - "tile.poweradvantage.steel_frame.name": "Steel Frame", - "item.poweradvantage.starch.name": "Starch", - "item.poweradvantage.bioplastic_ingot.name": "Bioplastic Ingot", - "item.poweradvantage.sprocket.name": "Sprocket", - "tile.poweradvantage.fluid_metal_tank.name": "Metal Fluid Tank", - "item.poweradvantage.rotator_tool.name": "Rotation Wrench", - "tile.poweradvantage.infinite_steam.name": "Unlimited Steam Power", - "tile.poweradvantage.infinite_electricity.name": "Unlimited Electrical Power", - "tile.poweradvantage.infinite_quantum.name": "Unlimited Quantum Power", - "tile.poweradvantage.crude_oil.name": "Crude Oil", - "fluid.poweradvantage.crude_oil": "Crude Oil", - "item.bucket.crude_oil": "Bucket of Crude Oil", - "tile.poweradvantage.still.name": "Distillation Furnace", - "tile.poweradvantage.refined_oil.name": "Refined Oil", - "fluid.poweradvantage.refined_oil": "Refined Oil", - "item.bucket.refined_oil": "Bucket of Refined Oil", - "tile.poweradvantage.fluid_switch.name": "Fluid Switch Valve", - "tile.poweradvantage.converter_rf_steam.name": "Steam-RF Transformer", - "tile.poweradvantage.converter_rf_electricity.name": "Electricity-RF Transformer", - "tile.poweradvantage.converter_rf_quantum.name": "Quantum-RF Transformer", - "tile.poweradvantage.converter_tr_electricity.name": "Electricity-EU Transformer", - "block.poweradvantage.fluid_drain": "Drain", - "block.poweradvantage.fluid_discharge": "Discharge Grate", - "block.poweradvantage.fluid_storage_tank": "Portable Fluid Tank", - "item.poweradvantage.fluid_storage_tank_item": "Storage Tank", - "block.poweradvantage.fluid_pipe": "Pipe", - "block.poweradvantage.item_conveyor": "Conveyor", - "block.poweradvantage.item_filter_block": "Block Filter Conveyor", - "block.poweradvantage.item_filter_food": "Food Filter Conveyor", - "block.poweradvantage.item_filter_fuel": "Fuel Filter Conveyor", - "block.poweradvantage.item_filter_inventory": "Filter Conveyor", - "block.poweradvantage.item_filter_ore": "Ore Filter Conveyor", - "block.poweradvantage.item_filter_plant": "Plantable Filter Conveyor", - "block.poweradvantage.item_filter_smelt": "Furnace Filter Conveyor", - "block.poweradvantage.item_filter_overflow": "Overflow Filter Conveyor", - "block.poweradvantage.steel_frame": "Steel Frame", - "item.poweradvantage.starch": "Starch", - "item.poweradvantage.bioplastic_ingot": "Bioplastic Ingot", - "item.poweradvantage.sprocket": "Sprocket", - "block.poweradvantage.fluid_metal_tank": "Metal Fluid Tank", - "item.poweradvantage.rotator_tool": "Rotation Wrench", - "block.poweradvantage.infinite_steam": "Unlimited Steam Power", - "block.poweradvantage.infinite_electricity": "Unlimited Electrical Power", - "block.poweradvantage.infinite_quantum": "Unlimited Quantum Power", - "block.poweradvantage.crude_oil": "Crude Oil", - "block.poweradvantage.still": "Distillation Furnace", - "block.poweradvantage.refined_oil": "Refined Oil", - "block.poweradvantage.fluid_switch": "Fluid Switch Valve", - "block.poweradvantage.converter_rf_steam": "Steam-RF Transformer", - "block.poweradvantage.converter_rf_electricity": "Electricity-RF Transformer", - "block.poweradvantage.converter_rf_quantum": "Quantum-RF Transformer", - "block.poweradvantage.converter_tr_electricity": "Electricity-EU Transformer", - "item.poweradvantage.crude_oil_bucket": "Bucket of Crude Oil", - "item.poweradvantage.refined_oil_bucket": "Bucket of Refined Oil", - "block.poweradvantage.fluid_pipe_terminal": "Pipe" -} diff --git a/src/main/resources/assets/poweradvantage/lang/en_en.json b/src/main/resources/assets/poweradvantage/lang/en_en.json deleted file mode 100644 index 7b1619fe..00000000 --- a/src/main/resources/assets/poweradvantage/lang/en_en.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", - "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)", - "nei.poweradvantage.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)", - "tooltip.poweradvantage.empty": "Empty", - "tooltip.poweradvantage.buckets_of": "%x buckets of %y", - "itemGroup.poweradvantage": "Power Advantage", - "tile.poweradvantage.fluid_drain.name": "Drain", - "tile.poweradvantage.fluid_discharge.name": "Discharge Grate", - "tile.poweradvantage.fluid_storage_tank.name": "Portable Fluid Tank", - "item.poweradvantage.fluid_storage_tank_item.name": "Storage Tank", - "tile.poweradvantage.fluid_pipe.name": "Pipe", - "tile.poweradvantage.item_conveyor.name": "Conveyor", - "tile.poweradvantage.item_filter_block.name": "Block Filter Conveyor", - "tile.poweradvantage.item_filter_food.name": "Food Filter Conveyor", - "tile.poweradvantage.item_filter_fuel.name": "Fuel Filter Conveyor", - "tile.poweradvantage.item_filter_inventory.name": "Filter Conveyor", - "tile.poweradvantage.item_filter_ore.name": "Ore Filter Conveyor", - "tile.poweradvantage.item_filter_plant.name": "Plantable Filter Conveyor", - "tile.poweradvantage.item_filter_smelt.name": "Furnace Filter Conveyor", - "tile.poweradvantage.item_filter_overflow.name": "Overflow Filter Conveyor", - "tile.poweradvantage.steel_frame.name": "Steel Frame", - "item.poweradvantage.starch.name": "Starch", - "item.poweradvantage.bioplastic_ingot.name": "Bioplastic Ingot", - "item.poweradvantage.sprocket.name": "Sprocket", - "tile.poweradvantage.fluid_metal_tank.name": "Metal Fluid Tank", - "item.poweradvantage.rotator_tool.name": "Rotation Wrench", - "tile.poweradvantage.infinite_steam.name": "Unlimited Steam Power", - "tile.poweradvantage.infinite_electricity.name": "Unlimited Electrical Power", - "tile.poweradvantage.infinite_quantum.name": "Unlimited Quantum Power", - "tile.poweradvantage.crude_oil.name": "Crude Oil", - "fluid.poweradvantage.crude_oil": "Crude Oil", - "item.bucket.crude_oil": "Bucket of Crude Oil", - "tile.poweradvantage.still.name": "Distillation Furnace", - "tile.poweradvantage.refined_oil.name": "Refined Oil", - "fluid.poweradvantage.refined_oil": "Refined Oil", - "item.bucket.refined_oil": "Bucket of Refined Oil", - "tile.poweradvantage.fluid_switch.name": "Fluid Switch Valve", - "tile.poweradvantage.converter_rf_steam.name": "Steam-RF Transformer", - "tile.poweradvantage.converter_rf_electricity.name": "Electricity-RF Transformer", - "tile.poweradvantage.converter_rf_quantum.name": "Quantum-RF Transformer", - "tile.poweradvantage.converter_tr_electricity.name": "Electricity-EU Transformer", - "block.poweradvantage.fluid_drain": "Drain", - "block.poweradvantage.fluid_discharge": "Discharge Grate", - "block.poweradvantage.fluid_storage_tank": "Portable Fluid Tank", - "item.poweradvantage.fluid_storage_tank_item": "Storage Tank", - "block.poweradvantage.fluid_pipe": "Pipe", - "block.poweradvantage.item_conveyor": "Conveyor", - "block.poweradvantage.item_filter_block": "Block Filter Conveyor", - "block.poweradvantage.item_filter_food": "Food Filter Conveyor", - "block.poweradvantage.item_filter_fuel": "Fuel Filter Conveyor", - "block.poweradvantage.item_filter_inventory": "Filter Conveyor", - "block.poweradvantage.item_filter_ore": "Ore Filter Conveyor", - "block.poweradvantage.item_filter_plant": "Plantable Filter Conveyor", - "block.poweradvantage.item_filter_smelt": "Furnace Filter Conveyor", - "block.poweradvantage.item_filter_overflow": "Overflow Filter Conveyor", - "block.poweradvantage.steel_frame": "Steel Frame", - "item.poweradvantage.starch": "Starch", - "item.poweradvantage.bioplastic_ingot": "Bioplastic Ingot", - "item.poweradvantage.sprocket": "Sprocket", - "block.poweradvantage.fluid_metal_tank": "Metal Fluid Tank", - "item.poweradvantage.rotator_tool": "Rotation Wrench", - "block.poweradvantage.infinite_steam": "Unlimited Steam Power", - "block.poweradvantage.infinite_electricity": "Unlimited Electrical Power", - "block.poweradvantage.infinite_quantum": "Unlimited Quantum Power", - "block.poweradvantage.crude_oil": "Crude Oil", - "block.poweradvantage.still": "Distillation Furnace", - "block.poweradvantage.refined_oil": "Refined Oil", - "block.poweradvantage.fluid_switch": "Fluid Switch Valve", - "block.poweradvantage.converter_rf_steam": "Steam-RF Transformer", - "block.poweradvantage.converter_rf_electricity": "Electricity-RF Transformer", - "block.poweradvantage.converter_rf_quantum": "Quantum-RF Transformer", - "block.poweradvantage.converter_tr_electricity": "Electricity-EU Transformer", - "item.poweradvantage.crude_oil_bucket": "Bucket of Crude Oil", - "item.poweradvantage.refined_oil_bucket": "Bucket of Refined Oil", - "block.poweradvantage.fluid_pipe_terminal": "Pipe" -} diff --git a/src/main/resources/assets/poweradvantage/lang/en_gb.json b/src/main/resources/assets/poweradvantage/lang/en_gb.json deleted file mode 100644 index 7b1619fe..00000000 --- a/src/main/resources/assets/poweradvantage/lang/en_gb.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", - "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)", - "nei.poweradvantage.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)", - "tooltip.poweradvantage.empty": "Empty", - "tooltip.poweradvantage.buckets_of": "%x buckets of %y", - "itemGroup.poweradvantage": "Power Advantage", - "tile.poweradvantage.fluid_drain.name": "Drain", - "tile.poweradvantage.fluid_discharge.name": "Discharge Grate", - "tile.poweradvantage.fluid_storage_tank.name": "Portable Fluid Tank", - "item.poweradvantage.fluid_storage_tank_item.name": "Storage Tank", - "tile.poweradvantage.fluid_pipe.name": "Pipe", - "tile.poweradvantage.item_conveyor.name": "Conveyor", - "tile.poweradvantage.item_filter_block.name": "Block Filter Conveyor", - "tile.poweradvantage.item_filter_food.name": "Food Filter Conveyor", - "tile.poweradvantage.item_filter_fuel.name": "Fuel Filter Conveyor", - "tile.poweradvantage.item_filter_inventory.name": "Filter Conveyor", - "tile.poweradvantage.item_filter_ore.name": "Ore Filter Conveyor", - "tile.poweradvantage.item_filter_plant.name": "Plantable Filter Conveyor", - "tile.poweradvantage.item_filter_smelt.name": "Furnace Filter Conveyor", - "tile.poweradvantage.item_filter_overflow.name": "Overflow Filter Conveyor", - "tile.poweradvantage.steel_frame.name": "Steel Frame", - "item.poweradvantage.starch.name": "Starch", - "item.poweradvantage.bioplastic_ingot.name": "Bioplastic Ingot", - "item.poweradvantage.sprocket.name": "Sprocket", - "tile.poweradvantage.fluid_metal_tank.name": "Metal Fluid Tank", - "item.poweradvantage.rotator_tool.name": "Rotation Wrench", - "tile.poweradvantage.infinite_steam.name": "Unlimited Steam Power", - "tile.poweradvantage.infinite_electricity.name": "Unlimited Electrical Power", - "tile.poweradvantage.infinite_quantum.name": "Unlimited Quantum Power", - "tile.poweradvantage.crude_oil.name": "Crude Oil", - "fluid.poweradvantage.crude_oil": "Crude Oil", - "item.bucket.crude_oil": "Bucket of Crude Oil", - "tile.poweradvantage.still.name": "Distillation Furnace", - "tile.poweradvantage.refined_oil.name": "Refined Oil", - "fluid.poweradvantage.refined_oil": "Refined Oil", - "item.bucket.refined_oil": "Bucket of Refined Oil", - "tile.poweradvantage.fluid_switch.name": "Fluid Switch Valve", - "tile.poweradvantage.converter_rf_steam.name": "Steam-RF Transformer", - "tile.poweradvantage.converter_rf_electricity.name": "Electricity-RF Transformer", - "tile.poweradvantage.converter_rf_quantum.name": "Quantum-RF Transformer", - "tile.poweradvantage.converter_tr_electricity.name": "Electricity-EU Transformer", - "block.poweradvantage.fluid_drain": "Drain", - "block.poweradvantage.fluid_discharge": "Discharge Grate", - "block.poweradvantage.fluid_storage_tank": "Portable Fluid Tank", - "item.poweradvantage.fluid_storage_tank_item": "Storage Tank", - "block.poweradvantage.fluid_pipe": "Pipe", - "block.poweradvantage.item_conveyor": "Conveyor", - "block.poweradvantage.item_filter_block": "Block Filter Conveyor", - "block.poweradvantage.item_filter_food": "Food Filter Conveyor", - "block.poweradvantage.item_filter_fuel": "Fuel Filter Conveyor", - "block.poweradvantage.item_filter_inventory": "Filter Conveyor", - "block.poweradvantage.item_filter_ore": "Ore Filter Conveyor", - "block.poweradvantage.item_filter_plant": "Plantable Filter Conveyor", - "block.poweradvantage.item_filter_smelt": "Furnace Filter Conveyor", - "block.poweradvantage.item_filter_overflow": "Overflow Filter Conveyor", - "block.poweradvantage.steel_frame": "Steel Frame", - "item.poweradvantage.starch": "Starch", - "item.poweradvantage.bioplastic_ingot": "Bioplastic Ingot", - "item.poweradvantage.sprocket": "Sprocket", - "block.poweradvantage.fluid_metal_tank": "Metal Fluid Tank", - "item.poweradvantage.rotator_tool": "Rotation Wrench", - "block.poweradvantage.infinite_steam": "Unlimited Steam Power", - "block.poweradvantage.infinite_electricity": "Unlimited Electrical Power", - "block.poweradvantage.infinite_quantum": "Unlimited Quantum Power", - "block.poweradvantage.crude_oil": "Crude Oil", - "block.poweradvantage.still": "Distillation Furnace", - "block.poweradvantage.refined_oil": "Refined Oil", - "block.poweradvantage.fluid_switch": "Fluid Switch Valve", - "block.poweradvantage.converter_rf_steam": "Steam-RF Transformer", - "block.poweradvantage.converter_rf_electricity": "Electricity-RF Transformer", - "block.poweradvantage.converter_rf_quantum": "Quantum-RF Transformer", - "block.poweradvantage.converter_tr_electricity": "Electricity-EU Transformer", - "item.poweradvantage.crude_oil_bucket": "Bucket of Crude Oil", - "item.poweradvantage.refined_oil_bucket": "Bucket of Refined Oil", - "block.poweradvantage.fluid_pipe_terminal": "Pipe" -} diff --git a/src/main/resources/assets/poweradvantage/lang/en_pt.json b/src/main/resources/assets/poweradvantage/lang/en_pt.json deleted file mode 100644 index 0c78c2f9..00000000 --- a/src/main/resources/assets/poweradvantage/lang/en_pt.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", - "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)", - "nei.poweradvantage.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)", - "tooltip.poweradvantage.empty": "Empty", - "tooltip.poweradvantage.buckets_of": "%x buckets of %y", - "itemGroup.poweradvantage": "Power Advantage", - "tile.poweradvantage.fluid_drain.name": "Drain", - "tile.poweradvantage.fluid_discharge.name": "Discharge Grate", - "tile.poweradvantage.fluid_storage_tank.name": "Portable Fluid Tank", - "item.poweradvantage.fluid_storage_tank_item.name": "Storage Tank", - "tile.poweradvantage.fluid_pipe.name": "Pipe", - "tile.poweradvantage.item_conveyor.name": "Conveyor", - "tile.poweradvantage.item_filter_block.name": "Block Filter Conveyor", - "tile.poweradvantage.item_filter_food.name": "Food Filter Conveyor", - "tile.poweradvantage.item_filter_fuel.name": "Fuel Filter Conveyor", - "tile.poweradvantage.item_filter_inventory.name": "Filter Conveyor", - "tile.poweradvantage.item_filter_ore.name": "Ore Filter Conveyor", - "tile.poweradvantage.item_filter_plant.name": "Plantable Filter Conveyor", - "tile.poweradvantage.item_filter_smelt.name": "Furnace Filter Conveyor", - "tile.poweradvantage.item_filter_overflow.name": "Overflow Filter Conveyor", - "tile.poweradvantage.steel_frame.name": "Steel Frame", - "item.poweradvantage.starch.name": "Starch", - "item.poweradvantage.bioplastic_ingot.name": "Bioplastic Ingot", - "item.poweradvantage.sprocket.name": "Sprocket", - "tile.poweradvantage.fluid_metal_tank.name": "Metal Fluid Tank", - "item.poweradvantage.rotator_tool.name": "Rotation Wrench", - "tile.poweradvantage.infinite_steam.name": "Unlimited Steam Power", - "tile.poweradvantage.infinite_electricity.name": "Unlimited Electrical Power", - "tile.poweradvantage.infinite_quantum.name": "Unlimited Quantum Power", - "tile.poweradvantage.crude_oil.name": "Black Gold", - "fluid.poweradvantage.crude_oil": "Black Gold", - "item.bucket.crude_oil": "Bucket o' Black Gold", - "tile.poweradvantage.still.name": "'Stillery", - "tile.poweradvantage.refined_oil.name": "Lamp Oil", - "fluid.poweradvantage.refined_oil": "Lamp Oil", - "item.bucket.refined_oil": "Bucket o' Lamp Oil", - "tile.poweradvantage.fluid_switch.name": "Big Red Valve", - "tile.poweradvantage.converter_rf_steam.name": "Steam-RF Transformer", - "tile.poweradvantage.converter_rf_electricity.name": "Electricity-RF Transformer", - "tile.poweradvantage.converter_rf_quantum.name": "Quantum-RF Transformer", - "tile.poweradvantage.converter_tr_electricity.name": "Electricity-EU Transformer", - "block.poweradvantage.fluid_drain": "Drain", - "block.poweradvantage.fluid_discharge": "Discharge Grate", - "block.poweradvantage.fluid_storage_tank": "Portable Fluid Tank", - "item.poweradvantage.fluid_storage_tank_item": "Storage Tank", - "block.poweradvantage.fluid_pipe": "Pipe", - "block.poweradvantage.item_conveyor": "Conveyor", - "block.poweradvantage.item_filter_block": "Block Filter Conveyor", - "block.poweradvantage.item_filter_food": "Food Filter Conveyor", - "block.poweradvantage.item_filter_fuel": "Fuel Filter Conveyor", - "block.poweradvantage.item_filter_inventory": "Filter Conveyor", - "block.poweradvantage.item_filter_ore": "Ore Filter Conveyor", - "block.poweradvantage.item_filter_plant": "Plantable Filter Conveyor", - "block.poweradvantage.item_filter_smelt": "Furnace Filter Conveyor", - "block.poweradvantage.item_filter_overflow": "Overflow Filter Conveyor", - "block.poweradvantage.steel_frame": "Steel Frame", - "item.poweradvantage.starch": "Starch", - "item.poweradvantage.bioplastic_ingot": "Bioplastic Ingot", - "item.poweradvantage.sprocket": "Sprocket", - "block.poweradvantage.fluid_metal_tank": "Metal Fluid Tank", - "item.poweradvantage.rotator_tool": "Rotation Wrench", - "block.poweradvantage.infinite_steam": "Unlimited Steam Power", - "block.poweradvantage.infinite_electricity": "Unlimited Electrical Power", - "block.poweradvantage.infinite_quantum": "Unlimited Quantum Power", - "block.poweradvantage.crude_oil": "Black Gold", - "block.poweradvantage.still": "'Stillery", - "block.poweradvantage.refined_oil": "Lamp Oil", - "block.poweradvantage.fluid_switch": "Big Red Valve", - "block.poweradvantage.converter_rf_steam": "Steam-RF Transformer", - "block.poweradvantage.converter_rf_electricity": "Electricity-RF Transformer", - "block.poweradvantage.converter_rf_quantum": "Quantum-RF Transformer", - "block.poweradvantage.converter_tr_electricity": "Electricity-EU Transformer", - "item.poweradvantage.crude_oil_bucket": "Bucket o' Black Gold", - "item.poweradvantage.refined_oil_bucket": "Bucket o' Lamp Oil", - "block.poweradvantage.fluid_pipe_terminal": "Pipe" -} diff --git a/src/main/resources/assets/poweradvantage/lang/en_us.json b/src/main/resources/assets/poweradvantage/lang/en_us.json deleted file mode 100644 index 76a243b9..00000000 --- a/src/main/resources/assets/poweradvantage/lang/en_us.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", - "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)", - "nei.poweradvantage.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)", - "tooltip.poweradvantage.empty": "Empty", - "tooltip.poweradvantage.buckets_of": "%x buckets of %y", - "itemGroup.poweradvantage": "Power Advantage", - "tile.poweradvantage.fluid_drain.name": "Drain", - "tile.poweradvantage.fluid_discharge.name": "Discharge Grate", - "tile.poweradvantage.fluid_storage_tank.name": "Portable Fluid Tank", - "tile.poweradvantage.fluid_pipe.name": "Pipe", - "tile.poweradvantage.item_conveyor.name": "Conveyor", - "tile.poweradvantage.item_filter_block.name": "Block Filter Conveyor", - "tile.poweradvantage.item_filter_food.name": "Food Filter Conveyor", - "tile.poweradvantage.item_filter_fuel.name": "Fuel Filter Conveyor", - "tile.poweradvantage.item_filter_inventory.name": "Filter Conveyor", - "tile.poweradvantage.item_filter_ore.name": "Ore Filter Conveyor", - "tile.poweradvantage.item_filter_plant.name": "Plantable Filter Conveyor", - "tile.poweradvantage.item_filter_smelt.name": "Furnace Filter Conveyor", - "tile.poweradvantage.item_filter_overflow.name": "Overflow Filter Conveyor", - "tile.poweradvantage.steel_frame.name": "Steel Frame", - "item.poweradvantage.starch.name": "Starch", - "item.poweradvantage.bioplastic_ingot.name": "Bioplastic Ingot", - "item.poweradvantage.sprocket.name": "Sprocket", - "tile.poweradvantage.fluid_metal_tank.name": "Metal Fluid Tank", - "item.poweradvantage.rotator_tool.name": "Rotation Wrench", - "tile.poweradvantage.infinite_steam.name": "Unlimited Steam Power", - "tile.poweradvantage.infinite_electricity.name": "Unlimited Electrical Power", - "tile.poweradvantage.infinite_quantum.name": "Unlimited Quantum Power", - "tile.poweradvantage.crude_oil.name": "Crude Oil", - "fluid.poweradvantage.crude_oil": "Crude Oil", - "item.bucket.crude_oil": "Bucket of Crude Oil", - "tile.poweradvantage.still.name": "Distillation Furnace", - "tile.poweradvantage.refined_oil.name": "Refined Oil", - "fluid.poweradvantage.refined_oil": "Refined Oil", - "item.bucket.refined_oil": "Bucket of Refined Oil", - "tile.poweradvantage.fluid_switch.name": "Fluid Switch Valve", - "tile.poweradvantage.converter_rf_steam.name": "Steam-RF Transformer", - "tile.poweradvantage.converter_rf_electricity.name": "Electricity-RF Transformer", - "tile.poweradvantage.converter_rf_quantum.name": "Quantum-RF Transformer", - "tile.poweradvantage.converter_tr_electricity.name": "Electricity-EU Transformer", - "block.poweradvantage.fluid_drain": "Drain", - "block.poweradvantage.fluid_discharge": "Discharge Grate", - "block.poweradvantage.fluid_storage_tank": "Portable Fluid Tank", - "block.poweradvantage.fluid_pipe": "Pipe", - "block.poweradvantage.item_conveyor": "Conveyor", - "block.poweradvantage.item_filter_block": "Block Filter Conveyor", - "block.poweradvantage.item_filter_food": "Food Filter Conveyor", - "block.poweradvantage.item_filter_fuel": "Fuel Filter Conveyor", - "block.poweradvantage.item_filter_inventory": "Filter Conveyor", - "block.poweradvantage.item_filter_ore": "Ore Filter Conveyor", - "block.poweradvantage.item_filter_plant": "Plantable Filter Conveyor", - "block.poweradvantage.item_filter_smelt": "Furnace Filter Conveyor", - "block.poweradvantage.item_filter_overflow": "Overflow Filter Conveyor", - "block.poweradvantage.steel_frame": "Steel Frame", - "item.poweradvantage.starch": "Starch", - "item.poweradvantage.bioplastic_ingot": "Bioplastic Ingot", - "item.poweradvantage.sprocket": "Sprocket", - "block.poweradvantage.fluid_metal_tank": "Metal Fluid Tank", - "item.poweradvantage.rotator_tool": "Rotation Wrench", - "block.poweradvantage.infinite_steam": "Unlimited Steam Power", - "block.poweradvantage.infinite_electricity": "Unlimited Electrical Power", - "block.poweradvantage.infinite_quantum": "Unlimited Quantum Power", - "block.poweradvantage.crude_oil": "Crude Oil", - "block.poweradvantage.still": "Distillation Furnace", - "block.poweradvantage.refined_oil": "Refined Oil", - "block.poweradvantage.fluid_switch": "Fluid Switch Valve", - "block.poweradvantage.converter_rf_steam": "Steam-RF Transformer", - "block.poweradvantage.converter_rf_electricity": "Electricity-RF Transformer", - "block.poweradvantage.converter_rf_quantum": "Quantum-RF Transformer", - "block.poweradvantage.converter_tr_electricity": "Electricity-EU Transformer", - "item.poweradvantage.crude_oil_bucket": "Bucket of Crude Oil", - "item.poweradvantage.refined_oil_bucket": "Bucket of Refined Oil", - "block.poweradvantage.fluid_pipe_terminal": "Pipe" -} diff --git a/src/main/resources/assets/poweradvantage/lang/es_es.json b/src/main/resources/assets/poweradvantage/lang/es_es.json deleted file mode 100644 index 68471f7c..00000000 --- a/src/main/resources/assets/poweradvantage/lang/es_es.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", - "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Gran Horno (no forma)", - "nei.poweradvantage.recipehandler.shapedfurnace.name": "Gran Horno (en forma)", - "tooltip.poweradvantage.empty": "Vacío", - "tooltip.poweradvantage.buckets_of ": "%x cubos de %y", - "itemGroup.poweradvantage": "Ventaja de Energía", - "tile.poweradvantage.fluid_drain.name": "Drenaje", - "tile.poweradvantage.fluid_discharge.name": "Descarga Grate", - "tile.poweradvantage.fluid_storage_tank.name": "Tanque de almacenamiento", - "item.poweradvantage.fluid_storage_tank_item.name": "Tanque de almacenamiento", - "tile.poweradvantage.fluid_pipe.name": "Tubo", - "tile.poweradvantage.item_conveyor.name": "Transportadores", - "tile.poweradvantage.item_filter_block.name": "Bloquear Filtro Transportadores", - "tile.poweradvantage.item_filter_food.name": "Filtro Alimentos Transportadores", - "tile.poweradvantage.item_filter_fuel.name": "Filtro de combustible Transportadores", - "tile.poweradvantage.item_filter_inventory.name": "Filtro Transportadores", - "tile.poweradvantage.item_filter_ore.name": "Ore Transportadores Filtro", - "tile.poweradvantage.item_filter_plant.name": "Plantable Transportadores Filtro", - "tile.poweradvantage.item_filter_smelt.name": "Horno Filtro Transportadores", - "tile.poweradvantage.item_filter_overflow.name": "Desbordamiento Filtro Transportadores", - "tile.poweradvantage.steel_frame.name": "Marco de Acero", - "item.poweradvantage.starch.name": "Almidón", - "item.poweradvantage.bioplastic_ingot.name": "Bioplástico Lingote", - "item.poweradvantage.sprocket.name": "Piñón", - "tile.poweradvantage.fluid_metal_tank.name": "Tanque de Almacenamiento de Acero", - "item.poweradvantage.rotator_tool.name": "Herramienta de Rotación", - "tile.poweradvantage.infinite_steam.name": "La Energía de Vapor Infinito", - "tile.poweradvantage.infinite_electricity.name": "Energía Eléctrica Infinito", - "tile.poweradvantage.infinite_quantum.name": "Cuántica Infinito", - "tile.poweradvantage.crude_oil.name": "Petróleo Crudo", - "fluid.poweradvantage.crude_oil": "Petróleo Crudo", - "item.bucket.crude_oil.name": "Cubo de Crudo", - "tile.poweradvantage.still.name": "Destilería", - "tile.poweradvantage.refined_oil.name": "Refinado de Petróleo", - "fluid.poweradvantage.refined_oil": "Refinado de Petróleo", - "item.bucket.refined_oil.name": "Cubo de Refinado de Petróleo", - "tile.poweradvantage.fluid_switch.name": "Válvula", - "tile.poweradvantage.converter_rf_steam.name": "Vapor-RF Transformador", - "tile.poweradvantage.converter_rf_electricity.name": "Electricidad-RF Transformador", - "tile.poweradvantage.converter_rf_quantum.name": "Cuántico-RF Transformador", - "tile.poweradvantage.converter_tr_electricity.name": "Electricidad-EU Transformador", - "block.poweradvantage.fluid_drain": "Drenaje", - "block.poweradvantage.fluid_discharge": "Descarga Grate", - "block.poweradvantage.fluid_storage_tank": "Tanque de almacenamiento", - "item.poweradvantage.fluid_storage_tank_item": "Tanque de almacenamiento", - "block.poweradvantage.fluid_pipe": "Tubo", - "block.poweradvantage.item_conveyor": "Transportadores", - "block.poweradvantage.item_filter_block": "Bloquear Filtro Transportadores", - "block.poweradvantage.item_filter_food": "Filtro Alimentos Transportadores", - "block.poweradvantage.item_filter_fuel": "Filtro de combustible Transportadores", - "block.poweradvantage.item_filter_inventory": "Filtro Transportadores", - "block.poweradvantage.item_filter_ore": "Ore Transportadores Filtro", - "block.poweradvantage.item_filter_plant": "Plantable Transportadores Filtro", - "block.poweradvantage.item_filter_smelt": "Horno Filtro Transportadores", - "block.poweradvantage.item_filter_overflow": "Desbordamiento Filtro Transportadores", - "block.poweradvantage.steel_frame": "Marco de Acero", - "item.poweradvantage.starch": "Almidón", - "item.poweradvantage.bioplastic_ingot": "Bioplástico Lingote", - "item.poweradvantage.sprocket": "Piñón", - "block.poweradvantage.fluid_metal_tank": "Tanque de Almacenamiento de Acero", - "item.poweradvantage.rotator_tool": "Herramienta de Rotación", - "block.poweradvantage.infinite_steam": "La Energía de Vapor Infinito", - "block.poweradvantage.infinite_electricity": "Energía Eléctrica Infinito", - "block.poweradvantage.infinite_quantum": "Cuántica Infinito", - "block.poweradvantage.crude_oil": "Petróleo Crudo", - "block.poweradvantage.still": "Destilería", - "block.poweradvantage.refined_oil": "Refinado de Petróleo", - "block.poweradvantage.fluid_switch": "Válvula", - "block.poweradvantage.converter_rf_steam": "Vapor-RF Transformador", - "block.poweradvantage.converter_rf_electricity": "Electricidad-RF Transformador", - "block.poweradvantage.converter_rf_quantum": "Cuántico-RF Transformador", - "block.poweradvantage.converter_tr_electricity": "Electricidad-EU Transformador", - "block.poweradvantage.fluid_pipe_terminal": "Tubo" -} diff --git a/src/main/resources/assets/poweradvantage/lang/es_mx.json b/src/main/resources/assets/poweradvantage/lang/es_mx.json deleted file mode 100644 index c0c5cb2e..00000000 --- a/src/main/resources/assets/poweradvantage/lang/es_mx.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", - "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Gran Horno (no forma)", - "nei.poweradvantage.recipehandler.shapedfurnace.name": "Gran Horno (en forma)", - "tooltip.poweradvantage.empty": "Vacío", - "tooltip.poweradvantage.buckets_of": "%x cubos de %y", - "itemGroup.poweradvantage": "Ventaja de Energía", - "tile.poweradvantage.fluid_drain.name": "Drenaje", - "tile.poweradvantage.fluid_discharge.name": "Descarga Grate", - "tile.poweradvantage.fluid_storage_tank.name": "Tanque de almacenamiento", - "item.poweradvantage.fluid_storage_tank_item.name": "Tanque de almacenamiento", - "tile.poweradvantage.fluid_pipe.name": "Tubo", - "tile.poweradvantage.item_conveyor.name": "Transportadores", - "tile.poweradvantage.item_filter_block.name": "Bloquear Filtro Transportadores", - "tile.poweradvantage.item_filter_food.name": "Filtro Alimentos Transportadores", - "tile.poweradvantage.item_filter_fuel.name": "Filtro de combustible Transportadores", - "tile.poweradvantage.item_filter_inventory.name": "Filtro Transportadores", - "tile.poweradvantage.item_filter_ore.name": "Ore Transportadores Filtro", - "tile.poweradvantage.item_filter_plant.name": "Plantable Transportadores Filtro", - "tile.poweradvantage.item_filter_smelt.name": "Horno Filtro Transportadores", - "tile.poweradvantage.item_filter_overflow.name": "Desbordamiento Filtro Transportadores", - "tile.poweradvantage.steel_frame.name": "Marco de Acero", - "item.poweradvantage.starch.name": "Almidón", - "item.poweradvantage.bioplastic_ingot.name": "Bioplástico Lingote", - "item.poweradvantage.sprocket.name": "Piñón", - "tile.poweradvantage.fluid_metal_tank.name": "Tanque de Almacenamiento de Acero", - "item.poweradvantage.rotator_tool.name": "Herramienta de Rotación", - "tile.poweradvantage.infinite_steam.name": "La Energía de Vapor Infinito", - "tile.poweradvantage.infinite_electricity.name": "Energía Eléctrica Infinito", - "tile.poweradvantage.infinite_quantum.name": "Cuántica Infinito", - "tile.poweradvantage.crude_oil.name": "Petróleo Crudo", - "fluid.poweradvantage.crude_oil": "Petróleo Crudo", - "item.bucket.crude_oil.name": "Cubo de Crudo", - "tile.poweradvantage.still.name": "Destilería", - "tile.poweradvantage.refined_oil.name": "Refinado de Petróleo", - "fluid.poweradvantage.refined_oil": "Refinado de Petróleo", - "item.bucket.refined_oil.name": "Cubo de Refinado de Petróleo", - "tile.poweradvantage.fluid_switch.name": "Válvula", - "tile.poweradvantage.converter_rf_steam.name": "Vapor-RF Transformador", - "tile.poweradvantage.converter_rf_electricity.name": "Electricidad-RF Transformador", - "tile.poweradvantage.converter_rf_quantum.name": "Cuántico-RF Transformador", - "tile.poweradvantage.converter_tr_electricity.name": "Electricidad-EU Transformador", - "block.poweradvantage.fluid_drain": "Drenaje", - "block.poweradvantage.fluid_discharge": "Descarga Grate", - "block.poweradvantage.fluid_storage_tank": "Tanque de almacenamiento", - "item.poweradvantage.fluid_storage_tank_item": "Tanque de almacenamiento", - "block.poweradvantage.fluid_pipe": "Tubo", - "block.poweradvantage.item_conveyor": "Transportadores", - "block.poweradvantage.item_filter_block": "Bloquear Filtro Transportadores", - "block.poweradvantage.item_filter_food": "Filtro Alimentos Transportadores", - "block.poweradvantage.item_filter_fuel": "Filtro de combustible Transportadores", - "block.poweradvantage.item_filter_inventory": "Filtro Transportadores", - "block.poweradvantage.item_filter_ore": "Ore Transportadores Filtro", - "block.poweradvantage.item_filter_plant": "Plantable Transportadores Filtro", - "block.poweradvantage.item_filter_smelt": "Horno Filtro Transportadores", - "block.poweradvantage.item_filter_overflow": "Desbordamiento Filtro Transportadores", - "block.poweradvantage.steel_frame": "Marco de Acero", - "item.poweradvantage.starch": "Almidón", - "item.poweradvantage.bioplastic_ingot": "Bioplástico Lingote", - "item.poweradvantage.sprocket": "Piñón", - "block.poweradvantage.fluid_metal_tank": "Tanque de Almacenamiento de Acero", - "item.poweradvantage.rotator_tool": "Herramienta de Rotación", - "block.poweradvantage.infinite_steam": "La Energía de Vapor Infinito", - "block.poweradvantage.infinite_electricity": "Energía Eléctrica Infinito", - "block.poweradvantage.infinite_quantum": "Cuántica Infinito", - "block.poweradvantage.crude_oil": "Petróleo Crudo", - "block.poweradvantage.still": "Destilería", - "block.poweradvantage.refined_oil": "Refinado de Petróleo", - "block.poweradvantage.fluid_switch": "Válvula", - "block.poweradvantage.converter_rf_steam": "Vapor-RF Transformador", - "block.poweradvantage.converter_rf_electricity": "Electricidad-RF Transformador", - "block.poweradvantage.converter_rf_quantum": "Cuántico-RF Transformador", - "block.poweradvantage.converter_tr_electricity": "Electricidad-EU Transformador", - "block.poweradvantage.fluid_pipe_terminal": "Tubo" -} diff --git a/src/main/resources/assets/poweradvantage/lang/it_it.json b/src/main/resources/assets/poweradvantage/lang/it_it.json deleted file mode 100644 index b69cfcc1..00000000 --- a/src/main/resources/assets/poweradvantage/lang/it_it.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", - "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Grande Fornace (informe)", - "nei.poweradvantage.recipehandler.shapedfurnace.name": "Grande Fornace (forma)", - "tooltip.poweradvantage.empty": "vuoto", - "tooltip.poweradvantage.buckets_of": "%x secchi d'%y", - "itemGroup.poweradvantage": "Potenza Advantage", - "tile.poweradvantage.fluid_drain.name": "Scarico", - "tile.poweradvantage.fluid_discharge.name": "Griglia di scarico", - "tile.poweradvantage.fluid_storage_tank.name": "Serbatoio", - "item.poweradvantage.fluid_storage_tank_item.name": "Serbatoio", - "tile.poweradvantage.fluid_pipe.name": "Tubo", - "tile.poweradvantage.item_conveyor.name": "Trasportatore", - "tile.poweradvantage.item_filter_block.name": "Filtro di blocco trasportatore", - "tile.poweradvantage.item_filter_food.name": "Filtro alimentare Conveyor", - "tile.poweradvantage.item_filter_fuel.name": "carburante Filtro Conveyor", - "tile.poweradvantage.item_filter_inventory.name": "Filtro Conveyor", - "tile.poweradvantage.item_filter_ore.name": "Ore Filtro Conveyor", - "tile.poweradvantage.item_filter_plant.name": "Plantable Filtro Conveyor", - "tile.poweradvantage.item_filter_smelt.name": "Forno Filtro Conveyor", - "tile.poweradvantage.item_filter_overflow.name": "Straripamento Filtro Conveyor", - "tile.poweradvantage.steel_frame.name": "Steel Frame", - "item.poweradvantage.starch.name": "Amido", - "item.poweradvantage.bioplastic_ingot.name": "Bioplastica Lingotto", - "item.poweradvantage.sprocket.name": "Sprocket", - "tile.poweradvantage.fluid_metal_tank.name": "Serbatoio in Metallo", - "item.poweradvantage.rotator_tool.name": "Strumento di Rotazione", - "tile.poweradvantage.infinite_steam.name": "Forza Vapore Infinite", - "tile.poweradvantage.infinite_electricity.name": "Elettricità Infinite", - "tile.poweradvantage.infinite_quantum.name": "Atomica Infinite", - "tile.poweradvantage.crude_oil.name": "Olio a Crudo", - "fluid.poweradvantage.crude_oil": "Olio a Crudo", - "item.bucket.crude_oil.name": "Secchio di Greggio", - "tile.poweradvantage.still.name": "Distilleria", - "tile.poweradvantage.refined_oil.name": "Olio Raffinato", - "fluid.poweradvantage.refined_oil": "Olio Raffinato", - "item.bucket.refined_oil.name": "Secchio di Olio Raffinato", - "tile.poweradvantage.fluid_switch.name": "Valvola", - "tile.poweradvantage.converter_rf_steam.name": "Vapore-RF Trasformatore", - "tile.poweradvantage.converter_rf_electricity.name": "Elettricità-RF Trasformatore", - "tile.poweradvantage.converter_rf_quantum.name": "Quantistico-RF Trasformatore", - "tile.poweradvantage.converter_tr_electricity.name": "Elettricità-EU Trasformatore", - "block.poweradvantage.fluid_drain": "Scarico", - "block.poweradvantage.fluid_discharge": "Griglia di scarico", - "block.poweradvantage.fluid_storage_tank": "Serbatoio", - "item.poweradvantage.fluid_storage_tank_item": "Serbatoio", - "block.poweradvantage.fluid_pipe": "Tubo", - "block.poweradvantage.item_conveyor": "Trasportatore", - "block.poweradvantage.item_filter_block": "Filtro di blocco trasportatore", - "block.poweradvantage.item_filter_food": "Filtro alimentare Conveyor", - "block.poweradvantage.item_filter_fuel": "carburante Filtro Conveyor", - "block.poweradvantage.item_filter_inventory": "Filtro Conveyor", - "block.poweradvantage.item_filter_ore": "Ore Filtro Conveyor", - "block.poweradvantage.item_filter_plant": "Plantable Filtro Conveyor", - "block.poweradvantage.item_filter_smelt": "Forno Filtro Conveyor", - "block.poweradvantage.item_filter_overflow": "Straripamento Filtro Conveyor", - "block.poweradvantage.steel_frame": "Steel Frame", - "item.poweradvantage.starch": "Amido", - "item.poweradvantage.bioplastic_ingot": "Bioplastica Lingotto", - "item.poweradvantage.sprocket": "Sprocket", - "block.poweradvantage.fluid_metal_tank": "Serbatoio in Metallo", - "item.poweradvantage.rotator_tool": "Strumento di Rotazione", - "block.poweradvantage.infinite_steam": "Forza Vapore Infinite", - "block.poweradvantage.infinite_electricity": "Elettricità Infinite", - "block.poweradvantage.infinite_quantum": "Atomica Infinite", - "block.poweradvantage.crude_oil": "Olio a Crudo", - "block.poweradvantage.still": "Distilleria", - "block.poweradvantage.refined_oil": "Olio Raffinato", - "block.poweradvantage.fluid_switch": "Valvola", - "block.poweradvantage.converter_rf_steam": "Vapore-RF Trasformatore", - "block.poweradvantage.converter_rf_electricity": "Elettricità-RF Trasformatore", - "block.poweradvantage.converter_rf_quantum": "Quantistico-RF Trasformatore", - "block.poweradvantage.converter_tr_electricity": "Elettricità-EU Trasformatore", - "block.poweradvantage.fluid_pipe_terminal": "Tubo" -} diff --git a/src/main/resources/assets/poweradvantage/lang/ja_jp.json b/src/main/resources/assets/poweradvantage/lang/ja_jp.json deleted file mode 100644 index b65f8ea5..00000000 --- a/src/main/resources/assets/poweradvantage/lang/ja_jp.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", - "nei.poweradvantage.recipehandler.shapelessfurnace.name": "大型炉(型崩れ)", - "nei.poweradvantage.recipehandler.shapedfurnace.name": "大型炉(形)", - "tooltip.poweradvantage.empty": "空", - "tooltip.poweradvantage.buckets_of": "%yの%xバケット", - "itemGroup.poweradvantage": "パワー·アドバンテージ", - "tile.poweradvantage.fluid_drain.name": "ドレン", - "tile.poweradvantage.fluid_discharge.name": "放電グレート", - "tile.poweradvantage.fluid_storage_tank.name": "貯蔵タンク", - "item.poweradvantage.fluid_storage_tank_item.name": "貯蔵タンク", - "tile.poweradvantage.fluid_pipe.name": "パイプ", - "tile.poweradvantage.item_conveyor.name": "コンベア", - "tile.poweradvantage.item_filter_block.name": "ブロックフィルタコンベア", - "tile.poweradvantage.item_filter_food.name": "フードフィルターコンベア", - "tile.poweradvantage.item_filter_fuel.name": "燃料フィルターコンベア", - "tile.poweradvantage.item_filter_inventory.name": "フィルタコンベヤー", - "tile.poweradvantage.item_filter_ore.name": "鉱石フィルターコンベア", - "tile.poweradvantage.item_filter_plant.name": "プラントフィルタコンベヤー", - "tile.poweradvantage.item_filter_smelt.name": "炉フィルターコンベア", - "tile.poweradvantage.item_filter_overflow.name": "オーバーフローフィルタコンベヤー", - "tile.poweradvantage.steel_frame.name": "鉄骨", - "item.poweradvantage.starch.name": "デンプン", - "item.poweradvantage.bioplastic_ingot.name": "は、バイオプラスチックインゴットを", - "item.poweradvantage.sprocket.name": "スプロケット", - "tile.poweradvantage.infinite_steam.name": "無限の蒸気動力", - "tile.poweradvantage.infinite_electricity.name": "無限の電力", - "tile.poweradvantage.infinite_quantum.name": "無限の量子パワー", - "tile.poweradvantage.fluid_metal_tank.name": "金属製貯蔵タンク", - "item.poweradvantage.rotator_tool.name": "回転ツール", - "tile.poweradvantage.crude_oil.name": "原油", - "fluid.poweradvantage.crude_oil": "原油", - "item.bucket.crude_oil.name": "原油のバケット", - "tile.poweradvantage.still.name": "蒸留酒製造所", - "tile.poweradvantage.refined_oil.name": "精製油", - "fluid.poweradvantage.refined_oil": "精製油", - "item.bucket.refined_oil.name": "精製油のバケツ", - "tile.poweradvantage.fluid_switch.name": "バルブ", - "tile.poweradvantage.converter_rf_steam.name": "蒸気-RFトランス", - "tile.poweradvantage.converter_rf_electricity.name": "電気-RFトランス", - "tile.poweradvantage.converter_rf_quantum.name": "量子-RFトランス", - "tile.poweradvantage.converter_tr_electricity.name": "電気-EUトランス", - "block.poweradvantage.fluid_drain": "ドレン", - "block.poweradvantage.fluid_discharge": "放電グレート", - "block.poweradvantage.fluid_storage_tank": "貯蔵タンク", - "item.poweradvantage.fluid_storage_tank_item": "貯蔵タンク", - "block.poweradvantage.fluid_pipe": "パイプ", - "block.poweradvantage.item_conveyor": "コンベア", - "block.poweradvantage.item_filter_block": "ブロックフィルタコンベア", - "block.poweradvantage.item_filter_food": "フードフィルターコンベア", - "block.poweradvantage.item_filter_fuel": "燃料フィルターコンベア", - "block.poweradvantage.item_filter_inventory": "フィルタコンベヤー", - "block.poweradvantage.item_filter_ore": "鉱石フィルターコンベア", - "block.poweradvantage.item_filter_plant": "プラントフィルタコンベヤー", - "block.poweradvantage.item_filter_smelt": "炉フィルターコンベア", - "block.poweradvantage.item_filter_overflow": "オーバーフローフィルタコンベヤー", - "block.poweradvantage.steel_frame": "鉄骨", - "item.poweradvantage.starch": "デンプン", - "item.poweradvantage.bioplastic_ingot": "は、バイオプラスチックインゴットを", - "item.poweradvantage.sprocket": "スプロケット", - "block.poweradvantage.infinite_steam": "無限の蒸気動力", - "block.poweradvantage.infinite_electricity": "無限の電力", - "block.poweradvantage.infinite_quantum": "無限の量子パワー", - "block.poweradvantage.fluid_metal_tank": "金属製貯蔵タンク", - "item.poweradvantage.rotator_tool": "回転ツール", - "block.poweradvantage.crude_oil": "原油", - "block.poweradvantage.still": "蒸留酒製造所", - "block.poweradvantage.refined_oil": "精製油", - "block.poweradvantage.fluid_switch": "バルブ", - "block.poweradvantage.converter_rf_steam": "蒸気-RFトランス", - "block.poweradvantage.converter_rf_electricity": "電気-RFトランス", - "block.poweradvantage.converter_rf_quantum": "量子-RFトランス", - "block.poweradvantage.converter_tr_electricity": "電気-EUトランス", - "block.poweradvantage.fluid_pipe_terminal": "パイプ" -} diff --git a/src/main/resources/assets/poweradvantage/lang/ko_kr.json b/src/main/resources/assets/poweradvantage/lang/ko_kr.json deleted file mode 100644 index 4680143e..00000000 --- a/src/main/resources/assets/poweradvantage/lang/ko_kr.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", - "nei.poweradvantage.recipehandler.shapelessfurnace.name": "대형 전기로 (못생긴)", - "nei.poweradvantage.recipehandler.shapedfurnace.name": "대형 전기로 (모양)", - "tooltip.poweradvantage.empty": "빈", - "tooltip.tooltip.poweradvantage.buckets_of": "%는 X 버킷", - "itemGroup.poweradvantage": "전원의 장점을 ", - "tile.poweradvantage.fluid_drain.name": "드레인", - "tile.poweradvantage.fluid_discharge.name": "방전 화격자", - "tile.poweradvantage.fluid_storage_tank.name": "저장 탱크", - "item.poweradvantage.fluid_storage_tank_item.name": "는 저장 탱크를 ", - "tile.poweradvantage.fluid_pipe.name": "파이프", - "tile.poweradvantage.item_conveyor.name": "컨베이어", - "tile.poweradvantage.item_filter_block.name": "블록 필터 컨베이어", - "tile.poweradvantage.item_filter_food.name": "식품 필터 컨베이어", - "tile.poweradvantage.item_filter_fuel.name": "연료 필터 컨베이어", - "tile.poweradvantage.item_filter_inventory.name": "필터 컨베이어", - "tile.poweradvantage.item_filter_ore.name": "광석 필터 컨베이어", - "tile.poweradvantage.item_filter_plant.name": "물만 주면 식물이 필터 컨베이어", - "tile.poweradvantage.item_filter_smelt.name": "노 필터 컨베이어", - "tile.poweradvantage.item_filter_overflow.name": "오버 플로우필터 컨베이어", - "tile.poweradvantage.steel_frame.name": "스틸 프레임", - "item.poweradvantage.starch.name": "전분", - "item.poweradvantage.bioplastic_ingot.name": "는 바이오 플라스틱 잉곳을 ", - "item.poweradvantage.sprocket.name": "스프로킷", - "tile.poweradvantage.fluid_metal_tank.name": "금속 저장조", - "item.poweradvantage.rotator_tool.name": "회전 도구", - "tile.poweradvantage.infinite_steam.name": "무한 증기 력", - "tile.poweradvantage.infinite_electricity.name": "무한 전력", - "tile.poweradvantage.infinite_quantum.name": "무한 양자 전력", - "tile.poweradvantage.crude_oil.name": "원유", - "fluid.poweradvantage.crude_oil": "원유", - "item.bucket.crude_oil.name": "원유의 버킷", - "tile.poweradvantage.still.name": "증류소", - "tile.poweradvantage.refined_oil.name": "정유", - "fluid.poweradvantage.refined_oil": "정유", - "item.bucket.refined_oil.name": "정유의 통", - "tile.poweradvantage.fluid_switch.name": "판막", - "tile.poweradvantage.converter_rf_steam.name": "증기-RF 전원 트랜스", - "tile.poweradvantage.converter_rf_electricity.name": "전기-RF 전원 트랜스", - "tile.poweradvantage.converter_rf_quantum.name": "양자-RF 전원 트랜스", - "tile.poweradvantage.converter_tr_electricity.name": "전기-EU 전원 트랜스", - "block.poweradvantage.fluid_drain": "드레인", - "block.poweradvantage.fluid_discharge": "방전 화격자", - "block.poweradvantage.fluid_storage_tank": "저장 탱크", - "item.poweradvantage.fluid_storage_tank_item": "는 저장 탱크를 ", - "block.poweradvantage.fluid_pipe": "파이프", - "block.poweradvantage.item_conveyor": "컨베이어", - "block.poweradvantage.item_filter_block": "블록 필터 컨베이어", - "block.poweradvantage.item_filter_food": "식품 필터 컨베이어", - "block.poweradvantage.item_filter_fuel": "연료 필터 컨베이어", - "block.poweradvantage.item_filter_inventory": "필터 컨베이어", - "block.poweradvantage.item_filter_ore": "광석 필터 컨베이어", - "block.poweradvantage.item_filter_plant": "물만 주면 식물이 필터 컨베이어", - "block.poweradvantage.item_filter_smelt": "노 필터 컨베이어", - "block.poweradvantage.item_filter_overflow": "오버 플로우필터 컨베이어", - "block.poweradvantage.steel_frame": "스틸 프레임", - "item.poweradvantage.starch": "전분", - "item.poweradvantage.bioplastic_ingot": "는 바이오 플라스틱 잉곳을 ", - "item.poweradvantage.sprocket": "스프로킷", - "block.poweradvantage.fluid_metal_tank": "금속 저장조", - "item.poweradvantage.rotator_tool": "회전 도구", - "block.poweradvantage.infinite_steam": "무한 증기 력", - "block.poweradvantage.infinite_electricity": "무한 전력", - "block.poweradvantage.infinite_quantum": "무한 양자 전력", - "block.poweradvantage.crude_oil": "원유", - "block.poweradvantage.still": "증류소", - "block.poweradvantage.refined_oil": "정유", - "block.poweradvantage.fluid_switch": "판막", - "block.poweradvantage.converter_rf_steam": "증기-RF 전원 트랜스", - "block.poweradvantage.converter_rf_electricity": "전기-RF 전원 트랜스", - "block.poweradvantage.converter_rf_quantum": "양자-RF 전원 트랜스", - "block.poweradvantage.converter_tr_electricity": "전기-EU 전원 트랜스", - "block.poweradvantage.fluid_pipe_terminal": "파이프" -} diff --git a/src/main/resources/assets/poweradvantage/lang/pt_br.json b/src/main/resources/assets/poweradvantage/lang/pt_br.json deleted file mode 100644 index f837618a..00000000 --- a/src/main/resources/assets/poweradvantage/lang/pt_br.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", - "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Forno Grande (no forma)", - "nei.poweradvantage.recipehandler.shapedfurnace.name": "Forno Grande (em forma)", - "tooltip.poweradvantage.empty": "vazio", - "tooltip.poweradvantage.buckets_of": "%x baldes de %y", - "itemGroup.poweradvantage": "Vantagem de Poder", - "tile.poweradvantage.fluid_drain.name": "Dreno", - "tile.poweradvantage.fluid_discharge.name": "Grate Discharge", - "tile.poweradvantage.fluid_storage_tank.name": "Tanque de armazenamento", - "item.poweradvantage.fluid_storage_tank_item.name": "Tanque de armazenamento", - "tile.poweradvantage.fluid_pipe.name": "Tubulação", - "tile.poweradvantage.item_conveyor.name": "Transportador", - "tile.poweradvantage.item_filter_block.name": "Bloquear filtro transportador", - "tile.poweradvantage.item_filter_food.name": "Transportador Filtro de Alimentos", - "tile.poweradvantage.item_filter_fuel.name": "Filtro de combustível Transportador", - "tile.poweradvantage.item_filter_inventory.name": "Transportador Filtro", - "tile.poweradvantage.item_filter_ore.name": "Ore Transportadora Filtro", - "tile.poweradvantage.item_filter_plant.name": "Plantable Transportadora Filtro", - "tile.poweradvantage.item_filter_smelt.name": "Fornalha Filtro Transportador", - "tile.poweradvantage.item_filter_overflow.name": "Transbordamento Transportador Filtro", - "tile.poweradvantage.steel_frame.name": "Armação de Aço", - "item.poweradvantage.starch.name": "Amido", - "item.poweradvantage.bioplastic_ingot.name": "Bioplastic lingote", - "item.poweradvantage.sprocket.name": "Roda Dentada", - "tile.poweradvantage.fluid_metal_tank.name": "Tanque de Armazenamento de Aço", - "item.poweradvantage.rotator_tool.name": "Ferramenta de Rotação", - "tile.poweradvantage.infinite_steam.name": "Energia a Vapor Infinito", - "tile.poweradvantage.infinite_electricity.name": "Energia Elétrica Infinito", - "tile.poweradvantage.infinite_quantum.name": "Energia Quantum Infinito", - "tile.poweradvantage.crude_oil.name": "Óleo Cru", - "fluid.poweradvantage.crude_oil": "Óleo Cru", - "item.bucket.crude_oil.name": "Balde de Óleo Cru", - "tile.poweradvantage.still.name": "Destilaria", - "tile.poweradvantage.refined_oil.name": "Óleo Refinado", - "fluid.poweradvantage.refined_oil": "Óleo Refinado", - "item.bucket.refined_oil.name": "Balde de Óleo Refinado", - "tile.poweradvantage.fluid_switch.name": "Válvula", - "tile.poweradvantage.converter_rf_steam.name": "Vapor-RF Transformador", - "tile.poweradvantage.converter_rf_electricity.name": "Electricidade-RF Transformador", - "tile.poweradvantage.converter_rf_quantum.name": "Quantidade-RF Transformador", - "tile.poweradvantage.converter_tr_electricity.name": "Electricidade-EU Transformador", - "block.poweradvantage.fluid_drain": "Dreno", - "block.poweradvantage.fluid_discharge": "Grate Discharge", - "block.poweradvantage.fluid_storage_tank": "Tanque de armazenamento", - "item.poweradvantage.fluid_storage_tank_item": "Tanque de armazenamento", - "block.poweradvantage.fluid_pipe": "Tubulação", - "block.poweradvantage.item_conveyor": "Transportador", - "block.poweradvantage.item_filter_block": "Bloquear filtro transportador", - "block.poweradvantage.item_filter_food": "Transportador Filtro de Alimentos", - "block.poweradvantage.item_filter_fuel": "Filtro de combustível Transportador", - "block.poweradvantage.item_filter_inventory": "Transportador Filtro", - "block.poweradvantage.item_filter_ore": "Ore Transportadora Filtro", - "block.poweradvantage.item_filter_plant": "Plantable Transportadora Filtro", - "block.poweradvantage.item_filter_smelt": "Fornalha Filtro Transportador", - "block.poweradvantage.item_filter_overflow": "Transbordamento Transportador Filtro", - "block.poweradvantage.steel_frame": "Armação de Aço", - "item.poweradvantage.starch": "Amido", - "item.poweradvantage.bioplastic_ingot": "Bioplastic lingote", - "item.poweradvantage.sprocket": "Roda Dentada", - "block.poweradvantage.fluid_metal_tank": "Tanque de Armazenamento de Aço", - "item.poweradvantage.rotator_tool": "Ferramenta de Rotação", - "block.poweradvantage.infinite_steam": "Energia a Vapor Infinito", - "block.poweradvantage.infinite_electricity": "Energia Elétrica Infinito", - "block.poweradvantage.infinite_quantum": "Energia Quantum Infinito", - "block.poweradvantage.crude_oil": "Óleo Cru", - "block.poweradvantage.still": "Destilaria", - "block.poweradvantage.refined_oil": "Óleo Refinado", - "block.poweradvantage.fluid_switch": "Válvula", - "block.poweradvantage.converter_rf_steam": "Vapor-RF Transformador", - "block.poweradvantage.converter_rf_electricity": "Electricidade-RF Transformador", - "block.poweradvantage.converter_rf_quantum": "Quantidade-RF Transformador", - "block.poweradvantage.converter_tr_electricity": "Electricidade-EU Transformador", - "block.poweradvantage.fluid_pipe_terminal": "Tubulação" -} diff --git a/src/main/resources/assets/poweradvantage/lang/zh_cn.json b/src/main/resources/assets/poweradvantage/lang/zh_cn.json deleted file mode 100644 index b8c066bb..00000000 --- a/src/main/resources/assets/poweradvantage/lang/zh_cn.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)", - "nei.poweradvantage.recipehandler.shapelessfurnace.name": "大熔炉(无形的)", - "nei.poweradvantage.recipehandler.shapedfurnace.name": "大熔炉(有形的)", - "tooltip.poweradvantage.empty": "空", - "tooltip.poweradvantage.buckets_of": "%x水桶的%y", - "itemGroup.poweradvantage": "能量优势", - "tile.poweradvantage.fluid_drain.name": "抽水机", - "tile.poweradvantage.fluid_discharge.name": "排液器", - "tile.poweradvantage.fluid_storage_tank.name": "便携式液体储罐", - "item.poweradvantage.fluid_storage_tank_item.name": "便携式液体储罐", - "tile.poweradvantage.fluid_pipe.name": "液体管", - "tile.poweradvantage.item_conveyor.name": "输送机", - "tile.poweradvantage.item_filter_block.name": "方块过滤输送机", - "tile.poweradvantage.item_filter_food.name": "食品过滤输送机", - "tile.poweradvantage.item_filter_fuel.name": "燃料过滤输送机", - "tile.poweradvantage.item_filter_inventory.name": "选择过滤输送机", - "tile.poweradvantage.item_filter_ore.name": "矿物过滤输送机", - "tile.poweradvantage.item_filter_plant.name": "种子过滤输送机", - "tile.poweradvantage.item_filter_smelt.name": "熔炉过滤输送机", - "tile.poweradvantage.item_filter_overflow.name": "溢出过滤输送机", - "tile.poweradvantage.steel_frame.name": "钢框架", - "item.poweradvantage.starch.name": "淀粉", - "item.poweradvantage.bioplastic_ingot.name": "生物塑料锭", - "item.poweradvantage.sprocket.name": "链轮", - "tile.poweradvantage.fluid_metal_tank.name": "金属液罐", - "item.poweradvantage.rotator_tool.name": "旋转扳手", - "tile.poweradvantage.infinite_steam.name": "无限蒸汽动力", - "tile.poweradvantage.infinite_electricity.name": "无限电力", - "tile.poweradvantage.infinite_quantum.name": "无限量子动力", - "tile.poweradvantage.crude_oil.name": "原油", - "fluid.poweradvantage.crude_oil": "原油", - "item.bucket.crude_oil.name": "原油桶", - "tile.poweradvantage.still.name": "酒廠", - "tile.poweradvantage.refined_oil.name": "成品油", - "fluid.poweradvantage.refined_oil": "成品油", - "item.bucket.refined_oil.name": "成品油的桶", - "tile.poweradvantage.fluid_switch.name": "閥", - "tile.poweradvantage.converter_rf_steam.name": "蒸汽-RF 變壓器", - "tile.poweradvantage.converter_rf_electricity.name": "電力-RF 變壓器", - "tile.poweradvantage.converter_rf_quantum.name": "量子-RF 變壓器", - "tile.poweradvantage.converter_tr_electricity.name": "電力-EU 變壓器", - "block.poweradvantage.fluid_drain": "抽水机", - "block.poweradvantage.fluid_discharge": "排液器", - "block.poweradvantage.fluid_storage_tank": "便携式液体储罐", - "item.poweradvantage.fluid_storage_tank_item": "便携式液体储罐", - "block.poweradvantage.fluid_pipe": "液体管", - "block.poweradvantage.item_conveyor": "输送机", - "block.poweradvantage.item_filter_block": "方块过滤输送机", - "block.poweradvantage.item_filter_food": "食品过滤输送机", - "block.poweradvantage.item_filter_fuel": "燃料过滤输送机", - "block.poweradvantage.item_filter_inventory": "选择过滤输送机", - "block.poweradvantage.item_filter_ore": "矿物过滤输送机", - "block.poweradvantage.item_filter_plant": "种子过滤输送机", - "block.poweradvantage.item_filter_smelt": "熔炉过滤输送机", - "block.poweradvantage.item_filter_overflow": "溢出过滤输送机", - "block.poweradvantage.steel_frame": "钢框架", - "item.poweradvantage.starch": "淀粉", - "item.poweradvantage.bioplastic_ingot": "生物塑料锭", - "item.poweradvantage.sprocket": "链轮", - "block.poweradvantage.fluid_metal_tank": "金属液罐", - "item.poweradvantage.rotator_tool": "旋转扳手", - "block.poweradvantage.infinite_steam": "无限蒸汽动力", - "block.poweradvantage.infinite_electricity": "无限电力", - "block.poweradvantage.infinite_quantum": "无限量子动力", - "block.poweradvantage.crude_oil": "原油", - "block.poweradvantage.still": "酒廠", - "block.poweradvantage.refined_oil": "成品油", - "block.poweradvantage.fluid_switch": "閥", - "block.poweradvantage.converter_rf_steam": "蒸汽-RF 變壓器", - "block.poweradvantage.converter_rf_electricity": "電力-RF 變壓器", - "block.poweradvantage.converter_rf_quantum": "量子-RF 變壓器", - "block.poweradvantage.converter_tr_electricity": "電力-EU 變壓器", - "block.poweradvantage.fluid_pipe_terminal": "液体管" -} diff --git a/src/main/resources/assets/poweradvantage/models/block/converter_tr_electricity.json b/src/main/resources/assets/poweradvantage/models/block/converter_tr_electricity.json deleted file mode 100644 index 7dff7e5a..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/converter_tr_electricity.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "poweradvantage:blocks/techreborn_converter" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_discharge.json b/src/main/resources/assets/poweradvantage/models/block/fluid_discharge.json deleted file mode 100644 index df0fc23e..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_discharge.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "parent": "block/cube_bottom_top", - "textures": { - "top": "poweradvantage:blocks/fluid_discharge_side", - "bottom": "poweradvantage:blocks/fluid_discharge_face", - "side": "poweradvantage:blocks/fluid_discharge_side" - } -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_drain.json b/src/main/resources/assets/poweradvantage/models/block/fluid_drain.json deleted file mode 100644 index 93445c16..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_drain.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "parent": "block/cube_bottom_top", - "textures": { - "top": "poweradvantage:blocks/fluid_drain_face", - "bottom": "poweradvantage:blocks/fluid_drain_side", - "side": "poweradvantage:blocks/fluid_drain_side" - } -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_0.json b/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_0.json deleted file mode 100644 index 25307680..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_0.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "parent": "poweradvantage:block/pipe4_0", - "textures": { - "texture": "poweradvantage:blocks/fluid_pipe", - "particle": "poweradvantage:materials/iron" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_densuw.json b/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_densuw.json deleted file mode 100644 index b27c5e3b..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_densuw.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "parent": "poweradvantage:block/pipe4_densuw", - "textures": { - "texture": "poweradvantage:blocks/fluid_pipe", - "particle": "poweradvantage:materials/iron" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_desuw.json b/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_desuw.json deleted file mode 100644 index b28fbf1a..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_desuw.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "parent": "poweradvantage:block/pipe4_desuw", - "textures": { - "texture": "poweradvantage:blocks/fluid_pipe", - "particle": "poweradvantage:materials/iron" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_dnu.json b/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_dnu.json deleted file mode 100644 index 58ad993c..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_dnu.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "parent": "poweradvantage:block/pipe4_dnu", - "textures": { - "texture": "poweradvantage:blocks/fluid_pipe", - "particle": "poweradvantage:materials/iron" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_ensw.json b/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_ensw.json deleted file mode 100644 index f909d919..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_ensw.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "parent": "poweradvantage:block/pipe4_ensw", - "textures": { - "texture": "poweradvantage:blocks/fluid_pipe", - "particle": "poweradvantage:materials/iron" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_n.json b/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_n.json deleted file mode 100644 index 549fa775..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_n.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "parent": "poweradvantage:block/pipe4_n", - "textures": { - "texture": "poweradvantage:blocks/fluid_pipe", - "particle": "poweradvantage:materials/iron" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_ns.json b/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_ns.json deleted file mode 100644 index 46b2b21e..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_ns.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "parent": "poweradvantage:block/pipe4_ns", - "textures": { - "texture": "poweradvantage:blocks/fluid_pipe", - "particle": "poweradvantage:materials/iron" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_nsuw.json b/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_nsuw.json deleted file mode 100644 index a2efd485..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_nsuw.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "parent": "poweradvantage:block/pipe4_nsuw", - "textures": { - "texture": "poweradvantage:blocks/fluid_pipe", - "particle": "poweradvantage:materials/iron" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_nsw.json b/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_nsw.json deleted file mode 100644 index a4d12d0a..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_nsw.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "parent": "poweradvantage:block/pipe4_nsw", - "textures": { - "texture": "poweradvantage:blocks/fluid_pipe", - "particle": "poweradvantage:materials/iron" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_nuw.json b/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_nuw.json deleted file mode 100644 index 2c6730c8..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_nuw.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "parent": "poweradvantage:block/pipe4_nuw", - "textures": { - "texture": "poweradvantage:blocks/fluid_pipe", - "particle": "poweradvantage:materials/iron" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_nw.json b/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_nw.json deleted file mode 100644 index 03097f73..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_pipe_nw.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "parent": "poweradvantage:block/pipe4_nw", - "textures": { - "texture": "poweradvantage:blocks/fluid_pipe", - "particle": "poweradvantage:materials/iron" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_switch_off.json b/src/main/resources/assets/poweradvantage/models/block/fluid_switch_off.json deleted file mode 100644 index 99376fe2..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_switch_off.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "poweradvantage:blocks/fluid_switch_off", - "particle": "poweradvantage:materials/steel" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/fluid_switch_on.json b/src/main/resources/assets/poweradvantage/models/block/fluid_switch_on.json deleted file mode 100644 index a09edb76..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/fluid_switch_on.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "poweradvantage:blocks/fluid_switch_on", - "particle": "poweradvantage:materials/steel" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/item_conveyor.json b/src/main/resources/assets/poweradvantage/models/block/item_conveyor.json deleted file mode 100644 index f176d901..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/item_conveyor.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "parent": "poweradvantage:block/conveyor_base", - "textures": { - "top": "poweradvantage:blocks/item_conveyor_top", - "bottom": "poweradvantage:blocks/item_conveyor_top", - "side": "poweradvantage:blocks/item_conveyor_side", - "end": "poweradvantage:blocks/item_conveyor_end", - "rim": "poweradvantage:materials/steel" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/item_filter_block.json b/src/main/resources/assets/poweradvantage/models/block/item_filter_block.json deleted file mode 100644 index 0d2c3a62..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/item_filter_block.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "parent": "poweradvantage:block/conveyor_base", - "textures": { - "top": "poweradvantage:blocks/item_conveyor_top", - "bottom": "poweradvantage:blocks/filter_bottom", - "side": "poweradvantage:blocks/filter_side_block", - "end": "poweradvantage:blocks/item_conveyor_end", - "rim": "poweradvantage:materials/steel" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/item_filter_food.json b/src/main/resources/assets/poweradvantage/models/block/item_filter_food.json deleted file mode 100644 index 2c1f6589..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/item_filter_food.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "parent": "poweradvantage:block/conveyor_base", - "textures": { - "top": "poweradvantage:blocks/item_conveyor_top", - "bottom": "poweradvantage:blocks/filter_bottom", - "side": "poweradvantage:blocks/filter_side_food", - "end": "poweradvantage:blocks/item_conveyor_end", - "rim": "poweradvantage:materials/steel" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/item_filter_fuel.json b/src/main/resources/assets/poweradvantage/models/block/item_filter_fuel.json deleted file mode 100644 index d12bdefb..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/item_filter_fuel.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "parent": "poweradvantage:block/conveyor_base", - "textures": { - "top": "poweradvantage:blocks/item_conveyor_top", - "bottom": "poweradvantage:blocks/filter_bottom", - "side": "poweradvantage:blocks/filter_side_fuel", - "end": "poweradvantage:blocks/item_conveyor_end", - "rim": "poweradvantage:materials/steel" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/item_filter_inventory.json b/src/main/resources/assets/poweradvantage/models/block/item_filter_inventory.json deleted file mode 100644 index 45200ce2..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/item_filter_inventory.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "parent": "poweradvantage:block/conveyor_base", - "textures": { - "top": "poweradvantage:blocks/item_conveyor_top", - "bottom": "poweradvantage:blocks/filter_bottom", - "side": "poweradvantage:blocks/filter_side_inventory", - "end": "poweradvantage:blocks/item_conveyor_end", - "rim": "poweradvantage:materials/steel" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/item_filter_ore.json b/src/main/resources/assets/poweradvantage/models/block/item_filter_ore.json deleted file mode 100644 index 26dbe01c..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/item_filter_ore.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "parent": "poweradvantage:block/conveyor_base", - "textures": { - "top": "poweradvantage:blocks/item_conveyor_top", - "bottom": "poweradvantage:blocks/filter_bottom", - "side": "poweradvantage:blocks/filter_side_ore", - "end": "poweradvantage:blocks/item_conveyor_end", - "rim": "poweradvantage:materials/steel" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/item_filter_overflow.json b/src/main/resources/assets/poweradvantage/models/block/item_filter_overflow.json deleted file mode 100644 index 8abd3a4b..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/item_filter_overflow.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "parent": "poweradvantage:block/conveyor_base", - "textures": { - "top": "poweradvantage:blocks/item_conveyor_top", - "bottom": "poweradvantage:blocks/filter_bottom", - "side": "poweradvantage:blocks/filter_side_overflow", - "end": "poweradvantage:blocks/item_conveyor_end", - "rim": "poweradvantage:materials/steel" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/item_filter_plant.json b/src/main/resources/assets/poweradvantage/models/block/item_filter_plant.json deleted file mode 100644 index 4d16d31e..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/item_filter_plant.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "parent": "poweradvantage:block/conveyor_base", - "textures": { - "top": "poweradvantage:blocks/item_conveyor_top", - "bottom": "poweradvantage:blocks/filter_bottom", - "side": "poweradvantage:blocks/filter_side_plantable", - "end": "poweradvantage:blocks/item_conveyor_end", - "rim": "poweradvantage:materials/steel" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/block/item_filter_smelt.json b/src/main/resources/assets/poweradvantage/models/block/item_filter_smelt.json deleted file mode 100644 index 49deac1d..00000000 --- a/src/main/resources/assets/poweradvantage/models/block/item_filter_smelt.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "parent": "poweradvantage:block/conveyor_base", - "textures": { - "top": "poweradvantage:blocks/item_conveyor_top", - "bottom": "poweradvantage:blocks/filter_bottom", - "side": "poweradvantage:blocks/filter_side_smeltable", - "end": "poweradvantage:blocks/item_conveyor_end", - "rim": "poweradvantage:materials/steel" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/item/bioplastic_ingot.json b/src/main/resources/assets/poweradvantage/models/item/bioplastic_ingot.json deleted file mode 100644 index 8c08c41c..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/bioplastic_ingot.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "item/generated", - "textures": { - "layer0": "poweradvantage:items/bioplastic_ingot" - } -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/bucket_crude_oil.json b/src/main/resources/assets/poweradvantage/models/item/bucket_crude_oil.json deleted file mode 100644 index 2d783539..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/bucket_crude_oil.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "item/generated", - "textures": { - "layer0": "poweradvantage:items/bucket_crude_oil" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/item/bucket_refined_oil.json b/src/main/resources/assets/poweradvantage/models/item/bucket_refined_oil.json deleted file mode 100644 index eb646820..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/bucket_refined_oil.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "item/generated", - "textures": { - "layer0": "poweradvantage:items/bucket_refined_oil" - } -} diff --git a/src/main/resources/assets/poweradvantage/models/item/converter_rf_electricity.json b/src/main/resources/assets/poweradvantage/models/item/converter_rf_electricity.json deleted file mode 100644 index b9b04298..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/converter_rf_electricity.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/converter_rf_electricity" -} diff --git a/src/main/resources/assets/poweradvantage/models/item/converter_rf_quantum.json b/src/main/resources/assets/poweradvantage/models/item/converter_rf_quantum.json deleted file mode 100644 index ca2c0b82..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/converter_rf_quantum.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/converter_rf_quantum" -} diff --git a/src/main/resources/assets/poweradvantage/models/item/converter_rf_steam.json b/src/main/resources/assets/poweradvantage/models/item/converter_rf_steam.json deleted file mode 100644 index 1ca73e42..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/converter_rf_steam.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/converter_rf_steam" -} diff --git a/src/main/resources/assets/poweradvantage/models/item/converter_tr_electricity.json b/src/main/resources/assets/poweradvantage/models/item/converter_tr_electricity.json deleted file mode 100644 index 9a7a15f8..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/converter_tr_electricity.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/converter_tr_electricity" -} diff --git a/src/main/resources/assets/poweradvantage/models/item/crude_oil.json b/src/main/resources/assets/poweradvantage/models/item/crude_oil.json deleted file mode 100644 index 79fdb9e9..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/crude_oil.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:item/crude_oil_bucket" -} diff --git a/src/main/resources/assets/poweradvantage/models/item/fluid_discharge.json b/src/main/resources/assets/poweradvantage/models/item/fluid_discharge.json deleted file mode 100644 index e0bcaeee..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/fluid_discharge.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/fluid_discharge" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/fluid_drain.json b/src/main/resources/assets/poweradvantage/models/item/fluid_drain.json deleted file mode 100644 index fc987457..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/fluid_drain.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/fluid_drain" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/fluid_metal_tank.json b/src/main/resources/assets/poweradvantage/models/item/fluid_metal_tank.json deleted file mode 100644 index 97ecaa98..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/fluid_metal_tank.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/fluid_metal_tank" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/fluid_pipe.json b/src/main/resources/assets/poweradvantage/models/item/fluid_pipe.json deleted file mode 100644 index 097b7358..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/fluid_pipe.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "parent": "poweradvantage:block/fluid_pipe_ns", - "ambientocclusion": false -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/fluid_pipe_terminal.json b/src/main/resources/assets/poweradvantage/models/item/fluid_pipe_terminal.json deleted file mode 100644 index 097b7358..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/fluid_pipe_terminal.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "parent": "poweradvantage:block/fluid_pipe_ns", - "ambientocclusion": false -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/fluid_storage_tank.json b/src/main/resources/assets/poweradvantage/models/item/fluid_storage_tank.json deleted file mode 100644 index a337b200..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/fluid_storage_tank.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "parent": "poweradvantage:block/fluid_storage_tank", - "ambientocclusion": false -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/fluid_switch.json b/src/main/resources/assets/poweradvantage/models/item/fluid_switch.json deleted file mode 100644 index f00c5426..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/fluid_switch.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/fluid_switch_on" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/infinite_electricity.json b/src/main/resources/assets/poweradvantage/models/item/infinite_electricity.json deleted file mode 100644 index 5b216c85..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/infinite_electricity.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/infinite_electricity" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/infinite_quantum.json b/src/main/resources/assets/poweradvantage/models/item/infinite_quantum.json deleted file mode 100644 index 03aab32c..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/infinite_quantum.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/infinite_quantum" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/infinite_steam.json b/src/main/resources/assets/poweradvantage/models/item/infinite_steam.json deleted file mode 100644 index 231eb2ca..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/infinite_steam.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/infinite_steam" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/item_conveyor.json b/src/main/resources/assets/poweradvantage/models/item/item_conveyor.json deleted file mode 100644 index 4fa32a5d..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/item_conveyor.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/item_conveyor" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/item_filter_block.json b/src/main/resources/assets/poweradvantage/models/item/item_filter_block.json deleted file mode 100644 index 0e5aca23..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/item_filter_block.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/item_filter_block" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/item_filter_food.json b/src/main/resources/assets/poweradvantage/models/item/item_filter_food.json deleted file mode 100644 index f238e136..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/item_filter_food.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/item_filter_food" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/item_filter_fuel.json b/src/main/resources/assets/poweradvantage/models/item/item_filter_fuel.json deleted file mode 100644 index 4fb288b6..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/item_filter_fuel.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/item_filter_fuel" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/item_filter_inventory.json b/src/main/resources/assets/poweradvantage/models/item/item_filter_inventory.json deleted file mode 100644 index 1e3a7703..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/item_filter_inventory.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/item_filter_inventory" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/item_filter_ore.json b/src/main/resources/assets/poweradvantage/models/item/item_filter_ore.json deleted file mode 100644 index ba5c859c..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/item_filter_ore.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/item_filter_ore" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/item_filter_overflow.json b/src/main/resources/assets/poweradvantage/models/item/item_filter_overflow.json deleted file mode 100644 index 92ae073c..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/item_filter_overflow.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/item_filter_overflow" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/item_filter_plant.json b/src/main/resources/assets/poweradvantage/models/item/item_filter_plant.json deleted file mode 100644 index 74d98116..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/item_filter_plant.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/item_filter_plant" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/item_filter_smelt.json b/src/main/resources/assets/poweradvantage/models/item/item_filter_smelt.json deleted file mode 100644 index d4f0bc20..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/item_filter_smelt.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/item_filter_smelt" -} \ No newline at end of file diff --git a/src/main/resources/assets/poweradvantage/models/item/refined_oil.json b/src/main/resources/assets/poweradvantage/models/item/refined_oil.json deleted file mode 100644 index 0a841a81..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/refined_oil.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:item/refined_oil_bucket" -} diff --git a/src/main/resources/assets/poweradvantage/models/item/steel_frame.json b/src/main/resources/assets/poweradvantage/models/item/steel_frame.json deleted file mode 100644 index e87a33bb..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/steel_frame.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/steel_frame" -} diff --git a/src/main/resources/assets/poweradvantage/models/item/still.json b/src/main/resources/assets/poweradvantage/models/item/still.json deleted file mode 100644 index e70ea2ae..00000000 --- a/src/main/resources/assets/poweradvantage/models/item/still.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "parent": "poweradvantage:block/still_off" -} \ No newline at end of file diff --git a/src/main/resources/coremods/poweradvantage_116_migration.js b/src/main/resources/coremods/poweradvantage_116_migration.js index f3093dcc..a0c0b935 100644 --- a/src/main/resources/coremods/poweradvantage_116_migration.js +++ b/src/main/resources/coremods/poweradvantage_116_migration.js @@ -6,13 +6,13 @@ 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'); -// Only the pre-datafix migration needs bytecode integration on Forge 1.16. -var hook = 'zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook'; +// Only pre-datafix migration needs bytecode integration on Forge 1.17. +var hook = 'zone/moddev/mc/basepower/migration/LegacyWorldDataHook'; function patchPlayerDataFix(classNode) { var descriptor = '(Lcom/mojang/datafixers/DataFixer;' + - 'Lnet/minecraft/util/datafix/DefaultTypeReferences;' + - 'Lnet/minecraft/nbt/CompoundNBT;I)Lnet/minecraft/nbt/CompoundNBT;'; + 'Lnet/minecraft/util/datafix/DataFixTypes;' + + 'Lnet/minecraft/nbt/CompoundTag;I)Lnet/minecraft/nbt/CompoundTag;'; var matches = 0; var matchedMethod = null; var matchedCall = null; @@ -21,7 +21,7 @@ function patchPlayerDataFix(classNode) { for (var instruction = method.instructions.getFirst(); instruction !== null; instruction = instruction.getNext()) { if (instruction.getOpcode() === Opcodes.INVOKESTATIC && - instruction.owner === 'net/minecraft/nbt/NBTUtil' && instruction.desc === descriptor) { + instruction.owner === 'net/minecraft/nbt/NbtUtils' && instruction.desc === descriptor) { ++matches; matchedMethod = method; matchedCall = instruction; @@ -32,60 +32,81 @@ function patchPlayerDataFix(classNode) { throw new Error('Power Advantage requires exactly one native player datafix call in ' + classNode.name); } matchedMethod.instructions.set(matchedCall, new MethodInsnNode(Opcodes.INVOKESTATIC, - 'zone/moddev/mc/poweradvantage/migration/LegacyPlayerDataFixes', 'update', descriptor, false)); + 'zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes', 'update', descriptor, false)); return classNode; } function initializeCoreMod() { return { 'poweradvantage_legacy_player_file': { - 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.storage.PlayerData' }, + 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.level.storage.PlayerDataStorage' }, 'transformer': patchPlayerDataFix }, 'poweradvantage_legacy_embedded_player': { - 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.storage.ServerWorldInfo' }, + 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.level.storage.PrimaryLevelData' }, 'transformer': patchPlayerDataFix }, 'poweradvantage_legacy_world_info': { - 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.storage.SaveFormat' }, + 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.level.storage.LevelStorageSource' }, 'transformer': function(classNode) { - var patched = false; + var worldReaders = 0; + var packReaders = 0; for (var i = 0; i < classNode.methods.size(); ++i) { var method = classNode.methods.get(i); - if (method.desc !== '(Lcom/mojang/serialization/DynamicOps;' + - 'Lnet/minecraft/util/datafix/codec/DatapackCodec;' + - 'Lnet/minecraft/world/storage/SaveFormat$LevelSave;' + + var worldReader = method.desc === '(Lcom/mojang/serialization/DynamicOps;' + + 'Lnet/minecraft/world/level/DataPackConfig;' + + 'Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;' + 'Ljava/io/File;Lcom/mojang/datafixers/DataFixer;)' + - 'Lnet/minecraft/world/storage/ServerWorldInfo;') continue; - var prefix = new InsnList(); - prefix.add(new VarInsnNode(Opcodes.ALOAD, 3)); - prefix.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, - 'prepareLegacyWorld', '(Ljava/io/File;)V', false)); - method.instructions.insert(prefix); - patched = true; + 'Lnet/minecraft/world/level/storage/PrimaryLevelData;'; + var packReader = method.desc === '(Ljava/io/File;Lcom/mojang/datafixers/DataFixer;)' + + 'Lnet/minecraft/world/level/DataPackConfig;'; + if (!worldReader && !packReader) continue; + if (worldReader) { + var prefix = new InsnList(); + prefix.add(new VarInsnNode(Opcodes.ALOAD, 3)); + prefix.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, + 'prepareLegacyWorld', '(Ljava/io/File;)V', false)); + method.instructions.insert(prefix); + } + 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; + ++reads; + } + } + if (reads !== 1) throw new Error('Power Advantage expected one level metadata read'); + if (worldReader) ++worldReaders; + if (packReader) ++packReaders; + } + if (worldReaders !== 1 || packReaders !== 1) { + throw new Error('Power Advantage could not attach both legacy level readers'); } - if (!patched) throw new Error('Power Advantage could not attach its legacy world reader'); return classNode; } }, 'poweradvantage_legacy_chunk_status': { - 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.chunk.storage.ChunkLoader' }, + 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.level.chunk.storage.ChunkStorage' }, 'transformer': function(classNode) { - var descriptor = '(Lnet/minecraft/util/RegistryKey;' + - 'Ljava/util/function/Supplier;Lnet/minecraft/nbt/CompoundNBT;)' + - 'Lnet/minecraft/nbt/CompoundNBT;'; + var descriptor = '(Lnet/minecraft/resources/ResourceKey;' + + 'Ljava/util/function/Supplier;Lnet/minecraft/nbt/CompoundTag;)' + + 'Lnet/minecraft/nbt/CompoundTag;'; var patchedRead = false; var patchedReturn = false; var patchedOwnership = false; var patchedEntities = false; for (var i = 0; i < classNode.methods.size(); ++i) { var method = classNode.methods.get(i); - if (method.desc === '(Lnet/minecraft/util/math/ChunkPos;)Lnet/minecraft/nbt/CompoundNBT;') { + if (method.desc === '(Lnet/minecraft/world/level/ChunkPos;)Lnet/minecraft/nbt/CompoundTag;') { for (var read = method.instructions.getFirst(); read !== null; read = read.getNext()) { if (read.getOpcode() === Opcodes.ARETURN) { method.instructions.insertBefore(read, new MethodInsnNode(Opcodes.INVOKESTATIC, hook, 'copyChunkForMigration', - '(Lnet/minecraft/nbt/CompoundNBT;)Lnet/minecraft/nbt/CompoundNBT;', false)); + '(Lnet/minecraft/nbt/CompoundTag;)Lnet/minecraft/nbt/CompoundTag;', false)); patchedOwnership = true; } } @@ -95,11 +116,11 @@ function initializeCoreMod() { // hooks so modded item durability is rescued before entity flattening. for (var first = method.instructions.getFirst(); first !== null; first = first.getNext()) { if (first.getOpcode() === Opcodes.INVOKESTATIC && first.owner === classNode.name && - first.desc === '(Lnet/minecraft/nbt/CompoundNBT;)I') { + first.desc === '(Lnet/minecraft/nbt/CompoundTag;)I') { var entities = new InsnList(); entities.add(new VarInsnNode(Opcodes.ALOAD, 3)); entities.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, - 'prepareLegacyEntities', '(Lnet/minecraft/nbt/CompoundNBT;)V', false)); + 'prepareLegacyEntities', '(Lnet/minecraft/nbt/CompoundTag;)V', false)); method.instructions.insertBefore(first, entities); patchedEntities = true; break; @@ -108,7 +129,7 @@ function initializeCoreMod() { var prepare = new InsnList(); prepare.add(new VarInsnNode(Opcodes.ALOAD, 3)); prepare.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, - 'prepareLegacyChunk', '(Lnet/minecraft/nbt/CompoundNBT;)V', false)); + 'prepareLegacyChunk', '(Lnet/minecraft/nbt/CompoundTag;)V', false)); method.instructions.insert(prepare); patchedRead = true; for (var instruction = method.instructions.getFirst(); instruction !== null; @@ -116,7 +137,7 @@ function initializeCoreMod() { if (instruction.getOpcode() === Opcodes.ARETURN) { method.instructions.insertBefore(instruction, new MethodInsnNode(Opcodes.INVOKESTATIC, hook, 'finalizeLegacyChunk', - '(Lnet/minecraft/nbt/CompoundNBT;)Lnet/minecraft/nbt/CompoundNBT;', false)); + '(Lnet/minecraft/nbt/CompoundTag;)Lnet/minecraft/nbt/CompoundTag;', false)); patchedReturn = true; } } @@ -127,11 +148,38 @@ function initializeCoreMod() { return classNode; } }, + 'poweradvantage_separate_entity_storage': { + 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.level.chunk.storage.EntityStorage' }, + 'transformer': function(classNode) { + var matches = 0; + var descriptor = '(Lnet/minecraft/nbt/CompoundTag;)Lnet/minecraft/nbt/CompoundTag;'; + for (var i = 0; i < classNode.methods.size(); ++i) { + var method = classNode.methods.get(i); + if (method.desc !== descriptor || (method.access & Opcodes.ACC_STATIC) !== 0) continue; + ++matches; + var prefix = new InsnList(); + prefix.add(new VarInsnNode(Opcodes.ALOAD, 1)); + prefix.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, + 'prepareEntityChunkForMigration', descriptor, false)); + prefix.add(new VarInsnNode(Opcodes.ASTORE, 1)); + method.instructions.insert(prefix); + for (var instruction = method.instructions.getFirst(); instruction !== null; + instruction = instruction.getNext()) { + if (instruction.getOpcode() === Opcodes.ARETURN) { + method.instructions.insertBefore(instruction, new MethodInsnNode(Opcodes.INVOKESTATIC, + hook, 'finalizeLegacyChunk', descriptor, false)); + } + } + } + if (matches !== 1) throw new Error('Power Advantage requires exactly one native entity-chunk converter'); + return classNode; + } + }, 'poweradvantage_legacy_worldgen_guard': { - 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.gen.WorldGenRegion' }, + 'target': { 'type': 'CLASS', 'name': 'net.minecraft.server.level.WorldGenRegion' }, 'transformer': function(classNode) { - var descriptor = '(Lnet/minecraft/util/math/BlockPos;' + - 'Lnet/minecraft/block/BlockState;II)Z'; + var descriptor = '(Lnet/minecraft/core/BlockPos;' + + 'Lnet/minecraft/world/level/block/state/BlockState;II)Z'; var patched = false; for (var i = 0; i < classNode.methods.size(); ++i) { var method = classNode.methods.get(i); @@ -140,7 +188,7 @@ function initializeCoreMod() { var prefix = new InsnList(); prefix.add(new VarInsnNode(Opcodes.ALOAD, 1)); prefix.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, - 'shouldBlockWorldgenWrite', '(Lnet/minecraft/util/math/BlockPos;)Z', false)); + 'shouldBlockWorldgenWrite', '(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/data/poweradvantage/advancements/recipes/bioplastic_from_starch.json b/src/main/resources/data/basepower/advancements/recipes/bioplastic_from_starch.json similarity index 73% rename from src/main/resources/data/poweradvantage/advancements/recipes/bioplastic_from_starch.json rename to src/main/resources/data/basepower/advancements/recipes/bioplastic_from_starch.json index b12696d4..c77b90f0 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/bioplastic_from_starch.json +++ b/src/main/resources/data/basepower/advancements/recipes/bioplastic_from_starch.json @@ -6,7 +6,9 @@ "conditions": { "items": [ { - "item": "poweradvantage:starch" + "items": [ + "basepower:starch" + ] } ] } @@ -14,7 +16,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:bioplastic_from_starch" + "recipe": "basepower:bioplastic_from_starch" } } }, @@ -26,7 +28,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:bioplastic_from_starch" + "basepower:bioplastic_from_starch" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_electricity.json b/src/main/resources/data/basepower/advancements/recipes/converter_rf_electricity.json similarity index 83% rename from src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_electricity.json rename to src/main/resources/data/basepower/advancements/recipes/converter_rf_electricity.json index fdb15ac4..29e395bc 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_electricity.json +++ b/src/main/resources/data/basepower/advancements/recipes/converter_rf_electricity.json @@ -26,7 +26,9 @@ "conditions": { "items": [ { - "item": "minecraft:redstone_block" + "items": [ + "minecraft:redstone_block" + ] } ] } @@ -34,7 +36,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:converter_rf_electricity" + "recipe": "basepower:converter_rf_electricity" } } }, @@ -48,7 +50,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:converter_rf_electricity" + "basepower:converter_rf_electricity" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_quantum.json b/src/main/resources/data/basepower/advancements/recipes/converter_rf_quantum.json similarity index 78% rename from src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_quantum.json rename to src/main/resources/data/basepower/advancements/recipes/converter_rf_quantum.json index 3e740797..09df8231 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_quantum.json +++ b/src/main/resources/data/basepower/advancements/recipes/converter_rf_quantum.json @@ -6,7 +6,9 @@ "conditions": { "items": [ { - "item": "minecraft:ender_pearl" + "items": [ + "minecraft:ender_pearl" + ] } ] } @@ -26,7 +28,9 @@ "conditions": { "items": [ { - "item": "minecraft:redstone_block" + "items": [ + "minecraft:redstone_block" + ] } ] } @@ -34,7 +38,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:converter_rf_quantum" + "recipe": "basepower:converter_rf_quantum" } } }, @@ -48,7 +52,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:converter_rf_quantum" + "basepower:converter_rf_quantum" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_steam.json b/src/main/resources/data/basepower/advancements/recipes/converter_rf_steam.json similarity index 84% rename from src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_steam.json rename to src/main/resources/data/basepower/advancements/recipes/converter_rf_steam.json index 990200c0..7538dfad 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_steam.json +++ b/src/main/resources/data/basepower/advancements/recipes/converter_rf_steam.json @@ -26,7 +26,9 @@ "conditions": { "items": [ { - "item": "minecraft:redstone_block" + "items": [ + "minecraft:redstone_block" + ] } ] } @@ -34,7 +36,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:converter_rf_steam" + "recipe": "basepower:converter_rf_steam" } } }, @@ -48,7 +50,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:converter_rf_steam" + "basepower:converter_rf_steam" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/converter_tr_electricity_iron.json b/src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_iron.json similarity index 86% rename from src/main/resources/data/poweradvantage/advancements/recipes/converter_tr_electricity_iron.json rename to src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_iron.json index bd7f8c97..cc616be6 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/converter_tr_electricity_iron.json +++ b/src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_iron.json @@ -24,7 +24,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:converter_tr_electricity_iron" + "recipe": "basepower:converter_tr_electricity_iron" } } }, @@ -37,7 +37,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:converter_tr_electricity_iron" + "basepower:converter_tr_electricity_iron" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/converter_tr_electricity_refined_iron.json b/src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_refined_iron.json similarity index 84% rename from src/main/resources/data/poweradvantage/advancements/recipes/converter_tr_electricity_refined_iron.json rename to src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_refined_iron.json index e8ad9a09..9ae4e5f7 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/converter_tr_electricity_refined_iron.json +++ b/src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_refined_iron.json @@ -24,7 +24,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:converter_tr_electricity_refined_iron" + "recipe": "basepower:converter_tr_electricity_refined_iron" } } }, @@ -37,7 +37,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:converter_tr_electricity_refined_iron" + "basepower:converter_tr_electricity_refined_iron" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_discharge.json b/src/main/resources/data/basepower/advancements/recipes/fluid_discharge.json similarity index 92% rename from src/main/resources/data/poweradvantage/advancements/recipes/fluid_discharge.json rename to src/main/resources/data/basepower/advancements/recipes/fluid_discharge.json index 92d9a799..6eee6ff0 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_discharge.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_discharge.json @@ -44,7 +44,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:fluid_discharge" + "recipe": "basepower:fluid_discharge" } } }, @@ -59,7 +59,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:fluid_discharge" + "basepower:fluid_discharge" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_drain.json b/src/main/resources/data/basepower/advancements/recipes/fluid_drain.json similarity index 93% rename from src/main/resources/data/poweradvantage/advancements/recipes/fluid_drain.json rename to src/main/resources/data/basepower/advancements/recipes/fluid_drain.json index e3105a37..c79372f5 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_drain.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_drain.json @@ -44,7 +44,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:fluid_drain" + "recipe": "basepower:fluid_drain" } } }, @@ -59,7 +59,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:fluid_drain" + "basepower:fluid_drain" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_metal_tank_iron_normal.json b/src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_iron_normal.json similarity index 86% rename from src/main/resources/data/poweradvantage/advancements/recipes/fluid_metal_tank_iron_normal.json rename to src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_iron_normal.json index c7d0feb8..c25d54ec 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_metal_tank_iron_normal.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_iron_normal.json @@ -24,7 +24,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:fluid_metal_tank_iron_normal" + "recipe": "basepower:fluid_metal_tank_iron_normal" } } }, @@ -37,7 +37,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:fluid_metal_tank_iron_normal" + "basepower:fluid_metal_tank_iron_normal" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_metal_tank_steel.json b/src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_steel.json similarity index 87% rename from src/main/resources/data/poweradvantage/advancements/recipes/fluid_metal_tank_steel.json rename to src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_steel.json index 6d74fa18..98f2b0e3 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_metal_tank_steel.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_steel.json @@ -24,7 +24,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:fluid_metal_tank_steel" + "recipe": "basepower:fluid_metal_tank_steel" } } }, @@ -37,7 +37,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:fluid_metal_tank_steel" + "basepower:fluid_metal_tank_steel" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_copper_apocalyptic.json b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_apocalyptic.json similarity index 80% rename from src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_copper_apocalyptic.json rename to src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_apocalyptic.json index 27009313..9ae581b8 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_copper_apocalyptic.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_apocalyptic.json @@ -14,7 +14,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:fluid_pipe_copper_apocalyptic" + "recipe": "basepower:fluid_pipe_copper_apocalyptic" } } }, @@ -26,7 +26,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:fluid_pipe_copper_apocalyptic" + "basepower:fluid_pipe_copper_apocalyptic" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_copper_normal.json b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_normal.json similarity index 82% rename from src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_copper_normal.json rename to src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_normal.json index 729c73ec..508016f9 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_copper_normal.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_normal.json @@ -14,7 +14,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:fluid_pipe_copper_normal" + "recipe": "basepower:fluid_pipe_copper_normal" } } }, @@ -26,7 +26,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:fluid_pipe_copper_normal" + "basepower:fluid_pipe_copper_normal" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_apocalyptic.json b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_apocalyptic.json similarity index 81% rename from src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_apocalyptic.json rename to src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_apocalyptic.json index 23f83225..902e5684 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_apocalyptic.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_apocalyptic.json @@ -14,7 +14,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:fluid_pipe_iron_apocalyptic" + "recipe": "basepower:fluid_pipe_iron_apocalyptic" } } }, @@ -26,7 +26,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:fluid_pipe_iron_apocalyptic" + "basepower:fluid_pipe_iron_apocalyptic" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_normal.json b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_normal.json similarity index 82% rename from src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_normal.json rename to src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_normal.json index 3e073674..69dfe6cb 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_normal.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_normal.json @@ -14,7 +14,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:fluid_pipe_iron_normal" + "recipe": "basepower:fluid_pipe_iron_normal" } } }, @@ -26,7 +26,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:fluid_pipe_iron_normal" + "basepower:fluid_pipe_iron_normal" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_tech.json b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_tech.json similarity index 83% rename from src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_tech.json rename to src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_tech.json index 0b9c5b1f..d9a093dc 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_tech.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_tech.json @@ -14,7 +14,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:fluid_pipe_iron_tech" + "recipe": "basepower:fluid_pipe_iron_tech" } } }, @@ -26,7 +26,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:fluid_pipe_iron_tech" + "basepower:fluid_pipe_iron_tech" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_lead_apocalyptic.json b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_lead_apocalyptic.json similarity index 81% rename from src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_lead_apocalyptic.json rename to src/main/resources/data/basepower/advancements/recipes/fluid_pipe_lead_apocalyptic.json index 32c9450b..9ebf5a61 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_lead_apocalyptic.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_lead_apocalyptic.json @@ -14,7 +14,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:fluid_pipe_lead_apocalyptic" + "recipe": "basepower:fluid_pipe_lead_apocalyptic" } } }, @@ -26,7 +26,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:fluid_pipe_lead_apocalyptic" + "basepower:fluid_pipe_lead_apocalyptic" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_storage_tank.json b/src/main/resources/data/basepower/advancements/recipes/fluid_storage_tank.json similarity index 88% rename from src/main/resources/data/poweradvantage/advancements/recipes/fluid_storage_tank.json rename to src/main/resources/data/basepower/advancements/recipes/fluid_storage_tank.json index 4ed10ce5..93c5e276 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_storage_tank.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_storage_tank.json @@ -24,7 +24,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:fluid_storage_tank" + "recipe": "basepower:fluid_storage_tank" } } }, @@ -37,7 +37,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:fluid_storage_tank" + "basepower:fluid_storage_tank" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_switch.json b/src/main/resources/data/basepower/advancements/recipes/fluid_switch.json similarity index 86% rename from src/main/resources/data/poweradvantage/advancements/recipes/fluid_switch.json rename to src/main/resources/data/basepower/advancements/recipes/fluid_switch.json index 08e18f07..ab63dfb3 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_switch.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_switch.json @@ -6,7 +6,9 @@ "conditions": { "items": [ { - "item": "minecraft:lever" + "items": [ + "minecraft:lever" + ] } ] } @@ -34,7 +36,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:fluid_switch" + "recipe": "basepower:fluid_switch" } } }, @@ -48,7 +50,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:fluid_switch" + "basepower:fluid_switch" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_conveyor.json b/src/main/resources/data/basepower/advancements/recipes/item_conveyor.json similarity index 86% rename from src/main/resources/data/poweradvantage/advancements/recipes/item_conveyor.json rename to src/main/resources/data/basepower/advancements/recipes/item_conveyor.json index 604ef11f..dc3f1487 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/item_conveyor.json +++ b/src/main/resources/data/basepower/advancements/recipes/item_conveyor.json @@ -26,7 +26,9 @@ "conditions": { "items": [ { - "item": "minecraft:hopper" + "items": [ + "minecraft:hopper" + ] } ] } @@ -34,7 +36,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:item_conveyor" + "recipe": "basepower:item_conveyor" } } }, @@ -48,7 +50,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:item_conveyor" + "basepower:item_conveyor" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_block.json b/src/main/resources/data/basepower/advancements/recipes/item_filter_block.json similarity index 72% rename from src/main/resources/data/poweradvantage/advancements/recipes/item_filter_block.json rename to src/main/resources/data/basepower/advancements/recipes/item_filter_block.json index 75e1064f..115d0e5a 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_block.json +++ b/src/main/resources/data/basepower/advancements/recipes/item_filter_block.json @@ -6,7 +6,9 @@ "conditions": { "items": [ { - "item": "poweradvantage:item_conveyor" + "items": [ + "basepower:item_conveyor" + ] } ] } @@ -16,7 +18,9 @@ "conditions": { "items": [ { - "item": "minecraft:stone" + "items": [ + "minecraft:stone" + ] } ] } @@ -26,7 +30,9 @@ "conditions": { "items": [ { - "item": "minecraft:oak_pressure_plate" + "items": [ + "minecraft:oak_pressure_plate" + ] } ] } @@ -34,7 +40,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:item_filter_block" + "recipe": "basepower:item_filter_block" } } }, @@ -48,7 +54,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:item_filter_block" + "basepower:item_filter_block" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_food.json b/src/main/resources/data/basepower/advancements/recipes/item_filter_food.json similarity index 72% rename from src/main/resources/data/poweradvantage/advancements/recipes/item_filter_food.json rename to src/main/resources/data/basepower/advancements/recipes/item_filter_food.json index a781e2d6..30354212 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_food.json +++ b/src/main/resources/data/basepower/advancements/recipes/item_filter_food.json @@ -6,7 +6,9 @@ "conditions": { "items": [ { - "item": "poweradvantage:item_conveyor" + "items": [ + "basepower:item_conveyor" + ] } ] } @@ -16,7 +18,9 @@ "conditions": { "items": [ { - "item": "minecraft:bread" + "items": [ + "minecraft:bread" + ] } ] } @@ -26,7 +30,9 @@ "conditions": { "items": [ { - "item": "minecraft:oak_pressure_plate" + "items": [ + "minecraft:oak_pressure_plate" + ] } ] } @@ -34,7 +40,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:item_filter_food" + "recipe": "basepower:item_filter_food" } } }, @@ -48,7 +54,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:item_filter_food" + "basepower:item_filter_food" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_fuel.json b/src/main/resources/data/basepower/advancements/recipes/item_filter_fuel.json similarity index 72% rename from src/main/resources/data/poweradvantage/advancements/recipes/item_filter_fuel.json rename to src/main/resources/data/basepower/advancements/recipes/item_filter_fuel.json index 4bef4dd2..da4a9fd6 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_fuel.json +++ b/src/main/resources/data/basepower/advancements/recipes/item_filter_fuel.json @@ -6,7 +6,9 @@ "conditions": { "items": [ { - "item": "poweradvantage:item_conveyor" + "items": [ + "basepower:item_conveyor" + ] } ] } @@ -16,7 +18,9 @@ "conditions": { "items": [ { - "item": "minecraft:coal" + "items": [ + "minecraft:coal" + ] } ] } @@ -26,7 +30,9 @@ "conditions": { "items": [ { - "item": "minecraft:oak_pressure_plate" + "items": [ + "minecraft:oak_pressure_plate" + ] } ] } @@ -34,7 +40,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:item_filter_fuel" + "recipe": "basepower:item_filter_fuel" } } }, @@ -48,7 +54,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:item_filter_fuel" + "basepower:item_filter_fuel" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_inventory.json b/src/main/resources/data/basepower/advancements/recipes/item_filter_inventory.json similarity index 72% rename from src/main/resources/data/poweradvantage/advancements/recipes/item_filter_inventory.json rename to src/main/resources/data/basepower/advancements/recipes/item_filter_inventory.json index cbf44f37..d5135156 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_inventory.json +++ b/src/main/resources/data/basepower/advancements/recipes/item_filter_inventory.json @@ -6,7 +6,9 @@ "conditions": { "items": [ { - "item": "poweradvantage:item_conveyor" + "items": [ + "basepower:item_conveyor" + ] } ] } @@ -16,7 +18,9 @@ "conditions": { "items": [ { - "item": "minecraft:chest" + "items": [ + "minecraft:chest" + ] } ] } @@ -26,7 +30,9 @@ "conditions": { "items": [ { - "item": "minecraft:oak_pressure_plate" + "items": [ + "minecraft:oak_pressure_plate" + ] } ] } @@ -34,7 +40,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:item_filter_inventory" + "recipe": "basepower:item_filter_inventory" } } }, @@ -48,7 +54,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:item_filter_inventory" + "basepower:item_filter_inventory" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_ore.json b/src/main/resources/data/basepower/advancements/recipes/item_filter_ore.json similarity index 78% rename from src/main/resources/data/poweradvantage/advancements/recipes/item_filter_ore.json rename to src/main/resources/data/basepower/advancements/recipes/item_filter_ore.json index 8b572bdf..20d8379e 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_ore.json +++ b/src/main/resources/data/basepower/advancements/recipes/item_filter_ore.json @@ -6,7 +6,9 @@ "conditions": { "items": [ { - "item": "poweradvantage:item_conveyor" + "items": [ + "basepower:item_conveyor" + ] } ] } @@ -26,7 +28,9 @@ "conditions": { "items": [ { - "item": "minecraft:oak_pressure_plate" + "items": [ + "minecraft:oak_pressure_plate" + ] } ] } @@ -34,7 +38,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:item_filter_ore" + "recipe": "basepower:item_filter_ore" } } }, @@ -48,7 +52,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:item_filter_ore" + "basepower:item_filter_ore" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_overflow.json b/src/main/resources/data/basepower/advancements/recipes/item_filter_overflow.json similarity index 72% rename from src/main/resources/data/poweradvantage/advancements/recipes/item_filter_overflow.json rename to src/main/resources/data/basepower/advancements/recipes/item_filter_overflow.json index 46807bcb..88edaf5e 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_overflow.json +++ b/src/main/resources/data/basepower/advancements/recipes/item_filter_overflow.json @@ -6,7 +6,9 @@ "conditions": { "items": [ { - "item": "poweradvantage:item_conveyor" + "items": [ + "basepower:item_conveyor" + ] } ] } @@ -16,7 +18,9 @@ "conditions": { "items": [ { - "item": "minecraft:oak_pressure_plate" + "items": [ + "minecraft:oak_pressure_plate" + ] } ] } @@ -24,7 +28,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:item_filter_overflow" + "recipe": "basepower:item_filter_overflow" } } }, @@ -37,7 +41,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:item_filter_overflow" + "basepower:item_filter_overflow" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_plant.json b/src/main/resources/data/basepower/advancements/recipes/item_filter_plant.json similarity index 78% rename from src/main/resources/data/poweradvantage/advancements/recipes/item_filter_plant.json rename to src/main/resources/data/basepower/advancements/recipes/item_filter_plant.json index 8fcd57bc..061a6d22 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_plant.json +++ b/src/main/resources/data/basepower/advancements/recipes/item_filter_plant.json @@ -6,7 +6,9 @@ "conditions": { "items": [ { - "item": "poweradvantage:item_conveyor" + "items": [ + "basepower:item_conveyor" + ] } ] } @@ -26,7 +28,9 @@ "conditions": { "items": [ { - "item": "minecraft:oak_pressure_plate" + "items": [ + "minecraft:oak_pressure_plate" + ] } ] } @@ -34,7 +38,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:item_filter_plant" + "recipe": "basepower:item_filter_plant" } } }, @@ -48,7 +52,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:item_filter_plant" + "basepower:item_filter_plant" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_smelt.json b/src/main/resources/data/basepower/advancements/recipes/item_filter_smelt.json similarity index 72% rename from src/main/resources/data/poweradvantage/advancements/recipes/item_filter_smelt.json rename to src/main/resources/data/basepower/advancements/recipes/item_filter_smelt.json index b0f57b85..c88d9095 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_smelt.json +++ b/src/main/resources/data/basepower/advancements/recipes/item_filter_smelt.json @@ -6,7 +6,9 @@ "conditions": { "items": [ { - "item": "poweradvantage:item_conveyor" + "items": [ + "basepower:item_conveyor" + ] } ] } @@ -16,7 +18,9 @@ "conditions": { "items": [ { - "item": "minecraft:furnace" + "items": [ + "minecraft:furnace" + ] } ] } @@ -26,7 +30,9 @@ "conditions": { "items": [ { - "item": "minecraft:oak_pressure_plate" + "items": [ + "minecraft:oak_pressure_plate" + ] } ] } @@ -34,7 +40,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:item_filter_smelt" + "recipe": "basepower:item_filter_smelt" } } }, @@ -48,7 +54,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:item_filter_smelt" + "basepower:item_filter_smelt" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/rotator_tool.json b/src/main/resources/data/basepower/advancements/recipes/rotator_tool.json similarity index 89% rename from src/main/resources/data/poweradvantage/advancements/recipes/rotator_tool.json rename to src/main/resources/data/basepower/advancements/recipes/rotator_tool.json index b8367265..eed458d8 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/rotator_tool.json +++ b/src/main/resources/data/basepower/advancements/recipes/rotator_tool.json @@ -24,7 +24,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:rotator_tool" + "recipe": "basepower:rotator_tool" } } }, @@ -37,7 +37,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:rotator_tool" + "basepower:rotator_tool" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json b/src/main/resources/data/basepower/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json similarity index 84% rename from src/main/resources/data/poweradvantage/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json rename to src/main/resources/data/basepower/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json index 088df53f..2591d5ee 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json +++ b/src/main/resources/data/basepower/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json @@ -24,7 +24,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:sprocket_bronze_nugget_tech_no_rod" + "recipe": "basepower:sprocket_bronze_nugget_tech_no_rod" } } }, @@ -37,7 +37,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:sprocket_bronze_nugget_tech_no_rod" + "basepower:sprocket_bronze_nugget_tech_no_rod" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_rod_normal.json b/src/main/resources/data/basepower/advancements/recipes/sprocket_rod_normal.json similarity index 87% rename from src/main/resources/data/poweradvantage/advancements/recipes/sprocket_rod_normal.json rename to src/main/resources/data/basepower/advancements/recipes/sprocket_rod_normal.json index a322a855..ea3a3bcc 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_rod_normal.json +++ b/src/main/resources/data/basepower/advancements/recipes/sprocket_rod_normal.json @@ -24,7 +24,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:sprocket_rod_normal" + "recipe": "basepower:sprocket_rod_normal" } } }, @@ -37,7 +37,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:sprocket_rod_normal" + "basepower:sprocket_rod_normal" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_rod_tech.json b/src/main/resources/data/basepower/advancements/recipes/sprocket_rod_tech.json similarity index 88% rename from src/main/resources/data/poweradvantage/advancements/recipes/sprocket_rod_tech.json rename to src/main/resources/data/basepower/advancements/recipes/sprocket_rod_tech.json index 5f79ed5b..4a6a2719 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_rod_tech.json +++ b/src/main/resources/data/basepower/advancements/recipes/sprocket_rod_tech.json @@ -24,7 +24,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:sprocket_rod_tech" + "recipe": "basepower:sprocket_rod_tech" } } }, @@ -37,7 +37,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:sprocket_rod_tech" + "basepower:sprocket_rod_tech" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_stickwood_normal.json b/src/main/resources/data/basepower/advancements/recipes/sprocket_stickwood_normal.json similarity index 81% rename from src/main/resources/data/poweradvantage/advancements/recipes/sprocket_stickwood_normal.json rename to src/main/resources/data/basepower/advancements/recipes/sprocket_stickwood_normal.json index 2ed87710..203b43ad 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_stickwood_normal.json +++ b/src/main/resources/data/basepower/advancements/recipes/sprocket_stickwood_normal.json @@ -14,7 +14,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:sprocket_stickwood_normal" + "recipe": "basepower:sprocket_stickwood_normal" } } }, @@ -26,7 +26,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:sprocket_stickwood_normal" + "basepower:sprocket_stickwood_normal" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/starch_from_potato.json b/src/main/resources/data/basepower/advancements/recipes/starch_from_potato.json similarity index 73% rename from src/main/resources/data/poweradvantage/advancements/recipes/starch_from_potato.json rename to src/main/resources/data/basepower/advancements/recipes/starch_from_potato.json index 1b43de31..ac7e7fb0 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/starch_from_potato.json +++ b/src/main/resources/data/basepower/advancements/recipes/starch_from_potato.json @@ -6,7 +6,9 @@ "conditions": { "items": [ { - "item": "minecraft:potato" + "items": [ + "minecraft:potato" + ] } ] } @@ -16,7 +18,9 @@ "conditions": { "items": [ { - "item": "minecraft:poisonous_potato" + "items": [ + "minecraft:poisonous_potato" + ] } ] } @@ -24,7 +28,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:starch_from_potato" + "recipe": "basepower:starch_from_potato" } } }, @@ -37,7 +41,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:starch_from_potato" + "basepower:starch_from_potato" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/steel_frame.json b/src/main/resources/data/basepower/advancements/recipes/steel_frame.json similarity index 85% rename from src/main/resources/data/poweradvantage/advancements/recipes/steel_frame.json rename to src/main/resources/data/basepower/advancements/recipes/steel_frame.json index 48cafc5e..f0268ab3 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/steel_frame.json +++ b/src/main/resources/data/basepower/advancements/recipes/steel_frame.json @@ -14,7 +14,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:steel_frame" + "recipe": "basepower:steel_frame" } } }, @@ -26,7 +26,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:steel_frame" + "basepower:steel_frame" ] } } diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/still.json b/src/main/resources/data/basepower/advancements/recipes/still.json similarity index 81% rename from src/main/resources/data/poweradvantage/advancements/recipes/still.json rename to src/main/resources/data/basepower/advancements/recipes/still.json index c3199575..9b2de58d 100644 --- a/src/main/resources/data/poweradvantage/advancements/recipes/still.json +++ b/src/main/resources/data/basepower/advancements/recipes/still.json @@ -6,7 +6,9 @@ "conditions": { "items": [ { - "item": "minecraft:bucket" + "items": [ + "minecraft:bucket" + ] } ] } @@ -26,7 +28,9 @@ "conditions": { "items": [ { - "item": "minecraft:furnace" + "items": [ + "minecraft:furnace" + ] } ] } @@ -34,7 +38,7 @@ "has_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { - "recipe": "poweradvantage:still" + "recipe": "basepower:still" } } }, @@ -48,7 +52,7 @@ ], "rewards": { "recipes": [ - "poweradvantage:still" + "basepower:still" ] } } diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_steam.json b/src/main/resources/data/basepower/loot_tables/blocks/converter_rf_electricity.json similarity index 82% rename from src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_steam.json rename to src/main/resources/data/basepower/loot_tables/blocks/converter_rf_electricity.json index 07737020..f0eeb562 100644 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_steam.json +++ b/src/main/resources/data/basepower/loot_tables/blocks/converter_rf_electricity.json @@ -6,7 +6,7 @@ "entries": [ { "type": "minecraft:item", - "name": "poweradvantage:converter_rf_steam" + "name": "basepower:converter_rf_electricity" } ], "conditions": [ diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_discharge.json b/src/main/resources/data/basepower/loot_tables/blocks/converter_rf_quantum.json similarity index 84% rename from src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_discharge.json rename to src/main/resources/data/basepower/loot_tables/blocks/converter_rf_quantum.json index 298c05f7..2e242d64 100644 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_discharge.json +++ b/src/main/resources/data/basepower/loot_tables/blocks/converter_rf_quantum.json @@ -6,7 +6,7 @@ "entries": [ { "type": "minecraft:item", - "name": "poweradvantage:fluid_discharge" + "name": "basepower:converter_rf_quantum" } ], "conditions": [ diff --git a/src/main/resources/data/basepower/loot_tables/blocks/converter_rf_steam.json b/src/main/resources/data/basepower/loot_tables/blocks/converter_rf_steam.json new file mode 100644 index 00000000..3d539b20 --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/converter_rf_steam.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:converter_rf_steam" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/basepower/loot_tables/blocks/converter_tr_electricity.json b/src/main/resources/data/basepower/loot_tables/blocks/converter_tr_electricity.json new file mode 100644 index 00000000..0b038ad7 --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/converter_tr_electricity.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:converter_tr_electricity" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/crude_oil.json b/src/main/resources/data/basepower/loot_tables/blocks/crude_oil.json similarity index 100% rename from src/main/resources/data/poweradvantage/loot_tables/blocks/crude_oil.json rename to src/main/resources/data/basepower/loot_tables/blocks/crude_oil.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/fluid_discharge.json b/src/main/resources/data/basepower/loot_tables/blocks/fluid_discharge.json new file mode 100644 index 00000000..16622703 --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/fluid_discharge.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:fluid_discharge" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_drain.json b/src/main/resources/data/basepower/loot_tables/blocks/fluid_drain.json similarity index 85% rename from src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_drain.json rename to src/main/resources/data/basepower/loot_tables/blocks/fluid_drain.json index b2ad111b..227d6e92 100644 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_drain.json +++ b/src/main/resources/data/basepower/loot_tables/blocks/fluid_drain.json @@ -6,7 +6,7 @@ "entries": [ { "type": "minecraft:item", - "name": "poweradvantage:fluid_drain" + "name": "basepower:fluid_drain" } ], "conditions": [ diff --git a/src/main/resources/data/basepower/loot_tables/blocks/fluid_metal_tank.json b/src/main/resources/data/basepower/loot_tables/blocks/fluid_metal_tank.json new file mode 100644 index 00000000..5064dee3 --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/fluid_metal_tank.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:fluid_metal_tank" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_pipe.json b/src/main/resources/data/basepower/loot_tables/blocks/fluid_pipe.json similarity index 85% rename from src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_pipe.json rename to src/main/resources/data/basepower/loot_tables/blocks/fluid_pipe.json index 3d5f6f46..9f8e3a2a 100644 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_pipe.json +++ b/src/main/resources/data/basepower/loot_tables/blocks/fluid_pipe.json @@ -6,7 +6,7 @@ "entries": [ { "type": "minecraft:item", - "name": "poweradvantage:fluid_pipe" + "name": "basepower:fluid_pipe" } ], "conditions": [ diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_pipe_terminal.json b/src/main/resources/data/basepower/loot_tables/blocks/fluid_pipe_terminal.json similarity index 85% rename from src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_pipe_terminal.json rename to src/main/resources/data/basepower/loot_tables/blocks/fluid_pipe_terminal.json index 3d5f6f46..9f8e3a2a 100644 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_pipe_terminal.json +++ b/src/main/resources/data/basepower/loot_tables/blocks/fluid_pipe_terminal.json @@ -6,7 +6,7 @@ "entries": [ { "type": "minecraft:item", - "name": "poweradvantage:fluid_pipe" + "name": "basepower:fluid_pipe" } ], "conditions": [ diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_storage_tank.json b/src/main/resources/data/basepower/loot_tables/blocks/fluid_storage_tank.json similarity index 93% rename from src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_storage_tank.json rename to src/main/resources/data/basepower/loot_tables/blocks/fluid_storage_tank.json index f36a05cc..c77ad259 100644 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_storage_tank.json +++ b/src/main/resources/data/basepower/loot_tables/blocks/fluid_storage_tank.json @@ -6,7 +6,7 @@ "entries": [ { "type": "minecraft:item", - "name": "poweradvantage:fluid_storage_tank", + "name": "basepower:fluid_storage_tank", "functions": [ { "function": "minecraft:copy_nbt", diff --git a/src/main/resources/data/basepower/loot_tables/blocks/fluid_switch.json b/src/main/resources/data/basepower/loot_tables/blocks/fluid_switch.json new file mode 100644 index 00000000..e17c40b0 --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/fluid_switch.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:fluid_switch" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/basepower/loot_tables/blocks/infinite_electricity.json b/src/main/resources/data/basepower/loot_tables/blocks/infinite_electricity.json new file mode 100644 index 00000000..f2619aa1 --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/infinite_electricity.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:infinite_electricity" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/basepower/loot_tables/blocks/infinite_quantum.json b/src/main/resources/data/basepower/loot_tables/blocks/infinite_quantum.json new file mode 100644 index 00000000..a6d00f22 --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/infinite_quantum.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:infinite_quantum" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/basepower/loot_tables/blocks/infinite_steam.json b/src/main/resources/data/basepower/loot_tables/blocks/infinite_steam.json new file mode 100644 index 00000000..d6160ac6 --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/infinite_steam.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:infinite_steam" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_conveyor.json b/src/main/resources/data/basepower/loot_tables/blocks/item_conveyor.json new file mode 100644 index 00000000..5ebadd8b --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/item_conveyor.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:item_conveyor" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_switch.json b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_block.json similarity index 84% rename from src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_switch.json rename to src/main/resources/data/basepower/loot_tables/blocks/item_filter_block.json index c7f47edc..954e7bca 100644 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_switch.json +++ b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_block.json @@ -6,7 +6,7 @@ "entries": [ { "type": "minecraft:item", - "name": "poweradvantage:fluid_switch" + "name": "basepower:item_filter_block" } ], "conditions": [ diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_filter_food.json b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_food.json new file mode 100644 index 00000000..037ac4ed --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_food.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:item_filter_food" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_filter_fuel.json b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_fuel.json new file mode 100644 index 00000000..441949c1 --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_fuel.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:item_filter_fuel" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_metal_tank.json b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_inventory.json similarity index 83% rename from src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_metal_tank.json rename to src/main/resources/data/basepower/loot_tables/blocks/item_filter_inventory.json index acd5684f..22738bc4 100644 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_metal_tank.json +++ b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_inventory.json @@ -6,7 +6,7 @@ "entries": [ { "type": "minecraft:item", - "name": "poweradvantage:fluid_metal_tank" + "name": "basepower:item_filter_inventory" } ], "conditions": [ diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_filter_ore.json b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_ore.json new file mode 100644 index 00000000..65407ae0 --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_ore.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:item_filter_ore" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_filter_overflow.json b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_overflow.json new file mode 100644 index 00000000..14643a76 --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_overflow.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:item_filter_overflow" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_filter_plant.json b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_plant.json new file mode 100644 index 00000000..789d5818 --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_plant.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:item_filter_plant" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_filter_smelt.json b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_smelt.json new file mode 100644 index 00000000..428ef83b --- /dev/null +++ b/src/main/resources/data/basepower/loot_tables/blocks/item_filter_smelt.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "basepower:item_filter_smelt" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/refined_oil.json b/src/main/resources/data/basepower/loot_tables/blocks/refined_oil.json similarity index 100% rename from src/main/resources/data/poweradvantage/loot_tables/blocks/refined_oil.json rename to src/main/resources/data/basepower/loot_tables/blocks/refined_oil.json diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/steel_frame.json b/src/main/resources/data/basepower/loot_tables/blocks/steel_frame.json similarity index 85% rename from src/main/resources/data/poweradvantage/loot_tables/blocks/steel_frame.json rename to src/main/resources/data/basepower/loot_tables/blocks/steel_frame.json index 9dd92362..db1c9265 100644 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/steel_frame.json +++ b/src/main/resources/data/basepower/loot_tables/blocks/steel_frame.json @@ -6,7 +6,7 @@ "entries": [ { "type": "minecraft:item", - "name": "poweradvantage:steel_frame" + "name": "basepower:steel_frame" } ], "conditions": [ diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/still.json b/src/main/resources/data/basepower/loot_tables/blocks/still.json similarity index 86% rename from src/main/resources/data/poweradvantage/loot_tables/blocks/still.json rename to src/main/resources/data/basepower/loot_tables/blocks/still.json index e71b507e..be2fb245 100644 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/still.json +++ b/src/main/resources/data/basepower/loot_tables/blocks/still.json @@ -6,7 +6,7 @@ "entries": [ { "type": "minecraft:item", - "name": "poweradvantage:still" + "name": "basepower:still" } ], "conditions": [ diff --git a/src/main/resources/data/poweradvantage/loot_tables/chests/advantage_parts.json b/src/main/resources/data/basepower/loot_tables/chests/advantage_parts.json similarity index 77% rename from src/main/resources/data/poweradvantage/loot_tables/chests/advantage_parts.json rename to src/main/resources/data/basepower/loot_tables/chests/advantage_parts.json index c4998bb3..c0958a9c 100644 --- a/src/main/resources/data/poweradvantage/loot_tables/chests/advantage_parts.json +++ b/src/main/resources/data/basepower/loot_tables/chests/advantage_parts.json @@ -10,7 +10,7 @@ }, { "type": "item", - "name": "poweradvantage:sprocket", + "name": "basepower:sprocket", "weight": 5, "functions": [ { @@ -24,7 +24,7 @@ }, { "type": "item", - "name": "poweradvantage:fluid_pipe", + "name": "basepower:fluid_pipe", "weight": 5, "functions": [ { @@ -38,22 +38,22 @@ }, { "type": "item", - "name": "poweradvantage:fluid_storage_tank", + "name": "basepower:fluid_storage_tank", "weight": 3 }, { "type": "item", - "name": "poweradvantage:fluid_drain", + "name": "basepower:fluid_drain", "weight": 2 }, { "type": "item", - "name": "poweradvantage:fluid_discharge", + "name": "basepower:fluid_discharge", "weight": 2 }, { "type": "item", - "name": "poweradvantage:item_conveyor", + "name": "basepower:item_conveyor", "weight": 3, "functions": [ { @@ -66,7 +66,7 @@ ] } ], - "name": "poweradvantage_legacy_parts_0" + "name": "basepower_legacy_parts_0" } ] } diff --git a/src/main/resources/data/poweradvantage/orespawn/provider.json b/src/main/resources/data/basepower/orespawn/provider.json similarity index 89% rename from src/main/resources/data/poweradvantage/orespawn/provider.json rename to src/main/resources/data/basepower/orespawn/provider.json index 926e1005..6dd46c28 100644 --- a/src/main/resources/data/poweradvantage/orespawn/provider.json +++ b/src/main/resources/data/basepower/orespawn/provider.json @@ -1,14 +1,14 @@ { "schema_version": 4, - "provider_modid": "poweradvantage", + "provider_modid": "basepower", "provider_revision": 1, "profile_defaults": { "place_fluid_deposits": true }, "fluid_deposits": { - "poweradvantage:fluid_deposit/desert_crude_oil": { + "basepower:fluid_deposit/desert_crude_oil": { "enabled": true, - "block": "poweradvantage:crude_oil", + "block": "basepower:crude_oil", "dimensions": { "minecraft:overworld": { "enabled": true, diff --git a/src/main/resources/data/basepower/recipes/bioplastic_from_starch.json b/src/main/resources/data/basepower/recipes/bioplastic_from_starch.json new file mode 100644 index 00000000..d0d38d7b --- /dev/null +++ b/src/main/resources/data/basepower/recipes/bioplastic_from_starch.json @@ -0,0 +1,11 @@ +{ + "type": "minecraft:smelting", + "ingredient": { + "item": "basepower:starch" + }, + "result": "basepower:bioplastic_ingot", + "experience": 0.1, + "cookingtime": 200, + "conditions": [], + "group": "basepower:bioplastic_ingot" +} diff --git a/src/main/resources/data/poweradvantage/recipes/converter_rf_electricity.json b/src/main/resources/data/basepower/recipes/converter_rf_electricity.json similarity index 66% rename from src/main/resources/data/poweradvantage/recipes/converter_rf_electricity.json rename to src/main/resources/data/basepower/recipes/converter_rf_electricity.json index 14ce2a66..2684a731 100644 --- a/src/main/resources/data/poweradvantage/recipes/converter_rf_electricity.json +++ b/src/main/resources/data/basepower/recipes/converter_rf_electricity.json @@ -15,17 +15,17 @@ } }, "result": { - "item": "poweradvantage:converter_rf_electricity" + "item": "basepower:converter_rf_electricity" }, "conditions": [ { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:power_supply_units" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:frames/steel" } ], - "group": "poweradvantage:converter_rf_electricity" + "group": "basepower:converter_rf_electricity" } diff --git a/src/main/resources/data/poweradvantage/recipes/converter_rf_quantum.json b/src/main/resources/data/basepower/recipes/converter_rf_quantum.json similarity index 70% rename from src/main/resources/data/poweradvantage/recipes/converter_rf_quantum.json rename to src/main/resources/data/basepower/recipes/converter_rf_quantum.json index 847a7766..42803316 100644 --- a/src/main/resources/data/poweradvantage/recipes/converter_rf_quantum.json +++ b/src/main/resources/data/basepower/recipes/converter_rf_quantum.json @@ -15,13 +15,13 @@ } }, "result": { - "item": "poweradvantage:converter_rf_quantum" + "item": "basepower:converter_rf_quantum" }, "conditions": [ { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:frames/steel" } ], - "group": "poweradvantage:converter_rf_quantum" + "group": "basepower:converter_rf_quantum" } diff --git a/src/main/resources/data/poweradvantage/recipes/converter_rf_steam.json b/src/main/resources/data/basepower/recipes/converter_rf_steam.json similarity index 67% rename from src/main/resources/data/poweradvantage/recipes/converter_rf_steam.json rename to src/main/resources/data/basepower/recipes/converter_rf_steam.json index 8063d676..af33bdfc 100644 --- a/src/main/resources/data/poweradvantage/recipes/converter_rf_steam.json +++ b/src/main/resources/data/basepower/recipes/converter_rf_steam.json @@ -15,17 +15,17 @@ } }, "result": { - "item": "poweradvantage:converter_rf_steam" + "item": "basepower:converter_rf_steam" }, "conditions": [ { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:governors" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:frames/steel" } ], - "group": "poweradvantage:converter_rf_steam" + "group": "basepower:converter_rf_steam" } diff --git a/src/main/resources/data/poweradvantage/recipes/converter_tr_electricity_iron.json b/src/main/resources/data/basepower/recipes/converter_tr_electricity_iron.json similarity index 63% rename from src/main/resources/data/poweradvantage/recipes/converter_tr_electricity_iron.json rename to src/main/resources/data/basepower/recipes/converter_tr_electricity_iron.json index 7e651368..1e4061fc 100644 --- a/src/main/resources/data/poweradvantage/recipes/converter_tr_electricity_iron.json +++ b/src/main/resources/data/basepower/recipes/converter_tr_electricity_iron.json @@ -14,21 +14,21 @@ } }, "result": { - "item": "poweradvantage:converter_tr_electricity" + "item": "basepower:converter_tr_electricity" }, "conditions": [ { - "type": "poweradvantage:tag_empty", + "type": "basepower:tag_empty", "tag": "forge:ingots/refined_iron" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/iron" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:power_supply_units" } ], - "group": "poweradvantage:converter_tr_electricity" + "group": "basepower:converter_tr_electricity" } diff --git a/src/main/resources/data/poweradvantage/recipes/converter_tr_electricity_refined_iron.json b/src/main/resources/data/basepower/recipes/converter_tr_electricity_refined_iron.json similarity index 65% rename from src/main/resources/data/poweradvantage/recipes/converter_tr_electricity_refined_iron.json rename to src/main/resources/data/basepower/recipes/converter_tr_electricity_refined_iron.json index b54899e9..478f6afa 100644 --- a/src/main/resources/data/poweradvantage/recipes/converter_tr_electricity_refined_iron.json +++ b/src/main/resources/data/basepower/recipes/converter_tr_electricity_refined_iron.json @@ -14,17 +14,17 @@ } }, "result": { - "item": "poweradvantage:converter_tr_electricity" + "item": "basepower:converter_tr_electricity" }, "conditions": [ { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/refined_iron" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:power_supply_units" } ], - "group": "poweradvantage:converter_tr_electricity" + "group": "basepower:converter_tr_electricity" } diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_fluid_discharge.json b/src/main/resources/data/basepower/recipes/crushing_fluid_discharge.json similarity index 63% rename from src/main/resources/data/poweradvantage/recipes/crushing_fluid_discharge.json rename to src/main/resources/data/basepower/recipes/crushing_fluid_discharge.json index be1433fc..287cfbfd 100644 --- a/src/main/resources/data/poweradvantage/recipes/crushing_fluid_discharge.json +++ b/src/main/resources/data/basepower/recipes/crushing_fluid_discharge.json @@ -1,10 +1,10 @@ { "type": "basemetals:crushing", "ingredient": { - "item": "poweradvantage:fluid_discharge" + "item": "basepower:fluid_discharge" }, "result": { - "item": "poweradvantage:fluid_pipe", + "item": "basepower:fluid_pipe", "count": 2 }, "conditions": [ @@ -13,7 +13,7 @@ "modid": "basemetals" }, { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "APOCALYPTIC" } ] diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_fluid_drain.json b/src/main/resources/data/basepower/recipes/crushing_fluid_drain.json similarity index 64% rename from src/main/resources/data/poweradvantage/recipes/crushing_fluid_drain.json rename to src/main/resources/data/basepower/recipes/crushing_fluid_drain.json index 9f29a41c..69d8e470 100644 --- a/src/main/resources/data/poweradvantage/recipes/crushing_fluid_drain.json +++ b/src/main/resources/data/basepower/recipes/crushing_fluid_drain.json @@ -1,10 +1,10 @@ { "type": "basemetals:crushing", "ingredient": { - "item": "poweradvantage:fluid_drain" + "item": "basepower:fluid_drain" }, "result": { - "item": "poweradvantage:fluid_pipe", + "item": "basepower:fluid_pipe", "count": 2 }, "conditions": [ @@ -13,7 +13,7 @@ "modid": "basemetals" }, { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "APOCALYPTIC" } ] diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_fluid_storage_tank.json b/src/main/resources/data/basepower/recipes/crushing_fluid_storage_tank.json similarity index 63% rename from src/main/resources/data/poweradvantage/recipes/crushing_fluid_storage_tank.json rename to src/main/resources/data/basepower/recipes/crushing_fluid_storage_tank.json index ef926eb3..a6d41a9e 100644 --- a/src/main/resources/data/poweradvantage/recipes/crushing_fluid_storage_tank.json +++ b/src/main/resources/data/basepower/recipes/crushing_fluid_storage_tank.json @@ -1,10 +1,10 @@ { "type": "basemetals:crushing", "ingredient": { - "item": "poweradvantage:fluid_storage_tank" + "item": "basepower:fluid_storage_tank" }, "result": { - "item": "poweradvantage:fluid_pipe", + "item": "basepower:fluid_pipe", "count": 1 }, "conditions": [ @@ -13,7 +13,7 @@ "modid": "basemetals" }, { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "APOCALYPTIC" } ] diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_conveyor.json b/src/main/resources/data/basepower/recipes/crushing_item_conveyor.json similarity index 70% rename from src/main/resources/data/poweradvantage/recipes/crushing_item_conveyor.json rename to src/main/resources/data/basepower/recipes/crushing_item_conveyor.json index 938aa2cd..f20d15eb 100644 --- a/src/main/resources/data/poweradvantage/recipes/crushing_item_conveyor.json +++ b/src/main/resources/data/basepower/recipes/crushing_item_conveyor.json @@ -1,7 +1,7 @@ { "type": "basemetals:crushing", "ingredient": { - "item": "poweradvantage:item_conveyor" + "item": "basepower:item_conveyor" }, "result": { "item": "basemetals:steel_plate", @@ -13,11 +13,11 @@ "modid": "basemetals" }, { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "APOCALYPTIC" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:plates/steel" } ] diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_block.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_block.json new file mode 100644 index 00000000..d3429ad9 --- /dev/null +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_block.json @@ -0,0 +1,24 @@ +{ + "type": "basemetals:crushing", + "ingredient": { + "item": "basepower:item_filter_block" + }, + "result": { + "item": "basemetals:steel_plate", + "count": 1 + }, + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] +} diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_food.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_food.json similarity index 69% rename from src/main/resources/data/poweradvantage/recipes/crushing_item_filter_food.json rename to src/main/resources/data/basepower/recipes/crushing_item_filter_food.json index 21c06c04..05ccc8e4 100644 --- a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_food.json +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_food.json @@ -1,7 +1,7 @@ { "type": "basemetals:crushing", "ingredient": { - "item": "poweradvantage:item_filter_food" + "item": "basepower:item_filter_food" }, "result": { "item": "basemetals:steel_plate", @@ -13,11 +13,11 @@ "modid": "basemetals" }, { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "APOCALYPTIC" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:plates/steel" } ] diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_fuel.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_fuel.json similarity index 69% rename from src/main/resources/data/poweradvantage/recipes/crushing_item_filter_fuel.json rename to src/main/resources/data/basepower/recipes/crushing_item_filter_fuel.json index 15f45a8f..44f31b31 100644 --- a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_fuel.json +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_fuel.json @@ -1,7 +1,7 @@ { "type": "basemetals:crushing", "ingredient": { - "item": "poweradvantage:item_filter_fuel" + "item": "basepower:item_filter_fuel" }, "result": { "item": "basemetals:steel_plate", @@ -13,11 +13,11 @@ "modid": "basemetals" }, { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "APOCALYPTIC" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:plates/steel" } ] diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_inventory.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_inventory.json new file mode 100644 index 00000000..807e993d --- /dev/null +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_inventory.json @@ -0,0 +1,24 @@ +{ + "type": "basemetals:crushing", + "ingredient": { + "item": "basepower:item_filter_inventory" + }, + "result": { + "item": "basemetals:steel_plate", + "count": 1 + }, + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] +} diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_ore.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_ore.json similarity index 70% rename from src/main/resources/data/poweradvantage/recipes/crushing_item_filter_ore.json rename to src/main/resources/data/basepower/recipes/crushing_item_filter_ore.json index c452fc85..e6163d6b 100644 --- a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_ore.json +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_ore.json @@ -1,7 +1,7 @@ { "type": "basemetals:crushing", "ingredient": { - "item": "poweradvantage:item_filter_ore" + "item": "basepower:item_filter_ore" }, "result": { "item": "basemetals:steel_plate", @@ -13,11 +13,11 @@ "modid": "basemetals" }, { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "APOCALYPTIC" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:plates/steel" } ] diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_overflow.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_overflow.json new file mode 100644 index 00000000..bebb9dbf --- /dev/null +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_overflow.json @@ -0,0 +1,24 @@ +{ + "type": "basemetals:crushing", + "ingredient": { + "item": "basepower:item_filter_overflow" + }, + "result": { + "item": "basemetals:steel_plate", + "count": 1 + }, + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] +} diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_plant.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_plant.json new file mode 100644 index 00000000..3477628d --- /dev/null +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_plant.json @@ -0,0 +1,24 @@ +{ + "type": "basemetals:crushing", + "ingredient": { + "item": "basepower:item_filter_plant" + }, + "result": { + "item": "basemetals:steel_plate", + "count": 1 + }, + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] +} diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_smelt.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_smelt.json new file mode 100644 index 00000000..4b460e80 --- /dev/null +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_smelt.json @@ -0,0 +1,24 @@ +{ + "type": "basemetals:crushing", + "ingredient": { + "item": "basepower:item_filter_smelt" + }, + "result": { + "item": "basemetals:steel_plate", + "count": 1 + }, + "conditions": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] +} diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_potato_starch.json b/src/main/resources/data/basepower/recipes/crushing_potato_starch.json similarity index 88% rename from src/main/resources/data/poweradvantage/recipes/crushing_potato_starch.json rename to src/main/resources/data/basepower/recipes/crushing_potato_starch.json index 9282b29e..6e8df4fa 100644 --- a/src/main/resources/data/poweradvantage/recipes/crushing_potato_starch.json +++ b/src/main/resources/data/basepower/recipes/crushing_potato_starch.json @@ -9,7 +9,7 @@ } ], "result": { - "item": "poweradvantage:starch", + "item": "basepower:starch", "count": 1 }, "conditions": [ diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_discharge.json b/src/main/resources/data/basepower/recipes/fluid_discharge.json similarity index 65% rename from src/main/resources/data/poweradvantage/recipes/fluid_discharge.json rename to src/main/resources/data/basepower/recipes/fluid_discharge.json index ecf2d303..6a49f66d 100644 --- a/src/main/resources/data/poweradvantage/recipes/fluid_discharge.json +++ b/src/main/resources/data/basepower/recipes/fluid_discharge.json @@ -20,24 +20,24 @@ } }, "result": { - "item": "poweradvantage:fluid_discharge" + "item": "basepower:fluid_discharge" }, - "group": "poweradvantage:fluid_discharge", + "group": "basepower:fluid_discharge", "conditions": [ { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:bars" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:frames/steel" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:pipes" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:plates/steel" } ] diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_drain.json b/src/main/resources/data/basepower/recipes/fluid_drain.json similarity index 65% rename from src/main/resources/data/poweradvantage/recipes/fluid_drain.json rename to src/main/resources/data/basepower/recipes/fluid_drain.json index 94a9e5b5..fd33b2c8 100644 --- a/src/main/resources/data/poweradvantage/recipes/fluid_drain.json +++ b/src/main/resources/data/basepower/recipes/fluid_drain.json @@ -20,24 +20,24 @@ } }, "result": { - "item": "poweradvantage:fluid_drain" + "item": "basepower:fluid_drain" }, - "group": "poweradvantage:fluid_drain", + "group": "basepower:fluid_drain", "conditions": [ { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:bars" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:frames/steel" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:pipes" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:plates/steel" } ] diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_metal_tank_iron_normal.json b/src/main/resources/data/basepower/recipes/fluid_metal_tank_iron_normal.json similarity index 61% rename from src/main/resources/data/poweradvantage/recipes/fluid_metal_tank_iron_normal.json rename to src/main/resources/data/basepower/recipes/fluid_metal_tank_iron_normal.json index f97e9c64..9c0fdc33 100644 --- a/src/main/resources/data/poweradvantage/recipes/fluid_metal_tank_iron_normal.json +++ b/src/main/resources/data/basepower/recipes/fluid_metal_tank_iron_normal.json @@ -14,21 +14,21 @@ } }, "result": { - "item": "poweradvantage:fluid_metal_tank" + "item": "basepower:fluid_metal_tank" }, "conditions": [ { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "NORMAL" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/iron" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:pipes" } ], - "group": "poweradvantage:fluid_metal_tank" + "group": "basepower:fluid_metal_tank" } diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_metal_tank_steel.json b/src/main/resources/data/basepower/recipes/fluid_metal_tank_steel.json similarity index 64% rename from src/main/resources/data/poweradvantage/recipes/fluid_metal_tank_steel.json rename to src/main/resources/data/basepower/recipes/fluid_metal_tank_steel.json index 2045c3b2..37733d05 100644 --- a/src/main/resources/data/poweradvantage/recipes/fluid_metal_tank_steel.json +++ b/src/main/resources/data/basepower/recipes/fluid_metal_tank_steel.json @@ -14,16 +14,16 @@ } }, "result": { - "item": "poweradvantage:fluid_metal_tank" + "item": "basepower:fluid_metal_tank" }, - "group": "poweradvantage:fluid_metal_tank", + "group": "basepower:fluid_metal_tank", "conditions": [ { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/steel" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:pipes" } ] diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_copper_apocalyptic.json b/src/main/resources/data/basepower/recipes/fluid_pipe_copper_apocalyptic.json similarity index 64% rename from src/main/resources/data/poweradvantage/recipes/fluid_pipe_copper_apocalyptic.json rename to src/main/resources/data/basepower/recipes/fluid_pipe_copper_apocalyptic.json index fb955d41..50fbd0c6 100644 --- a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_copper_apocalyptic.json +++ b/src/main/resources/data/basepower/recipes/fluid_pipe_copper_apocalyptic.json @@ -11,18 +11,18 @@ } }, "result": { - "item": "poweradvantage:fluid_pipe", + "item": "basepower:fluid_pipe", "count": 3 }, "conditions": [ { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "APOCALYPTIC" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/copper" } ], - "group": "poweradvantage:fluid_pipe" + "group": "basepower:fluid_pipe" } diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_copper_normal.json b/src/main/resources/data/basepower/recipes/fluid_pipe_copper_normal.json similarity index 64% rename from src/main/resources/data/poweradvantage/recipes/fluid_pipe_copper_normal.json rename to src/main/resources/data/basepower/recipes/fluid_pipe_copper_normal.json index 28b0f330..e94d98eb 100644 --- a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_copper_normal.json +++ b/src/main/resources/data/basepower/recipes/fluid_pipe_copper_normal.json @@ -11,18 +11,18 @@ } }, "result": { - "item": "poweradvantage:fluid_pipe", + "item": "basepower:fluid_pipe", "count": 6 }, "conditions": [ { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "NORMAL" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/copper" } ], - "group": "poweradvantage:fluid_pipe" + "group": "basepower:fluid_pipe" } diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_apocalyptic.json b/src/main/resources/data/basepower/recipes/fluid_pipe_iron_apocalyptic.json similarity index 64% rename from src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_apocalyptic.json rename to src/main/resources/data/basepower/recipes/fluid_pipe_iron_apocalyptic.json index 304275bb..36d5c6b9 100644 --- a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_apocalyptic.json +++ b/src/main/resources/data/basepower/recipes/fluid_pipe_iron_apocalyptic.json @@ -11,18 +11,18 @@ } }, "result": { - "item": "poweradvantage:fluid_pipe", + "item": "basepower:fluid_pipe", "count": 3 }, "conditions": [ { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "APOCALYPTIC" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/iron" } ], - "group": "poweradvantage:fluid_pipe" + "group": "basepower:fluid_pipe" } diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_normal.json b/src/main/resources/data/basepower/recipes/fluid_pipe_iron_normal.json similarity index 63% rename from src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_normal.json rename to src/main/resources/data/basepower/recipes/fluid_pipe_iron_normal.json index 138d1a20..86a88671 100644 --- a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_normal.json +++ b/src/main/resources/data/basepower/recipes/fluid_pipe_iron_normal.json @@ -11,18 +11,18 @@ } }, "result": { - "item": "poweradvantage:fluid_pipe", + "item": "basepower:fluid_pipe", "count": 6 }, "conditions": [ { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "NORMAL" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/iron" } ], - "group": "poweradvantage:fluid_pipe" + "group": "basepower:fluid_pipe" } diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_tech.json b/src/main/resources/data/basepower/recipes/fluid_pipe_iron_tech.json similarity index 64% rename from src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_tech.json rename to src/main/resources/data/basepower/recipes/fluid_pipe_iron_tech.json index ecfae1ea..8d6d6ce8 100644 --- a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_tech.json +++ b/src/main/resources/data/basepower/recipes/fluid_pipe_iron_tech.json @@ -11,18 +11,18 @@ } }, "result": { - "item": "poweradvantage:fluid_pipe", + "item": "basepower:fluid_pipe", "count": 6 }, "conditions": [ { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "TECH_PROGRESSION" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/iron" } ], - "group": "poweradvantage:fluid_pipe" + "group": "basepower:fluid_pipe" } diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_lead_apocalyptic.json b/src/main/resources/data/basepower/recipes/fluid_pipe_lead_apocalyptic.json similarity index 64% rename from src/main/resources/data/poweradvantage/recipes/fluid_pipe_lead_apocalyptic.json rename to src/main/resources/data/basepower/recipes/fluid_pipe_lead_apocalyptic.json index d5d4c736..b6a655a8 100644 --- a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_lead_apocalyptic.json +++ b/src/main/resources/data/basepower/recipes/fluid_pipe_lead_apocalyptic.json @@ -11,18 +11,18 @@ } }, "result": { - "item": "poweradvantage:fluid_pipe", + "item": "basepower:fluid_pipe", "count": 3 }, "conditions": [ { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "APOCALYPTIC" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/lead" } ], - "group": "poweradvantage:fluid_pipe" + "group": "basepower:fluid_pipe" } diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_storage_tank.json b/src/main/resources/data/basepower/recipes/fluid_storage_tank.json similarity index 64% rename from src/main/resources/data/poweradvantage/recipes/fluid_storage_tank.json rename to src/main/resources/data/basepower/recipes/fluid_storage_tank.json index 65034d0e..fd205290 100644 --- a/src/main/resources/data/poweradvantage/recipes/fluid_storage_tank.json +++ b/src/main/resources/data/basepower/recipes/fluid_storage_tank.json @@ -14,16 +14,16 @@ } }, "result": { - "item": "poweradvantage:fluid_storage_tank" + "item": "basepower:fluid_storage_tank" }, - "group": "poweradvantage:fluid_storage_tank", + "group": "basepower:fluid_storage_tank", "conditions": [ { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/plastic" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:pipes" } ] diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_switch.json b/src/main/resources/data/basepower/recipes/fluid_switch.json similarity index 67% rename from src/main/resources/data/poweradvantage/recipes/fluid_switch.json rename to src/main/resources/data/basepower/recipes/fluid_switch.json index c5614c49..2e3b9dc0 100644 --- a/src/main/resources/data/poweradvantage/recipes/fluid_switch.json +++ b/src/main/resources/data/basepower/recipes/fluid_switch.json @@ -16,16 +16,16 @@ } }, "result": { - "item": "poweradvantage:fluid_switch" + "item": "basepower:fluid_switch" }, - "group": "poweradvantage:fluid_switch", + "group": "basepower:fluid_switch", "conditions": [ { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:frames/steel" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:pipes" } ] diff --git a/src/main/resources/data/poweradvantage/recipes/item_conveyor.json b/src/main/resources/data/basepower/recipes/item_conveyor.json similarity index 69% rename from src/main/resources/data/poweradvantage/recipes/item_conveyor.json rename to src/main/resources/data/basepower/recipes/item_conveyor.json index ecbcede0..ea0c68d7 100644 --- a/src/main/resources/data/poweradvantage/recipes/item_conveyor.json +++ b/src/main/resources/data/basepower/recipes/item_conveyor.json @@ -17,17 +17,17 @@ } }, "result": { - "item": "poweradvantage:item_conveyor", + "item": "basepower:item_conveyor", "count": 5 }, - "group": "poweradvantage:item_conveyor", + "group": "basepower:item_conveyor", "conditions": [ { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:plates/steel" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:sprockets" } ] diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_block.json b/src/main/resources/data/basepower/recipes/item_filter_block.json similarity index 66% rename from src/main/resources/data/poweradvantage/recipes/item_filter_block.json rename to src/main/resources/data/basepower/recipes/item_filter_block.json index 1e341238..3d80c30a 100644 --- a/src/main/resources/data/poweradvantage/recipes/item_filter_block.json +++ b/src/main/resources/data/basepower/recipes/item_filter_block.json @@ -7,7 +7,7 @@ ], "key": { "x": { - "item": "poweradvantage:item_conveyor" + "item": "basepower:item_conveyor" }, "y": { "item": "minecraft:stone", @@ -18,7 +18,7 @@ } }, "result": { - "item": "poweradvantage:item_filter_block" + "item": "basepower:item_filter_block" }, - "group": "poweradvantage:item_filter_block" + "group": "basepower:item_filter_block" } diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_food.json b/src/main/resources/data/basepower/recipes/item_filter_food.json similarity index 65% rename from src/main/resources/data/poweradvantage/recipes/item_filter_food.json rename to src/main/resources/data/basepower/recipes/item_filter_food.json index 624079b6..794a1a07 100644 --- a/src/main/resources/data/poweradvantage/recipes/item_filter_food.json +++ b/src/main/resources/data/basepower/recipes/item_filter_food.json @@ -7,7 +7,7 @@ ], "key": { "x": { - "item": "poweradvantage:item_conveyor" + "item": "basepower:item_conveyor" }, "y": { "item": "minecraft:bread" @@ -17,7 +17,7 @@ } }, "result": { - "item": "poweradvantage:item_filter_food" + "item": "basepower:item_filter_food" }, - "group": "poweradvantage:item_filter_food" + "group": "basepower:item_filter_food" } diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_fuel.json b/src/main/resources/data/basepower/recipes/item_filter_fuel.json similarity index 66% rename from src/main/resources/data/poweradvantage/recipes/item_filter_fuel.json rename to src/main/resources/data/basepower/recipes/item_filter_fuel.json index d6a5c935..423d8a50 100644 --- a/src/main/resources/data/poweradvantage/recipes/item_filter_fuel.json +++ b/src/main/resources/data/basepower/recipes/item_filter_fuel.json @@ -7,7 +7,7 @@ ], "key": { "x": { - "item": "poweradvantage:item_conveyor" + "item": "basepower:item_conveyor" }, "y": { "item": "minecraft:coal", @@ -18,7 +18,7 @@ } }, "result": { - "item": "poweradvantage:item_filter_fuel" + "item": "basepower:item_filter_fuel" }, - "group": "poweradvantage:item_filter_fuel" + "group": "basepower:item_filter_fuel" } diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_inventory.json b/src/main/resources/data/basepower/recipes/item_filter_inventory.json similarity index 63% rename from src/main/resources/data/poweradvantage/recipes/item_filter_inventory.json rename to src/main/resources/data/basepower/recipes/item_filter_inventory.json index 597acd77..915d055a 100644 --- a/src/main/resources/data/poweradvantage/recipes/item_filter_inventory.json +++ b/src/main/resources/data/basepower/recipes/item_filter_inventory.json @@ -7,7 +7,7 @@ ], "key": { "x": { - "item": "poweradvantage:item_conveyor" + "item": "basepower:item_conveyor" }, "y": { "item": "minecraft:chest" @@ -17,7 +17,7 @@ } }, "result": { - "item": "poweradvantage:item_filter_inventory" + "item": "basepower:item_filter_inventory" }, - "group": "poweradvantage:item_filter_inventory" + "group": "basepower:item_filter_inventory" } diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_ore.json b/src/main/resources/data/basepower/recipes/item_filter_ore.json similarity index 64% rename from src/main/resources/data/poweradvantage/recipes/item_filter_ore.json rename to src/main/resources/data/basepower/recipes/item_filter_ore.json index acc6ad2b..2cc51d2a 100644 --- a/src/main/resources/data/poweradvantage/recipes/item_filter_ore.json +++ b/src/main/resources/data/basepower/recipes/item_filter_ore.json @@ -7,7 +7,7 @@ ], "key": { "x": { - "item": "poweradvantage:item_conveyor" + "item": "basepower:item_conveyor" }, "y": { "tag": "forge:ingots/gold" @@ -17,12 +17,12 @@ } }, "result": { - "item": "poweradvantage:item_filter_ore" + "item": "basepower:item_filter_ore" }, - "group": "poweradvantage:item_filter_ore", + "group": "basepower:item_filter_ore", "conditions": [ { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/gold" } ] diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_overflow.json b/src/main/resources/data/basepower/recipes/item_filter_overflow.json similarity index 57% rename from src/main/resources/data/poweradvantage/recipes/item_filter_overflow.json rename to src/main/resources/data/basepower/recipes/item_filter_overflow.json index ce858ccc..8b6bf07f 100644 --- a/src/main/resources/data/poweradvantage/recipes/item_filter_overflow.json +++ b/src/main/resources/data/basepower/recipes/item_filter_overflow.json @@ -6,14 +6,14 @@ ], "key": { "x": { - "item": "poweradvantage:item_conveyor" + "item": "basepower:item_conveyor" }, "z": { "item": "minecraft:oak_pressure_plate" } }, "result": { - "item": "poweradvantage:item_filter_overflow" + "item": "basepower:item_filter_overflow" }, - "group": "poweradvantage:item_filter_overflow" + "group": "basepower:item_filter_overflow" } diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_plant.json b/src/main/resources/data/basepower/recipes/item_filter_plant.json similarity index 64% rename from src/main/resources/data/poweradvantage/recipes/item_filter_plant.json rename to src/main/resources/data/basepower/recipes/item_filter_plant.json index 5d4f1c23..1d0761b5 100644 --- a/src/main/resources/data/poweradvantage/recipes/item_filter_plant.json +++ b/src/main/resources/data/basepower/recipes/item_filter_plant.json @@ -7,7 +7,7 @@ ], "key": { "x": { - "item": "poweradvantage:item_conveyor" + "item": "basepower:item_conveyor" }, "y": { "tag": "minecraft:saplings" @@ -17,7 +17,7 @@ } }, "result": { - "item": "poweradvantage:item_filter_plant" + "item": "basepower:item_filter_plant" }, - "group": "poweradvantage:item_filter_plant" + "group": "basepower:item_filter_plant" } diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_smelt.json b/src/main/resources/data/basepower/recipes/item_filter_smelt.json similarity index 64% rename from src/main/resources/data/poweradvantage/recipes/item_filter_smelt.json rename to src/main/resources/data/basepower/recipes/item_filter_smelt.json index 04e84649..71df57a1 100644 --- a/src/main/resources/data/poweradvantage/recipes/item_filter_smelt.json +++ b/src/main/resources/data/basepower/recipes/item_filter_smelt.json @@ -7,7 +7,7 @@ ], "key": { "x": { - "item": "poweradvantage:item_conveyor" + "item": "basepower:item_conveyor" }, "y": { "item": "minecraft:furnace" @@ -17,7 +17,7 @@ } }, "result": { - "item": "poweradvantage:item_filter_smelt" + "item": "basepower:item_filter_smelt" }, - "group": "poweradvantage:item_filter_smelt" + "group": "basepower:item_filter_smelt" } diff --git a/src/main/resources/data/poweradvantage/recipes/rotator_tool.json b/src/main/resources/data/basepower/recipes/rotator_tool.json similarity index 65% rename from src/main/resources/data/poweradvantage/recipes/rotator_tool.json rename to src/main/resources/data/basepower/recipes/rotator_tool.json index d90ea08a..70b03cd7 100644 --- a/src/main/resources/data/poweradvantage/recipes/rotator_tool.json +++ b/src/main/resources/data/basepower/recipes/rotator_tool.json @@ -14,16 +14,16 @@ } }, "result": { - "item": "poweradvantage:rotator_tool" + "item": "basepower:rotator_tool" }, - "group": "poweradvantage:rotator_tool", + "group": "basepower:rotator_tool", "conditions": [ { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/iron" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:sprockets" } ] diff --git a/src/main/resources/data/poweradvantage/recipes/sprocket_bronze_nugget_tech_no_rod.json b/src/main/resources/data/basepower/recipes/sprocket_bronze_nugget_tech_no_rod.json similarity index 64% rename from src/main/resources/data/poweradvantage/recipes/sprocket_bronze_nugget_tech_no_rod.json rename to src/main/resources/data/basepower/recipes/sprocket_bronze_nugget_tech_no_rod.json index b699bc2c..8fc3c678 100644 --- a/src/main/resources/data/poweradvantage/recipes/sprocket_bronze_nugget_tech_no_rod.json +++ b/src/main/resources/data/basepower/recipes/sprocket_bronze_nugget_tech_no_rod.json @@ -14,26 +14,26 @@ } }, "result": { - "item": "poweradvantage:sprocket", + "item": "basepower:sprocket", "count": 4 }, "conditions": [ { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "TECH_PROGRESSION" }, { - "type": "poweradvantage:tag_empty", + "type": "basepower:tag_empty", "tag": "forge:rods" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/steel" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:nuggets/bronze" } ], - "group": "poweradvantage:sprocket" + "group": "basepower:sprocket" } diff --git a/src/main/resources/data/poweradvantage/recipes/sprocket_rod_normal.json b/src/main/resources/data/basepower/recipes/sprocket_rod_normal.json similarity index 64% rename from src/main/resources/data/poweradvantage/recipes/sprocket_rod_normal.json rename to src/main/resources/data/basepower/recipes/sprocket_rod_normal.json index 5ce0069a..cb030cbe 100644 --- a/src/main/resources/data/poweradvantage/recipes/sprocket_rod_normal.json +++ b/src/main/resources/data/basepower/recipes/sprocket_rod_normal.json @@ -14,22 +14,22 @@ } }, "result": { - "item": "poweradvantage:sprocket", + "item": "basepower:sprocket", "count": 4 }, "conditions": [ { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "NORMAL" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/steel" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:rods" } ], - "group": "poweradvantage:sprocket" + "group": "basepower:sprocket" } diff --git a/src/main/resources/data/poweradvantage/recipes/sprocket_rod_tech.json b/src/main/resources/data/basepower/recipes/sprocket_rod_tech.json similarity index 65% rename from src/main/resources/data/poweradvantage/recipes/sprocket_rod_tech.json rename to src/main/resources/data/basepower/recipes/sprocket_rod_tech.json index 4a080402..2f361a78 100644 --- a/src/main/resources/data/poweradvantage/recipes/sprocket_rod_tech.json +++ b/src/main/resources/data/basepower/recipes/sprocket_rod_tech.json @@ -14,22 +14,22 @@ } }, "result": { - "item": "poweradvantage:sprocket", + "item": "basepower:sprocket", "count": 4 }, "conditions": [ { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "TECH_PROGRESSION" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/steel" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:rods" } ], - "group": "poweradvantage:sprocket" + "group": "basepower:sprocket" } diff --git a/src/main/resources/data/poweradvantage/recipes/sprocket_stickwood_normal.json b/src/main/resources/data/basepower/recipes/sprocket_stickwood_normal.json similarity index 65% rename from src/main/resources/data/poweradvantage/recipes/sprocket_stickwood_normal.json rename to src/main/resources/data/basepower/recipes/sprocket_stickwood_normal.json index 90d6d9fc..d0a47360 100644 --- a/src/main/resources/data/poweradvantage/recipes/sprocket_stickwood_normal.json +++ b/src/main/resources/data/basepower/recipes/sprocket_stickwood_normal.json @@ -14,22 +14,22 @@ } }, "result": { - "item": "poweradvantage:sprocket", + "item": "basepower:sprocket", "count": 4 }, "conditions": [ { - "type": "poweradvantage:recipe_mode", + "type": "basepower:recipe_mode", "mode": "NORMAL" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:ingots/steel" }, { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:rods/wooden" } ], - "group": "poweradvantage:sprocket" + "group": "basepower:sprocket" } diff --git a/src/main/resources/data/poweradvantage/recipes/starch_from_potato.json b/src/main/resources/data/basepower/recipes/starch_from_potato.json similarity index 63% rename from src/main/resources/data/poweradvantage/recipes/starch_from_potato.json rename to src/main/resources/data/basepower/recipes/starch_from_potato.json index 1333f013..cabc1111 100644 --- a/src/main/resources/data/poweradvantage/recipes/starch_from_potato.json +++ b/src/main/resources/data/basepower/recipes/starch_from_potato.json @@ -6,6 +6,6 @@ { "item": "minecraft:poisonous_potato" } ] ], - "result": { "item": "poweradvantage:starch", "count": 1 }, - "group": "poweradvantage:starch" + "result": { "item": "basepower:starch", "count": 1 }, + "group": "basepower:starch" } diff --git a/src/main/resources/data/poweradvantage/recipes/steel_frame.json b/src/main/resources/data/basepower/recipes/steel_frame.json similarity index 65% rename from src/main/resources/data/poweradvantage/recipes/steel_frame.json rename to src/main/resources/data/basepower/recipes/steel_frame.json index b1452d5a..a9c6b2ea 100644 --- a/src/main/resources/data/poweradvantage/recipes/steel_frame.json +++ b/src/main/resources/data/basepower/recipes/steel_frame.json @@ -11,12 +11,12 @@ } }, "result": { - "item": "poweradvantage:steel_frame" + "item": "basepower:steel_frame" }, - "group": "poweradvantage:steel_frame", + "group": "basepower:steel_frame", "conditions": [ { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:bars/steel" } ] diff --git a/src/main/resources/data/poweradvantage/recipes/still.json b/src/main/resources/data/basepower/recipes/still.json similarity index 73% rename from src/main/resources/data/poweradvantage/recipes/still.json rename to src/main/resources/data/basepower/recipes/still.json index ddb37426..46e0a417 100644 --- a/src/main/resources/data/poweradvantage/recipes/still.json +++ b/src/main/resources/data/basepower/recipes/still.json @@ -16,12 +16,12 @@ } }, "result": { - "item": "poweradvantage:still" + "item": "basepower:still" }, - "group": "poweradvantage:still", + "group": "basepower:still", "conditions": [ { - "type": "poweradvantage:tag_not_empty", + "type": "basepower:tag_not_empty", "tag": "forge:pipes" } ] diff --git a/src/main/resources/data/forge/tags/items/frames/steel.json b/src/main/resources/data/forge/tags/items/frames/steel.json index c0f5e8e6..2ec93a5c 100644 --- a/src/main/resources/data/forge/tags/items/frames/steel.json +++ b/src/main/resources/data/forge/tags/items/frames/steel.json @@ -1,6 +1,6 @@ { "replace": false, "values": [ - "poweradvantage:steel_frame" + "basepower:steel_frame" ] } diff --git a/src/main/resources/data/forge/tags/items/gears.json b/src/main/resources/data/forge/tags/items/gears.json index b3b11578..28b81e98 100644 --- a/src/main/resources/data/forge/tags/items/gears.json +++ b/src/main/resources/data/forge/tags/items/gears.json @@ -1,6 +1,6 @@ { "replace": false, "values": [ - "poweradvantage:sprocket" + "basepower:sprocket" ] } diff --git a/src/main/resources/data/forge/tags/items/gears/steel.json b/src/main/resources/data/forge/tags/items/gears/steel.json index b3b11578..28b81e98 100644 --- a/src/main/resources/data/forge/tags/items/gears/steel.json +++ b/src/main/resources/data/forge/tags/items/gears/steel.json @@ -1,6 +1,6 @@ { "replace": false, "values": [ - "poweradvantage:sprocket" + "basepower:sprocket" ] } diff --git a/src/main/resources/data/forge/tags/items/ingots/plastic.json b/src/main/resources/data/forge/tags/items/ingots/plastic.json index 6f60c386..d922cf4f 100644 --- a/src/main/resources/data/forge/tags/items/ingots/plastic.json +++ b/src/main/resources/data/forge/tags/items/ingots/plastic.json @@ -1,6 +1,6 @@ { "replace": false, "values": [ - "poweradvantage:bioplastic_ingot" + "basepower:bioplastic_ingot" ] } diff --git a/src/main/resources/data/forge/tags/items/pipes.json b/src/main/resources/data/forge/tags/items/pipes.json index c62f9bb8..8c70e3c2 100644 --- a/src/main/resources/data/forge/tags/items/pipes.json +++ b/src/main/resources/data/forge/tags/items/pipes.json @@ -1,6 +1,6 @@ { "replace": false, "values": [ - "poweradvantage:fluid_pipe" + "basepower:fluid_pipe" ] } diff --git a/src/main/resources/data/forge/tags/items/plastic.json b/src/main/resources/data/forge/tags/items/plastic.json index 6f60c386..d922cf4f 100644 --- a/src/main/resources/data/forge/tags/items/plastic.json +++ b/src/main/resources/data/forge/tags/items/plastic.json @@ -1,6 +1,6 @@ { "replace": false, "values": [ - "poweradvantage:bioplastic_ingot" + "basepower:bioplastic_ingot" ] } diff --git a/src/main/resources/data/forge/tags/items/sprockets.json b/src/main/resources/data/forge/tags/items/sprockets.json index b3b11578..28b81e98 100644 --- a/src/main/resources/data/forge/tags/items/sprockets.json +++ b/src/main/resources/data/forge/tags/items/sprockets.json @@ -1,6 +1,6 @@ { "replace": false, "values": [ - "poweradvantage:sprocket" + "basepower:sprocket" ] } diff --git a/src/main/resources/data/forge/tags/items/sprockets/steel.json b/src/main/resources/data/forge/tags/items/sprockets/steel.json index b3b11578..28b81e98 100644 --- a/src/main/resources/data/forge/tags/items/sprockets/steel.json +++ b/src/main/resources/data/forge/tags/items/sprockets/steel.json @@ -1,6 +1,6 @@ { "replace": false, "values": [ - "poweradvantage:sprocket" + "basepower:sprocket" ] } diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_electricity.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_electricity.json deleted file mode 100644 index 841e0a21..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_electricity.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:converter_rf_electricity" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_quantum.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_quantum.json deleted file mode 100644 index 231a57a9..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_quantum.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:converter_rf_quantum" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_tr_electricity.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_tr_electricity.json deleted file mode 100644 index 065fbc45..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_tr_electricity.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:converter_tr_electricity" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_electricity.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_electricity.json deleted file mode 100644 index 42ebe1ce..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_electricity.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:infinite_electricity" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_quantum.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_quantum.json deleted file mode 100644 index 104c117f..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_quantum.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:infinite_quantum" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_steam.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_steam.json deleted file mode 100644 index 9192aa7b..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_steam.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:infinite_steam" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_conveyor.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_conveyor.json deleted file mode 100644 index b86de0c3..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_conveyor.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:item_conveyor" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_block.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_block.json deleted file mode 100644 index 49abd9f3..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_block.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:item_filter_block" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_food.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_food.json deleted file mode 100644 index d4af70a2..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_food.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:item_filter_food" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_fuel.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_fuel.json deleted file mode 100644 index 242a6e99..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_fuel.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:item_filter_fuel" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_inventory.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_inventory.json deleted file mode 100644 index 6f6f24d4..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_inventory.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:item_filter_inventory" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_ore.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_ore.json deleted file mode 100644 index 6d82c6ce..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_ore.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:item_filter_ore" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_overflow.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_overflow.json deleted file mode 100644 index 5e9446a0..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_overflow.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:item_filter_overflow" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_plant.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_plant.json deleted file mode 100644 index e7894efb..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_plant.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:item_filter_plant" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_smelt.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_smelt.json deleted file mode 100644 index 9d3df40c..00000000 --- a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_smelt.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "minecraft:block", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "poweradvantage:item_filter_smelt" - } - ], - "conditions": [ - { - "condition": "minecraft:survives_explosion" - } - ] - } - ] -} diff --git a/src/main/resources/data/poweradvantage/recipes/bioplastic_from_starch.json b/src/main/resources/data/poweradvantage/recipes/bioplastic_from_starch.json deleted file mode 100644 index 2eea1c7b..00000000 --- a/src/main/resources/data/poweradvantage/recipes/bioplastic_from_starch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "minecraft:smelting", - "ingredient": { - "item": "poweradvantage:starch" - }, - "result": "poweradvantage:bioplastic_ingot", - "experience": 0.1, - "cookingtime": 200, - "conditions": [], - "group": "poweradvantage:bioplastic_ingot" -} diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_block.json b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_block.json deleted file mode 100644 index dfbcd2d6..00000000 --- a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_block.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "basemetals:crushing", - "ingredient": { - "item": "poweradvantage:item_filter_block" - }, - "result": { - "item": "basemetals:steel_plate", - "count": 1 - }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "poweradvantage:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "poweradvantage:tag_not_empty", - "tag": "forge:plates/steel" - } - ] -} diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_inventory.json b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_inventory.json deleted file mode 100644 index c3121aaf..00000000 --- a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_inventory.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "basemetals:crushing", - "ingredient": { - "item": "poweradvantage:item_filter_inventory" - }, - "result": { - "item": "basemetals:steel_plate", - "count": 1 - }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "poweradvantage:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "poweradvantage:tag_not_empty", - "tag": "forge:plates/steel" - } - ] -} diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_overflow.json b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_overflow.json deleted file mode 100644 index 87033def..00000000 --- a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_overflow.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "basemetals:crushing", - "ingredient": { - "item": "poweradvantage:item_filter_overflow" - }, - "result": { - "item": "basemetals:steel_plate", - "count": 1 - }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "poweradvantage:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "poweradvantage:tag_not_empty", - "tag": "forge:plates/steel" - } - ] -} diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_plant.json b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_plant.json deleted file mode 100644 index 07236306..00000000 --- a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_plant.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "basemetals:crushing", - "ingredient": { - "item": "poweradvantage:item_filter_plant" - }, - "result": { - "item": "basemetals:steel_plate", - "count": 1 - }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "poweradvantage:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "poweradvantage:tag_not_empty", - "tag": "forge:plates/steel" - } - ] -} diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_smelt.json b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_smelt.json deleted file mode 100644 index bdf912e2..00000000 --- a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_smelt.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "basemetals:crushing", - "ingredient": { - "item": "poweradvantage:item_filter_smelt" - }, - "result": { - "item": "basemetals:steel_plate", - "count": 1 - }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "poweradvantage:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "poweradvantage:tag_not_empty", - "tag": "forge:plates/steel" - } - ] -} diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info index a78e7516..611a5e31 100644 --- a/src/main/resources/mcmod.info +++ b/src/main/resources/mcmod.info @@ -1,11 +1,11 @@ [ { - "modid": "poweradvantage", - "name": "Power Advantage", + "modid": "basepower", + "name": "basePower", "description": "API for power DrCyano's power systems mods.", "version": "${version}", "mcversion": "${mcversion}", - "url": "https://github.com/MinecraftModDevelopmentMods/PowerAdvantage", + "url": "https://github.com/MinecraftModDevelopmentMods/basePower", "updateUrl": "", "authorList": ["DrCyano (aka Cyanobacterium)"], "credits": "The Forge and FML guys, for making the Forge modding API (you guys are awesome), engineers everywhere, and the mod author DrCyano (aka Cyanobacterium)", diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index 97ba2398..99861c4b 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,6 +1,6 @@ { "pack": { "description": "Power Advantage resources", - "pack_format": 6 + "pack_format": 7 } } diff --git a/src/test/java/cyano/poweradvantage/api/fluid/FluidNetworkApiTest.java b/src/test/java/cyano/poweradvantage/api/fluid/FluidNetworkApiTest.java index 76729aa7..038420cb 100644 --- a/src/test/java/cyano/poweradvantage/api/fluid/FluidNetworkApiTest.java +++ b/src/test/java/cyano/poweradvantage/api/fluid/FluidNetworkApiTest.java @@ -10,15 +10,15 @@ import cyano.poweradvantage.api.IPowerMachine; import cyano.poweradvantage.api.PowerConnectorContext; import cyano.poweradvantage.api.PowerRequest; -import net.minecraft.fluid.EmptyFluid; -import net.minecraft.fluid.Fluid; +import net.minecraft.world.level.material.EmptyFluid; +import net.minecraft.world.level.material.Fluid; public class FluidNetworkApiTest { private static final ConduitType WATER = new ConduitType("test-water"); private static final Fluid TEST_FLUID = testFluid(); private static Fluid testFluid() { - net.minecraft.util.registry.Bootstrap.bootStrap(); + net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); return new EmptyFluid(); } diff --git a/src/test/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManagerTest.java b/src/test/java/zone/moddev/mc/basepower/conduitnetwork/ConduitNetworkManagerTest.java similarity index 75% rename from src/test/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManagerTest.java rename to src/test/java/zone/moddev/mc/basepower/conduitnetwork/ConduitNetworkManagerTest.java index 9e7d4b64..76cf5857 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManagerTest.java +++ b/src/test/java/zone/moddev/mc/basepower/conduitnetwork/ConduitNetworkManagerTest.java @@ -1,17 +1,17 @@ -package zone.moddev.mc.poweradvantage.conduitnetwork; +package zone.moddev.mc.basepower.conduitnetwork; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import cyano.poweradvantage.api.ConduitType; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.ChunkPos; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ChunkPos; import org.junit.Test; -import zone.moddev.mc.poweradvantage.math.BlockPos4D; +import zone.moddev.mc.basepower.math.BlockPos4D; public class ConduitNetworkManagerTest { @org.junit.BeforeClass public static void bootstrap() { - net.minecraft.util.registry.Bootstrap.bootStrap(); + net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); } private final ConduitNetworkManager manager = new ConduitNetworkManager(new ConduitType("test")); @@ -24,7 +24,7 @@ public void unloadingAnIntermediateChunkInvalidatesTheWholeNetwork() { BlockPos4D source = pos(0, 13, 8), bridge = pos(0, 20, 8), sink = pos(0, 40, 8); manager.addBlockToNetwork(source, bridge); manager.addBlockToNetwork(source, sink); - manager.invalidateChunk(net.minecraft.world.World.OVERWORLD, new ChunkPos(1, 0)); + manager.invalidateChunk(net.minecraft.world.level.Level.OVERWORLD, new ChunkPos(1, 0)); assertFalse(manager.isValidatedNetwork(source)); assertFalse(manager.isValidatedNetwork(bridge)); assertFalse(manager.isValidatedNetwork(sink)); @@ -35,7 +35,7 @@ public void loadingAPipeOnlyChunkInvalidatesBothBoundaryNetworks() { BlockPos4D left = pos(0, 15, 8), right = pos(0, 32, 8); manager.createNewNetwork(left); manager.createNewNetwork(right); - manager.invalidateChunk(net.minecraft.world.World.OVERWORLD, new ChunkPos(1, 0)); + manager.invalidateChunk(net.minecraft.world.level.Level.OVERWORLD, new ChunkPos(1, 0)); assertFalse(manager.isValidatedNetwork(left)); assertFalse(manager.isValidatedNetwork(right)); } @@ -44,7 +44,7 @@ public void loadingAPipeOnlyChunkInvalidatesBothBoundaryNetworks() { public void allFourChunkBoundariesAreInvalidated() { BlockPos4D[] edges = {pos(0, 15, 20), pos(0, 32, 20), pos(0, 20, 15), pos(0, 20, 32)}; for (BlockPos4D edge : edges) manager.createNewNetwork(edge); - manager.invalidateChunk(net.minecraft.world.World.OVERWORLD, new ChunkPos(1, 1)); + manager.invalidateChunk(net.minecraft.world.level.Level.OVERWORLD, new ChunkPos(1, 1)); for (BlockPos4D edge : edges) assertFalse(manager.isValidatedNetwork(edge)); } @@ -52,7 +52,7 @@ public void allFourChunkBoundariesAreInvalidated() { public void negativeChunkCoordinatesUseTheSameBoundaryRules() { BlockPos4D edge = pos(0, -17, -8); manager.createNewNetwork(edge); - manager.invalidateChunk(net.minecraft.world.World.OVERWORLD, new ChunkPos(-1, -1)); + manager.invalidateChunk(net.minecraft.world.level.Level.OVERWORLD, new ChunkPos(-1, -1)); assertFalse(manager.isValidatedNetwork(edge)); } @@ -62,7 +62,7 @@ public void unrelatedNetworksAndDimensionsRemainCached() { manager.createNewNetwork(nearby); manager.createNewNetwork(distant); manager.createNewNetwork(otherDimension); - manager.invalidateChunk(net.minecraft.world.World.OVERWORLD, new ChunkPos(1, 0)); + manager.invalidateChunk(net.minecraft.world.level.Level.OVERWORLD, new ChunkPos(1, 0)); assertTrue(manager.isValidatedNetwork(nearby)); assertTrue(manager.isValidatedNetwork(distant)); assertTrue(manager.isValidatedNetwork(otherDimension)); diff --git a/src/test/java/zone/moddev/mc/poweradvantage/conduitnetwork/PowerTransferAccountingTest.java b/src/test/java/zone/moddev/mc/basepower/conduitnetwork/PowerTransferAccountingTest.java similarity index 97% rename from src/test/java/zone/moddev/mc/poweradvantage/conduitnetwork/PowerTransferAccountingTest.java rename to src/test/java/zone/moddev/mc/basepower/conduitnetwork/PowerTransferAccountingTest.java index 070739c1..709e5940 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/conduitnetwork/PowerTransferAccountingTest.java +++ b/src/test/java/zone/moddev/mc/basepower/conduitnetwork/PowerTransferAccountingTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.conduitnetwork; +package zone.moddev.mc.basepower.conduitnetwork; import cyano.poweradvantage.api.*; import java.util.Arrays; diff --git a/src/test/java/zone/moddev/mc/poweradvantage/init/FluidLookupTest.java b/src/test/java/zone/moddev/mc/basepower/init/FluidLookupTest.java similarity index 60% rename from src/test/java/zone/moddev/mc/poweradvantage/init/FluidLookupTest.java rename to src/test/java/zone/moddev/mc/basepower/init/FluidLookupTest.java index 030e6c9c..96e8d450 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/init/FluidLookupTest.java +++ b/src/test/java/zone/moddev/mc/basepower/init/FluidLookupTest.java @@ -1,18 +1,18 @@ -package zone.moddev.mc.poweradvantage.init; +package zone.moddev.mc.basepower.init; import cyano.poweradvantage.api.fluid.FluidStack; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.CompoundTag; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.*; public class FluidLookupTest { - @BeforeClass public static void bootstrap() { net.minecraft.util.registry.Bootstrap.bootStrap(); } + @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); } @Test public void resolvesLegacyUnqualifiedVanillaNamesInsteadOfTheEmptyDefault() { - assertSame(net.minecraft.fluid.Fluids.WATER, Fluids.byName("water")); - assertSame(net.minecraft.fluid.Fluids.LAVA, Fluids.byName("lava")); - assertSame(net.minecraft.fluid.Fluids.WATER, Fluids.byName("minecraft:water")); + assertSame(net.minecraft.world.level.material.Fluids.WATER, Fluids.byName("water")); + assertSame(net.minecraft.world.level.material.Fluids.LAVA, Fluids.byName("lava")); + assertSame(net.minecraft.world.level.material.Fluids.WATER, Fluids.byName("minecraft:water")); } @Test public void unknownNamesAreNotReplacedByAnEmptyFluid() { @@ -22,15 +22,15 @@ public class FluidLookupTest { } @Test public void retainsAmountAndNbtWhenReadingLegacySavedWater() { - CompoundNBT data = new CompoundNBT(); + CompoundTag data = new CompoundTag(); data.putString("FluidName", "water"); data.putInt("Amount", 700); - CompoundNBT tag = new CompoundNBT(); + CompoundTag tag = new CompoundTag(); tag.putInt("batch", 37); data.put("Tag", tag); FluidStack stack = FluidStack.loadFluidStackFromNBT(data); assertNotNull(stack); - assertSame(net.minecraft.fluid.Fluids.WATER, stack.getFluid()); + assertSame(net.minecraft.world.level.material.Fluids.WATER, stack.getFluid()); assertEquals(700, stack.amount); assertEquals(tag, stack.tag); } diff --git a/src/test/java/zone/moddev/mc/poweradvantage/machines/conveyors/ConveyorTransferTest.java b/src/test/java/zone/moddev/mc/basepower/machines/conveyors/ConveyorTransferTest.java similarity index 74% rename from src/test/java/zone/moddev/mc/poweradvantage/machines/conveyors/ConveyorTransferTest.java rename to src/test/java/zone/moddev/mc/basepower/machines/conveyors/ConveyorTransferTest.java index bcd961d2..7d0e9878 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/machines/conveyors/ConveyorTransferTest.java +++ b/src/test/java/zone/moddev/mc/basepower/machines/conveyors/ConveyorTransferTest.java @@ -1,31 +1,31 @@ -package zone.moddev.mc.poweradvantage.machines.conveyors; +package zone.moddev.mc.basepower.machines.conveyors; -import net.minecraft.inventory.Inventory; -import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; -import net.minecraft.util.Direction; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.core.Direction; import org.junit.BeforeClass; import org.junit.Test; -import zone.moddev.mc.poweradvantage.util.InventoryWrapper; +import zone.moddev.mc.basepower.util.InventoryWrapper; import static org.junit.Assert.*; public class ConveyorTransferTest { - @BeforeClass public static void bootstrap() { net.minecraft.util.registry.Bootstrap.bootStrap(); } + @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); } @Test public void laterEmptySlotRemainsAnAvailableDestination() { - Inventory destination = new Inventory(2); + SimpleContainer destination = new SimpleContainer(2); destination.setItem(0, new ItemStack(Items.COBBLESTONE, 64)); assertTrue(TileEntityConveyor.canInsertItemInto(new ItemStack(Items.DIRT), InventoryWrapper.wrap(destination), Direction.UP)); } @Test public void fullStackIsNotAnAvailableDestination() { - Inventory destination = new Inventory(1); + SimpleContainer destination = new SimpleContainer(1); destination.setItem(0, new ItemStack(Items.DIRT, 64)); assertFalse(TileEntityConveyor.canInsertItemInto(new ItemStack(Items.DIRT), InventoryWrapper.wrap(destination), Direction.UP)); } @Test public void transfersPreserveDifferentCustomItemData() { - Inventory source = new Inventory(2), target = new Inventory(2); + SimpleContainer source = new SimpleContainer(2), target = new SimpleContainer(2); source.setItem(0, tagged(3, "A")); source.setItem(1, tagged(3, "B")); target.setItem(0, tagged(1, "A")); for (int n = 0; n < 6; n++) assertTrue(TileEntityConveyor.transferItem(InventoryWrapper.wrap(source), Direction.EAST, InventoryWrapper.wrap(target), Direction.WEST)); diff --git a/src/test/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/WorldFluidPickupTest.java b/src/test/java/zone/moddev/mc/basepower/machines/fluidmachines/WorldFluidPickupTest.java similarity index 71% rename from src/test/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/WorldFluidPickupTest.java rename to src/test/java/zone/moddev/mc/basepower/machines/fluidmachines/WorldFluidPickupTest.java index 3c27adac..48415db2 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/WorldFluidPickupTest.java +++ b/src/test/java/zone/moddev/mc/basepower/machines/fluidmachines/WorldFluidPickupTest.java @@ -1,20 +1,20 @@ -package zone.moddev.mc.poweradvantage.machines.fluidmachines; +package zone.moddev.mc.basepower.machines.fluidmachines; import java.lang.reflect.Proxy; import java.util.concurrent.atomic.AtomicReference; -import net.minecraft.block.BlockState; -import net.minecraft.block.Blocks; -import net.minecraft.block.FlowingFluidBlock; -import net.minecraft.fluid.Fluids; -import net.minecraft.state.properties.BlockStateProperties; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.IWorld; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelAccessor; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.*; public class WorldFluidPickupTest { - @BeforeClass public static void bootstrap() { net.minecraft.util.registry.Bootstrap.bootStrap(); } + @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); } @Test public void waterloggedHostSurvivesTheNativePickup() { AtomicReference state = new AtomicReference<>(Blocks.OAK_SLAB.defaultBlockState() @@ -31,7 +31,7 @@ public class WorldFluidPickupTest { } @Test public void flowingWaterDoesNotProduceABucket() { - AtomicReference state = new AtomicReference<>(Blocks.WATER.defaultBlockState().setValue(FlowingFluidBlock.LEVEL, 3)); + AtomicReference state = new AtomicReference<>(Blocks.WATER.defaultBlockState().setValue(LiquidBlock.LEVEL, 3)); BlockState original = state.get(); assertSame(Fluids.EMPTY, FluidDrainTileEntity.pickupSource(world(state), BlockPos.ZERO, state.get())); assertSame(original, state.get()); @@ -43,8 +43,8 @@ public class WorldFluidPickupTest { assertTrue(state.get().is(Blocks.BEDROCK)); } - private static IWorld world(AtomicReference state) { - return (IWorld)Proxy.newProxyInstance(IWorld.class.getClassLoader(), new Class[]{IWorld.class}, (proxy, method, args) -> { + private static LevelAccessor world(AtomicReference state) { + return (LevelAccessor)Proxy.newProxyInstance(LevelAccessor.class.getClassLoader(), new Class[]{LevelAccessor.class}, (proxy, method, args) -> { if (method.getName().equals("setBlock")) { state.set((BlockState)args[1]); return true; } if (method.getName().equals("getBlockState")) return state.get(); throw new AssertionError("Unexpected world operation " + method.getName()); diff --git a/src/test/java/zone/moddev/mc/poweradvantage/math/BlockPos4DTest.java b/src/test/java/zone/moddev/mc/basepower/math/BlockPos4DTest.java similarity index 56% rename from src/test/java/zone/moddev/mc/poweradvantage/math/BlockPos4DTest.java rename to src/test/java/zone/moddev/mc/basepower/math/BlockPos4DTest.java index 697faa63..0ea4e69b 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/math/BlockPos4DTest.java +++ b/src/test/java/zone/moddev/mc/basepower/math/BlockPos4DTest.java @@ -1,21 +1,21 @@ -package zone.moddev.mc.poweradvantage.math; +package zone.moddev.mc.basepower.math; -import net.minecraft.nbt.IntArrayNBT; -import net.minecraft.util.RegistryKey; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.registry.Registry; -import net.minecraft.world.World; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.world.level.Level; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.*; public class BlockPos4DTest { - @BeforeClass public static void bootstrap() { net.minecraft.util.registry.Bootstrap.bootStrap(); } + @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); } @Test public void customDimensionKeysNeverShareCoordinates() { - RegistryKey first = RegistryKey.create(Registry.DIMENSION_REGISTRY, new ResourceLocation("test:first")); - RegistryKey second = RegistryKey.create(Registry.DIMENSION_REGISTRY, new ResourceLocation("test:second")); + ResourceKey first = ResourceKey.create(Registry.DIMENSION_REGISTRY, new ResourceLocation("test:first")); + ResourceKey second = ResourceKey.create(Registry.DIMENSION_REGISTRY, new ResourceLocation("test:second")); BlockPos4D pos = new BlockPos4D(first, new BlockPos(-17, 80, 32)); assertNotEquals(pos, new BlockPos4D(second, pos.pos)); assertEquals(pos, BlockPos4D.fromNBT(pos.toNBT())); @@ -23,15 +23,15 @@ public class BlockPos4DTest { } @Test public void cacheKeysCopyMutableInputPositions() { - BlockPos.Mutable source = new BlockPos.Mutable(1, 2, 3); - BlockPos4D key = new BlockPos4D(World.OVERWORLD, source); + BlockPos.MutableBlockPos source = new BlockPos.MutableBlockPos(1, 2, 3); + BlockPos4D key = new BlockPos4D(Level.OVERWORLD, source); source.set(4, 5, 6); assertEquals(new BlockPos(1, 2, 3), key.pos); } @Test public void legacyVanillaCoordinatesRetainTheirDimension() { - assertEquals(new BlockPos4D(World.NETHER, new BlockPos(1, 2, 3)), - BlockPos4D.fromNBT(new IntArrayNBT(new int[] {-1, 1, 2, 3}))); + assertEquals(new BlockPos4D(Level.NETHER, new BlockPos(1, 2, 3)), + BlockPos4D.fromNBT(new IntArrayTag(new int[] {-1, 1, 2, 3}))); } @Test(expected = IllegalArgumentException.class) diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrationsTest.java b/src/test/java/zone/moddev/mc/basepower/migration/AdvantageLegacyMigrationsTest.java similarity index 67% rename from src/test/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrationsTest.java rename to src/test/java/zone/moddev/mc/basepower/migration/AdvantageLegacyMigrationsTest.java index 81b77250..6dc4828c 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrationsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/AdvantageLegacyMigrationsTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -6,7 +6,7 @@ import cyano.poweradvantage.api.migration.LegacyMigrationApi; import java.util.Arrays; -import net.minecraft.util.ResourceLocation; +import net.minecraft.resources.ResourceLocation; import org.junit.Test; public class AdvantageLegacyMigrationsTest { @@ -22,21 +22,25 @@ public class AdvantageLegacyMigrationsTest { "converter_rf_steam", "converter_rf_electricity", "converter_rf_quantum", "converter_tr_electricity")) { assertNotNull(path, LegacyMigrationApi.blockStateCodecs().get( - new ResourceLocation("poweradvantage", path))); + new ResourceLocation("basepower", path))); + assertEquals(new ResourceLocation("basepower", path), + LegacyMigrationApi.blockTarget(new ResourceLocation("poweradvantage", path))); } } @Test public void retainsLegacyTileFluidAndBucketIdentities() { AdvantageLegacyMigrations.registerPowerAdvantage(); assertTrue(LegacyMigrationApi.handlesNamespace("poweradvantage")); - assertEquals("poweradvantage:tileentity.fluid_storage_tank", + assertTrue(LegacyMigrationApi.handlesNamespace("basepower")); + assertEquals("basepower:tileentity.fluid_storage_tank", LegacyMigrationApi.tileTarget("poweradvantage.tileentity.fluid_storage_tank")); - assertEquals("poweradvantage:tileentity.fluid_storage_tank", + assertEquals("basepower:tileentity.fluid_storage_tank", LegacyMigrationApi.tileTarget("minecraft:poweradvantage.tileentity.fluid_storage_tank")); - assertEquals("poweradvantage:crude_oil", LegacyMigrationApi.fluidTarget("crude_oil")); - assertEquals(new ResourceLocation("poweradvantage", "crude_oil_bucket"), + assertEquals("basepower:crude_oil", LegacyMigrationApi.fluidTarget("crude_oil")); + assertEquals("basepower:crude_oil", LegacyMigrationApi.fluidTarget("poweradvantage:crude_oil")); + assertEquals(new ResourceLocation("basepower", "crude_oil_bucket"), LegacyMigrationApi.bucketTarget("crude_oil")); - assertEquals(new ResourceLocation("poweradvantage", "refined_oil_bucket"), + assertEquals(new ResourceLocation("basepower", "refined_oil_bucket"), LegacyMigrationApi.bucketTarget("poweradvantage:refined_oil")); } } diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/FlattenedBiomeSnapshotTest.java b/src/test/java/zone/moddev/mc/basepower/migration/FlattenedBiomeSnapshotTest.java similarity index 70% rename from src/test/java/zone/moddev/mc/poweradvantage/migration/FlattenedBiomeSnapshotTest.java rename to src/test/java/zone/moddev/mc/basepower/migration/FlattenedBiomeSnapshotTest.java index 361074cf..807c4a22 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/FlattenedBiomeSnapshotTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/FlattenedBiomeSnapshotTest.java @@ -1,47 +1,47 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ListNBT; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; import org.junit.Test; import static org.junit.Assert.*; public class FlattenedBiomeSnapshotTest { @Test public void preservesEveryNumericIdAndUnknownModEntry() { - CompoundNBT root = root(1631); - CompoundNBT original = registries(root).getCompound("minecraft:biomes").copy(); + CompoundTag root = root(1631); + CompoundTag original = registries(root).getCompound("minecraft:biomes").copy(); assertTrue(LegacyWorldDataHook.renameFlattenedBiomeRegistry(root)); assertEquals(original, registries(root).getCompound("minecraft:worldgen/biome")); assertFalse(registries(root).contains("minecraft:biomes")); } @Test public void isIdempotent() { - CompoundNBT root = root(2230); + CompoundTag root = root(2230); assertTrue(LegacyWorldDataHook.renameFlattenedBiomeRegistry(root)); - CompoundNBT once = root.copy(); + CompoundTag once = root.copy(); assertFalse(LegacyWorldDataHook.renameFlattenedBiomeRegistry(root)); assertEquals(once, root); } @Test public void neverReinterpretsAPreFlatteningSnapshot() { - CompoundNBT root = root(1343), before = root.copy(); + CompoundTag root = root(1343), before = root.copy(); assertFalse(LegacyWorldDataHook.renameFlattenedBiomeRegistry(root)); assertEquals(before, root); } @Test public void leavesTheUppercaseLegacyArchiveUntouched() { - CompoundNBT root = root(1631); - CompoundNBT archive = new CompoundNBT(); + CompoundTag root = root(1631); + CompoundTag archive = new CompoundTag(); archive.put("Registries", registries(root).copy()); root.put("FML", archive); - CompoundNBT before = archive.copy(); + CompoundTag before = archive.copy(); assertTrue(LegacyWorldDataHook.renameFlattenedBiomeRegistry(root)); assertEquals(before, root.getCompound("FML")); } @Test public void refusesConflictingSnapshotsWithoutChangingEither() { - CompoundNBT root = root(2230); - registries(root).put("minecraft:worldgen/biome", new CompoundNBT()); - CompoundNBT before = root.copy(); + CompoundTag root = root(2230); + registries(root).put("minecraft:worldgen/biome", new CompoundTag()); + CompoundTag before = root.copy(); try { LegacyWorldDataHook.renameFlattenedBiomeRegistry(root); fail("A conflicting registry must not be merged or discarded"); @@ -51,31 +51,31 @@ public class FlattenedBiomeSnapshotTest { } @Test public void coalescesOnlyAnIdenticalDuplicate() { - CompoundNBT root = root(2230); + CompoundTag root = root(2230); registries(root).put("minecraft:worldgen/biome", registries(root).getCompound("minecraft:biomes").copy()); assertTrue(LegacyWorldDataHook.renameFlattenedBiomeRegistry(root)); assertFalse(registries(root).contains("minecraft:biomes")); } - private static CompoundNBT root(int version) { - CompoundNBT root = new CompoundNBT(), data = new CompoundNBT(), fml = new CompoundNBT(); - CompoundNBT registries = new CompoundNBT(), snapshot = new CompoundNBT(); + private static CompoundTag root(int version) { + CompoundTag root = new CompoundTag(), data = new CompoundTag(), fml = new CompoundTag(); + CompoundTag registries = new CompoundTag(), snapshot = new CompoundTag(); data.putInt("DataVersion", version); root.put("Data", data); root.put("fml", fml); fml.put("Registries", registries); registries.put("minecraft:biomes", snapshot); - ListNBT ids = new ListNBT(); + ListTag ids = new ListTag(); for (String name : new String[] {"minecraft:nether", "othermod:marsh"}) { - CompoundNBT entry = new CompoundNBT(); + CompoundTag entry = new CompoundTag(); entry.putString("K", name); entry.putInt("V", name.startsWith("minecraft:") ? 8 : 200); ids.add(entry); } snapshot.put("ids", ids); snapshot.putIntArray("blocked", new int[] {77}); - ListNBT aliases = new ListNBT(); - CompoundNBT alias = new CompoundNBT(); + ListTag aliases = new ListTag(); + CompoundTag alias = new CompoundTag(); alias.putString("K", "othermod:old_marsh"); alias.putString("V", "othermod:marsh"); aliases.add(alias); @@ -83,7 +83,7 @@ private static CompoundNBT root(int version) { return root; } - private static CompoundNBT registries(CompoundNBT root) { + private static CompoundTag registries(CompoundTag root) { return root.getCompound("fml").getCompound("Registries"); } } diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEnchantmentsTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyEnchantmentsTest.java similarity index 81% rename from src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEnchantmentsTest.java rename to src/test/java/zone/moddev/mc/basepower/migration/LegacyEnchantmentsTest.java index 52acfef7..134e19ad 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEnchantmentsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyEnchantmentsTest.java @@ -1,9 +1,9 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; import java.util.LinkedHashMap; import java.util.Map; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ListNBT; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; import org.junit.Test; import static org.junit.Assert.*; @@ -14,10 +14,10 @@ public class LegacyEnchantmentsTest { saved.put(11, "steamadvantage:powderless"); saved.put(12, "steamadvantage:rapid_reload"); saved.put(13, "steamadvantage:recoil"); - CompoundNBT tag = new CompoundNBT(); - ListNBT list = new ListNBT(); + CompoundTag tag = new CompoundTag(); + ListTag list = new ListTag(); for (Integer id : saved.keySet()) { - CompoundNBT entry = new CompoundNBT(); + CompoundTag entry = new CompoundTag(); entry.putShort("id", id.shortValue()); entry.putShort("lvl", (short) 3); list.add(entry); @@ -35,9 +35,9 @@ public class LegacyEnchantmentsTest { @Test public void honorsWorldSpecificIdsAndPreservesStoredEnchantments() { Map saved = new LinkedHashMap<>(); saved.put(173, "steamadvantage:rapid_reload"); - CompoundNBT book = new CompoundNBT(); - ListNBT list = new ListNBT(); - CompoundNBT entry = new CompoundNBT(); + CompoundTag book = new CompoundTag(); + ListTag list = new ListTag(); + CompoundTag entry = new CompoundTag(); entry.putShort("id", (short) 173); entry.putShort("lvl", (short) 2); list.add(entry); @@ -51,9 +51,9 @@ public class LegacyEnchantmentsTest { public void refusesIncompleteOldListInsteadOfSilentlyDroppingAnEnchantment() { Map saved = new LinkedHashMap<>(); saved.put(10, "steamadvantage:high_explosive"); - CompoundNBT tag = new CompoundNBT(); - ListNBT list = new ListNBT(); - CompoundNBT entry = new CompoundNBT(); + CompoundTag tag = new CompoundTag(); + ListTag list = new ListTag(); + CompoundTag entry = new CompoundTag(); entry.putShort("id", (short) 99); list.add(entry); tag.put("ench", list); diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEntitiesTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntitiesTest.java similarity index 72% rename from src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEntitiesTest.java rename to src/test/java/zone/moddev/mc/basepower/migration/LegacyEntitiesTest.java index a382f540..d7344fc5 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEntitiesTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntitiesTest.java @@ -1,12 +1,12 @@ -package zone.moddev.mc.poweradvantage.migration; - -import net.minecraft.util.datafix.DefaultTypeReferences; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.NBTUtil; -import net.minecraft.nbt.StringNBT; -import net.minecraft.util.datafix.DataFixesManager; -import net.minecraft.util.SharedConstants; +package zone.moddev.mc.basepower.migration; + +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.StringTag; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.SharedConstants; import java.util.UUID; import org.junit.Test; @@ -22,24 +22,24 @@ public class LegacyEntitiesTest { } @Test public void fixesVanillaPassengersInsideUnknownModEntities() { - CompoundNBT vehicle = entity("example:vehicle"); + CompoundTag vehicle = entity("example:vehicle"); vehicle.putString("CustomState", "keep-verbatim"); - CompoundNBT wolf = entity("Wolf"); + CompoundTag wolf = entity("Wolf"); wolf.putString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f"); - CompoundNBT nested = entity("example:seat"); - ListNBT nestedPassengers = new ListNBT(); + CompoundTag nested = entity("example:seat"); + ListTag nestedPassengers = new ListTag(); nestedPassengers.add(entity("Cow")); nested.put("Passengers", nestedPassengers); - ListNBT passengers = new ListNBT(); + ListTag passengers = new ListTag(); passengers.add(wolf); passengers.add(nested); vehicle.put("Passengers", passengers); - CompoundNBT root = chunk(vehicle, 512); + CompoundTag root = chunk(vehicle, 512); root = convert(root, 512); - CompoundNBT actual = root.getCompound("Level").getList("Entities", 10).getCompound(0); + CompoundTag actual = root.getCompound("Level").getList("Entities", 10).getCompound(0); assertEquals("example:vehicle", actual.getString("id")); assertEquals("keep-verbatim", actual.getString("CustomState")); - ListNBT fixed = actual.getList("Passengers", 10); + ListTag fixed = actual.getList("Passengers", 10); assertEquals(2, fixed.size()); assertEquals("minecraft:wolf", fixed.getCompound(0).getString("id")); assertEquals(UUID.fromString(wolf.getString("OwnerUUID")), fixed.getCompound(0).getUUID("Owner")); @@ -48,44 +48,44 @@ public class LegacyEntitiesTest { } @Test public void usesVanillaSubtypeFixersRatherThanLowercasingOldIds() { - CompoundNBT skeleton = entity("Skeleton"); + CompoundTag skeleton = entity("Skeleton"); skeleton.putByte("SkeletonType", (byte) 1); - CompoundNBT root = chunk(skeleton, 512); + CompoundTag root = chunk(skeleton, 512); root = convert(root, 512); assertEquals("minecraft:wither_skeleton", root.getCompound("Level").getList("Entities", 10).getCompound(0).getString("id")); } @Test public void leavesModernEntityAndUnrelatedCustomNbtUnchangedOnRepeatedPreparation() { - CompoundNBT wolf = entity("minecraft:wolf"); + CompoundTag wolf = entity("minecraft:wolf"); wolf.putString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f"); wolf.put("CustomData", entity("Wolf")); - CompoundNBT root = chunk(wolf, SharedConstants.getCurrentVersion().getWorldVersion()); + CompoundTag root = chunk(wolf, SharedConstants.getCurrentVersion().getWorldVersion()); LegacyWorldDataHook.prepareLegacyChunk(root); LegacyWorldDataHook.prepareLegacyChunk(root); assertEquals(wolf, root.getCompound("Level").getList("Entities", 10).getCompound(0)); } @Test public void repeatPreparationDoesNotLoseConvertedVanillaState() { - CompoundNBT wolf = entity("Wolf"); + CompoundTag wolf = entity("Wolf"); wolf.putString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f"); - CompoundNBT root = chunk(wolf, 512); + CompoundTag root = chunk(wolf, 512); LegacyWorldDataHook.prepareLegacyChunk(root); - CompoundNBT once = root.copy(); + CompoundTag once = root.copy(); LegacyWorldDataHook.prepareLegacyChunk(root); assertEquals(once, root); } - private static CompoundNBT entity(String id) { - CompoundNBT entity = new CompoundNBT(); + private static CompoundTag entity(String id) { + CompoundTag entity = new CompoundTag(); entity.putString("id", id); return entity; } - private static CompoundNBT chunk(CompoundNBT entity, int version) { - CompoundNBT root = new CompoundNBT(); + private static CompoundTag chunk(CompoundTag entity, int version) { + CompoundTag root = new CompoundTag(); root.putInt("DataVersion", version); - CompoundNBT level = new CompoundNBT(); - ListNBT entities = new ListNBT(); + CompoundTag level = new CompoundTag(); + ListTag entities = new ListTag(); entities.add(entity); level.put("Entities", entities); root.put("Level", level); @@ -93,11 +93,11 @@ private static CompoundNBT chunk(CompoundNBT entity, int version) { } private static void assertWolf(boolean customEntity) { - CompoundNBT root = new CompoundNBT(); + CompoundTag root = new CompoundTag(); root.putInt("DataVersion", 512); - CompoundNBT level = new CompoundNBT(); - ListNBT entities = new ListNBT(); - CompoundNBT wolf = new CompoundNBT(); + CompoundTag level = new CompoundTag(); + ListTag entities = new ListTag(); + CompoundTag wolf = new CompoundTag(); wolf.putString("id", "Wolf"); wolf.putString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f"); wolf.putBoolean("Sitting", true); @@ -105,15 +105,15 @@ private static void assertWolf(boolean customEntity) { wolf.putLong("UUIDLeast", 456L); entities.add(wolf); if (customEntity) { - CompoundNBT turbine = new CompoundNBT(); + CompoundTag turbine = new CompoundTag(); turbine.putString("id", "electricadvantage.electricadvantage.Hydroturbine"); turbine.putIntArray("parentPos", new int[] {164, 81, 8}); entities.add(turbine); } level.put("Entities", entities); root.put("Level", level); - CompoundNBT fixed = convert(root, 512); - CompoundNBT actual = fixed.getCompound("Level").getList("Entities", 10).getCompound(0); + CompoundTag fixed = convert(root, 512); + CompoundTag actual = fixed.getCompound("Level").getList("Entities", 10).getCompound(0); assertEquals("minecraft:wolf", actual.getString("id")); assertEquals(UUID.fromString(wolf.getString("OwnerUUID")), actual.getUUID("Owner")); assertTrue(actual.getBoolean("Sitting")); @@ -122,16 +122,16 @@ private static void assertWolf(boolean customEntity) { } @Test public void preservesCustomEntityUuidAndPrivateNbtThroughWholeChunkFix() { - CompoundNBT custom = entity("example:turbine"); + CompoundTag custom = entity("example:turbine"); custom.putLong("UUIDMost", -9014291490481488382L); custom.putLong("UUIDLeast", -5032249493117728499L); - CompoundNBT privateData = entity("Wolf"); + CompoundTag privateData = entity("Wolf"); privateData.putLong("UUIDMost", 88L); privateData.putLong("UUIDLeast", 99L); custom.put("PrivateData", privateData); for (int version : new int[] {512, 1343, 1631, 1976, 2230}) { - CompoundNBT converted = convert(chunk(custom.copy(), version), version); - CompoundNBT actual = converted.getCompound("Level").getList("Entities", 10).getCompound(0); + CompoundTag converted = convert(chunk(custom.copy(), version), version); + CompoundTag actual = converted.getCompound("Level").getList("Entities", 10).getCompound(0); assertEquals(new UUID(-9014291490481488382L, -5032249493117728499L), actual.getUUID("UUID")); assertEquals(privateData, actual.getCompound("PrivateData")); assertFalse(converted.contains("AdvantageLegacyEntityDataFix")); @@ -140,7 +140,7 @@ private static void assertWolf(boolean customEntity) { @Test public void preservesOwnerHealthAndUuidInPostFlatteningMixedChunks() { for (int version : new int[] {1343, 1631, 1976, 2230}) { - CompoundNBT wolf = entity("minecraft:wolf"); + CompoundTag wolf = entity("minecraft:wolf"); UUID uuid = new UUID(-1767247258114444277L, -6740529603542625373L); UUID owner = UUID.fromString("a614e658-6270-33c3-9297-7ce80908964f"); wolf.putLong("UUIDMost", uuid.getMostSignificantBits()); @@ -148,10 +148,10 @@ private static void assertWolf(boolean customEntity) { wolf.putString("OwnerUUID", owner.toString()); wolf.putFloat("Health", 8F); wolf.putBoolean("Sitting", true); - CompoundNBT root = chunk(wolf, version); + CompoundTag root = chunk(wolf, version); root.getCompound("Level").getList("Entities", 10).add(entity("example:turbine")); - CompoundNBT converted = convert(root, version); - CompoundNBT actual = converted.getCompound("Level").getList("Entities", 10).getCompound(0); + CompoundTag converted = convert(root, version); + CompoundTag actual = converted.getCompound("Level").getList("Entities", 10).getCompound(0); assertEquals(uuid, actual.getUUID("UUID")); assertEquals(owner, actual.getUUID("Owner")); assertEquals(8F, actual.getFloat("Health"), 0F); @@ -161,23 +161,23 @@ private static void assertWolf(boolean customEntity) { } } - private static CompoundNBT convert(CompoundNBT root, int version) { + private static CompoundTag convert(CompoundTag root, int version) { LegacyWorldDataHook.prepareLegacyChunk(root); LegacyWorldDataHook.prepareLegacyEntities(root); - CompoundNBT fixed = NBTUtil.update(DataFixesManager.getDataFixer(), DefaultTypeReferences.CHUNK, + CompoundTag fixed = NbtUtils.update(DataFixers.getDataFixer(), DataFixTypes.CHUNK, root, version, SharedConstants.getCurrentVersion().getWorldVersion()); return LegacyWorldDataHook.finalizeLegacyChunk(fixed); } @Test public void retainsZeroHalfAndExistingNativeUuids() { for (UUID uuid : new UUID[] {new UUID(0L, 1L), new UUID(1L, 0L), new UUID(0L, 0L)}) { - CompoundNBT custom = entity("example:vehicle"); + 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")); } - CompoundNBT custom = entity("example:vehicle"); + CompoundTag custom = entity("example:vehicle"); custom.putUUID("UUID", new UUID(17L, 19L)); custom.putLong("UUIDMost", 88L); custom.putLong("UUIDLeast", 99L); @@ -187,25 +187,25 @@ private static CompoundNBT convert(CompoundNBT root, int version) { @Test public void baseMetalsDurabilitySurvivesBothEntryHookOrders() throws Exception { java.lang.reflect.Method basePrepare = Class.forName("zone.moddev.mc.basemetals.migration.LegacyWorldDataHook") - .getMethod("prepareLegacyChunk", CompoundNBT.class); + .getMethod("prepareLegacyChunk", CompoundTag.class); for (boolean baseFirst : new boolean[] {false, true}) { - CompoundNBT cart = entity("minecraft:chest_minecart"); - CompoundNBT pick = entity("basemetals:steel_pickaxe"); + CompoundTag cart = entity("minecraft:chest_minecart"); + CompoundTag pick = entity("basemetals:steel_pickaxe"); pick.putByte("Count", (byte) 1); pick.putShort("Damage", (short) 17); pick.putByte("Slot", (byte) 0); - ListNBT items = new ListNBT(); + ListTag items = new ListTag(); items.add(pick); cart.put("Items", items); - CompoundNBT root = chunk(cart, 1343); + CompoundTag root = chunk(cart, 1343); if (baseFirst) basePrepare.invoke(null, root); LegacyWorldDataHook.prepareLegacyChunk(root); if (!baseFirst) basePrepare.invoke(null, root); LegacyWorldDataHook.prepareLegacyEntities(root); - root = NBTUtil.update(DataFixesManager.getDataFixer(), DefaultTypeReferences.CHUNK, + root = NbtUtils.update(DataFixers.getDataFixer(), DataFixTypes.CHUNK, root, 1343, SharedConstants.getCurrentVersion().getWorldVersion()); root = LegacyWorldDataHook.finalizeLegacyChunk(root); - CompoundNBT actual = root.getCompound("Level").getList("Entities", 10).getCompound(0) + CompoundTag actual = root.getCompound("Level").getList("Entities", 10).getCompound(0) .getList("Items", 10).getCompound(0); assertEquals("basemetals:steel_pickaxe", actual.getString("id")); assertEquals(1, actual.getInt("Count")); @@ -214,34 +214,34 @@ private static CompoundNBT convert(CompoundNBT root, int version) { } @Test(expected = IllegalStateException.class) public void rejectsMalformedEntityListWithoutErasingIt() { - CompoundNBT root = chunk(entity("example:vehicle"), 512); - ListNBT wrong = new ListNBT(); - wrong.add(StringNBT.valueOf("keep")); + CompoundTag root = chunk(entity("example:vehicle"), 512); + ListTag wrong = new ListTag(); + wrong.add(StringTag.valueOf("keep")); root.getCompound("Level").put("Entities", wrong); LegacyWorldDataHook.prepareLegacyEntities(root); } @Test(expected = IllegalStateException.class) public void rejectsMalformedPassengerList() { - CompoundNBT custom = entity("example:vehicle"); - ListNBT wrong = new ListNBT(); - wrong.add(StringNBT.valueOf("keep")); + CompoundTag custom = entity("example:vehicle"); + ListTag wrong = new ListTag(); + wrong.add(StringTag.valueOf("keep")); custom.put("Passengers", wrong); LegacyWorldDataHook.prepareLegacyEntities(chunk(custom, 512)); } @Test(expected = IllegalStateException.class) public void rejectsMalformedStagedList() { - CompoundNBT root = chunk(entity("example:vehicle"), 512); - ListNBT wrong = new ListNBT(); - wrong.add(StringNBT.valueOf("keep")); + CompoundTag root = chunk(entity("example:vehicle"), 512); + ListTag wrong = new ListTag(); + wrong.add(StringTag.valueOf("keep")); root.put("AdvantageLegacyEntityDataFix", wrong); LegacyWorldDataHook.finalizeLegacyChunk(root); } @Test public void detachesPendingChunkNbtIncludingNestedMapsAndArrays() { - CompoundNBT pending = chunk(entity("example:vehicle"), SharedConstants.getCurrentVersion().getWorldVersion()); + CompoundTag pending = chunk(entity("example:vehicle"), SharedConstants.getCurrentVersion().getWorldVersion()); pending.getCompound("Level").putIntArray("Biomes", new int[] {1, 2, 3}); - CompoundNBT before = pending.copy(); - CompoundNBT read = LegacyWorldDataHook.copyChunkForMigration(pending); + CompoundTag before = pending.copy(); + CompoundTag read = LegacyWorldDataHook.copyChunkForMigration(pending); assertNotSame(pending, read); assertNotSame(pending.getCompound("Level"), read.getCompound("Level")); read.getCompound("Level").putBoolean("TerrainPopulated", true); diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEntityAttributeAliasesTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntityAttributeAliasesTest.java similarity index 62% rename from src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEntityAttributeAliasesTest.java rename to src/test/java/zone/moddev/mc/basepower/migration/LegacyEntityAttributeAliasesTest.java index 58d35ad4..8fb6966e 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEntityAttributeAliasesTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntityAttributeAliasesTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; import static org.junit.Assert.*; @@ -11,22 +11,22 @@ import java.util.UUID; import java.util.function.Supplier; import java.util.function.UnaryOperator; -import net.minecraft.entity.EntityType; -import net.minecraft.entity.passive.WolfEntity; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.NBTUtil; -import net.minecraft.nbt.NBTDynamicOps; -import net.minecraft.nbt.StringNBT; -import net.minecraft.profiler.EmptyProfiler; -import net.minecraft.profiler.IProfiler; -import net.minecraft.util.SharedConstants; -import net.minecraft.util.datafix.DataFixesManager; -import net.minecraft.util.datafix.DefaultTypeReferences; -import net.minecraft.util.datafix.TypeReferences; -import net.minecraft.util.registry.Bootstrap; -import net.minecraft.world.World; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.animal.Wolf; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.StringTag; +import net.minecraft.util.profiling.InactiveProfiler; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.SharedConstants; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.util.datafix.fixes.References; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.Level; +import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.ForgeMod; import net.minecraftforge.event.RegistryEvent; import net.minecraftforge.registries.DeferredRegister; @@ -40,44 +40,44 @@ public class LegacyEntityAttributeAliasesTest { @BeforeClass public static void bootstrap() { Bootstrap.bootStrap(); } @Test public void changesOnlyExactNameAndPreservesUnknownFieldsAndUuidModifiers() { - CompoundNBT entity = entity("example:vehicle", true); - CompoundNBT before = entity.copy(); - CompoundNBT root = chunk(entity); + CompoundTag entity = entity("example:vehicle", true); + CompoundTag before = entity.copy(); + CompoundTag root = chunk(entity); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); - CompoundNBT actual = root.getCompound("Level").getList("Entities", 10).getCompound(0); - CompoundNBT expected = before.copy(); + CompoundTag actual = root.getCompound("Level").getList("Entities", 10).getCompound(0); + CompoundTag expected = before.copy(); expected.getList("Attributes", 10).getCompound(0).putString("Name", "forge:swim_speed"); assertEquals(expected, actual); assertEquals(before, entity); } @Test public void aliasesVanillaEntityBeforeNativeDfuAndPreservesModifierIdentity() { - CompoundNBT root = chunk(entity("minecraft:wolf", true)); + CompoundTag root = chunk(entity("minecraft:wolf", true)); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); - CompoundNBT attribute = root.getCompound("Level").getList("Entities", 10).getCompound(0) + CompoundTag attribute = root.getCompound("Level").getList("Entities", 10).getCompound(0) .getList("Attributes", 10).getCompound(0); assertEquals("forge:swim_speed", attribute.getString("Name")); assertEquals(2D, attribute.getDouble("Base"), 0D); assertEquals("keep-attribute-field", attribute.getString("PrivateNote")); - CompoundNBT modifier = attribute.getList("Modifiers", 10).getCompound(0); + CompoundTag modifier = attribute.getList("Modifiers", 10).getCompound(0); assertEquals(MODIFIER, modifier.getUUID("UUID")); assertEquals("keep-modifier-field", modifier.getString("PrivateNote")); } @Test public void aliasesOnlyEntityPassengersNotInventoryOrPrivateCompounds() { - CompoundNBT parent = entity("example:vehicle", false); - CompoundNBT child = entity("example:seat", true); - ListNBT passengers = new ListNBT(); passengers.add(child); + CompoundTag parent = entity("example:vehicle", false); + CompoundTag child = entity("example:seat", true); + ListTag passengers = new ListTag(); passengers.add(child); parent.put("Passengers", passengers); - CompoundNBT foreign = entity("example:private", true); + CompoundTag foreign = entity("example:private", true); parent.put("PrivateData", foreign.copy()); parent.put("ForgeData", foreign.copy()); - ListNBT items = new ListNBT(); items.add(foreign.copy()); parent.put("Items", items); - CompoundNBT root = chunk(parent); + ListTag items = new ListTag(); items.add(foreign.copy()); parent.put("Items", items); + CompoundTag root = chunk(parent); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); - CompoundNBT actual = root.getCompound("Level").getList("Entities", 10).getCompound(0); + CompoundTag actual = root.getCompound("Level").getList("Entities", 10).getCompound(0); assertEquals("forge:swim_speed", actual.getList("Passengers", 10).getCompound(0) .getList("Attributes", 10).getCompound(0).getString("Name")); assertEquals(foreign, actual.getCompound("PrivateData")); @@ -95,50 +95,50 @@ public class LegacyEntityAttributeAliasesTest { } @Test public void exactAliasDoesNotRenameOtherForgeOrUnknownAttributes() { - CompoundNBT entity = entity("example:vehicle", false); - ListNBT list = new ListNBT(); + CompoundTag entity = entity("example:vehicle", false); + ListTag list = new ListTag(); for (String name : new String[] {"forge.SwimSpeed", "example:swimSpeed", "forge:other", "forge.swimSpeed.extra"}) { - CompoundNBT attribute = attribute(false); attribute.putString("Name", name); list.add(attribute); + CompoundTag attribute = attribute(false); attribute.putString("Name", name); list.add(attribute); } entity.put("Attributes", list); - CompoundNBT root = chunk(entity), before = root.copy(); + CompoundTag root = chunk(entity), before = root.copy(); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); assertEquals(before, root); } @Test public void repeatedAliasPreparationIsIdempotent() { - CompoundNBT root = chunk(entity("example:vehicle", true)); + CompoundTag root = chunk(entity("example:vehicle", true)); LegacyEntityDataFixes.prepare(root); - CompoundNBT staged = root.copy(); + CompoundTag staged = root.copy(); LegacyEntityDataFixes.prepare(root); assertEquals(staged, root); LegacyEntityDataFixes.finish(root); - CompoundNBT once = root.copy(); + CompoundTag once = root.copy(); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); assertEquals(once, root); } @Test public void modernChunksAndPlayersRemainUnchanged() { - CompoundNBT root = chunk(entity("minecraft:wolf", true)); + CompoundTag root = chunk(entity("minecraft:wolf", true)); root.putInt("DataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); - CompoundNBT before = root.copy(); + CompoundTag before = root.copy(); LegacyEntityDataFixes.prepare(root); assertEquals(before, root); - CompoundNBT player = player(entity("minecraft:wolf", true)); - CompoundNBT result = LegacyPlayerDataFixes.update(DataFixesManager.getDataFixer(), DefaultTypeReferences.PLAYER, + CompoundTag player = player(entity("minecraft:wolf", true)); + CompoundTag result = LegacyPlayerDataFixes.update(DataFixers.getDataFixer(), DataFixTypes.PLAYER, player, SharedConstants.getCurrentVersion().getWorldVersion()); assertEquals(player, result); } @Test public void playerRootVehicleAndPassengersArePreparedBeforeNativePlayerDfu() { - CompoundNBT vehicle = entity("example:vehicle", true); - ListNBT passengers = new ListNBT(); passengers.add(entity("example:seat", true)); vehicle.put("Passengers", passengers); - CompoundNBT source = player(vehicle), expected = source.copy(); - CompoundNBT result = LegacyPlayerDataFixes.update(new RecordingFixer(value -> { - CompoundNBT rootVehicle = value.getCompound("RootVehicle").getCompound("Entity"); + CompoundTag vehicle = entity("example:vehicle", true); + ListTag passengers = new ListTag(); passengers.add(entity("example:seat", true)); vehicle.put("Passengers", passengers); + CompoundTag source = player(vehicle), expected = source.copy(); + CompoundTag result = LegacyPlayerDataFixes.update(new RecordingFixer(value -> { + CompoundTag rootVehicle = value.getCompound("RootVehicle").getCompound("Entity"); assertEquals("forge:swim_speed", rootVehicle.getList("Attributes", 10).getCompound(0).getString("Name")); assertEquals("forge:swim_speed", rootVehicle.getList("Passengers", 10).getCompound(0) .getList("Attributes", 10).getCompound(0).getString("Name")); return value; - }), DefaultTypeReferences.PLAYER, source, 512); + }), DataFixTypes.PLAYER, source, 512); assertEquals(expected, source); expected.getCompound("RootVehicle").getCompound("Entity").getList("Attributes", 10) .getCompound(0).putString("Name", "forge:swim_speed"); @@ -148,40 +148,40 @@ public class LegacyEntityAttributeAliasesTest { } @Test public void playerInventoryAndForeignRootVehicleLookalikesAreNotWalked() { - CompoundNBT player = player(entity("example:vehicle", true)); - CompoundNBT foreign = entity("example:private", true); + CompoundTag player = player(entity("example:vehicle", true)); + CompoundTag foreign = entity("example:private", true); foreign.putByte("Count", (byte) 3); - ListNBT inventory = new ListNBT(); inventory.add(foreign.copy()); player.put("Inventory", inventory); + ListTag inventory = new ListTag(); inventory.add(foreign.copy()); player.put("Inventory", inventory); player.put("PrivateData", player(entity("example:foreign", true))); - CompoundNBT before = player.copy(); - CompoundNBT result = LegacyPlayerDataFixes.update(new RecordingFixer(value -> value), - DefaultTypeReferences.PLAYER, player, 1343); + CompoundTag before = player.copy(); + CompoundTag result = LegacyPlayerDataFixes.update(new RecordingFixer(value -> value), + DataFixTypes.PLAYER, player, 1343); assertEquals(before, player); assertEquals(inventory, result.getList("Inventory", 10)); assertEquals(before.getCompound("PrivateData"), result.getCompound("PrivateData")); } @Test public void rootVehiclePassengerCollisionFailsWithoutChangingPlayer() { - CompoundNBT vehicle = entity("example:vehicle", true); - CompoundNBT child = entity("example:seat", true); - CompoundNBT duplicate = attribute(false); duplicate.putString("Name", "forge:swim_speed"); + CompoundTag vehicle = entity("example:vehicle", true); + CompoundTag child = entity("example:seat", true); + CompoundTag duplicate = attribute(false); duplicate.putString("Name", "forge:swim_speed"); child.getList("Attributes", 10).add(duplicate); - ListNBT passengers = new ListNBT(); passengers.add(child); vehicle.put("Passengers", passengers); - CompoundNBT player = player(vehicle), before = player.copy(); + ListTag passengers = new ListTag(); passengers.add(child); vehicle.put("Passengers", passengers); + CompoundTag player = player(vehicle), before = player.copy(); try { - LegacyPlayerDataFixes.update(DataFixesManager.getDataFixer(), DefaultTypeReferences.PLAYER, player, 1343); + LegacyPlayerDataFixes.update(DataFixers.getDataFixer(), DataFixTypes.PLAYER, player, 1343); fail("Alias collision accepted"); } catch (IllegalStateException expected) { assertEquals(before, player); } } @Test public void nativePlayerDfuRetainsVehicleAndPassengerCustomizedAttributes() { - CompoundNBT vehicle = entity("minecraft:wolf", true); - ListNBT passengers = new ListNBT(); passengers.add(entity("minecraft:wolf", true)); vehicle.put("Passengers", passengers); - CompoundNBT player = player(vehicle), before = player.copy(); - CompoundNBT result = LegacyPlayerDataFixes.update(DataFixesManager.getDataFixer(), DefaultTypeReferences.PLAYER, player, 1343); - CompoundNBT fixed = result.getCompound("RootVehicle").getCompound("Entity"); - for (CompoundNBT actual : new CompoundNBT[] {fixed, fixed.getList("Passengers", 10).getCompound(0)}) { - CompoundNBT attribute = actual.getList("Attributes", 10).getCompound(0); + CompoundTag vehicle = entity("minecraft:wolf", true); + ListTag passengers = new ListTag(); passengers.add(entity("minecraft:wolf", true)); vehicle.put("Passengers", passengers); + CompoundTag player = player(vehicle), before = player.copy(); + CompoundTag result = LegacyPlayerDataFixes.update(DataFixers.getDataFixer(), DataFixTypes.PLAYER, player, 1343); + CompoundTag fixed = result.getCompound("RootVehicle").getCompound("Entity"); + for (CompoundTag actual : new CompoundTag[] {fixed, fixed.getList("Passengers", 10).getCompound(0)}) { + CompoundTag attribute = actual.getList("Attributes", 10).getCompound(0); assertEquals("forge:swim_speed", attribute.getString("Name")); assertEquals(2D, attribute.getDouble("Base"), 0D); assertEquals(MODIFIER, attribute.getList("Modifiers", 10).getCompound(0).getUUID("UUID")); @@ -190,13 +190,13 @@ public class LegacyEntityAttributeAliasesTest { } @Test public void ownPlayerAttributeBaseTwoAndModifierSurviveNativePlayerDfu() { - CompoundNBT player = player(entity("minecraft:wolf", true)); + CompoundTag player = player(entity("minecraft:wolf", true)); player.put("Attributes", entity("minecraft:wolf", true).getList("Attributes", 10)); - CompoundNBT foreign = entity("example:private", true); + CompoundTag foreign = entity("example:private", true); player.put("PrivateData", foreign.copy()); - CompoundNBT before = player.copy(); - CompoundNBT result = LegacyPlayerDataFixes.update(DataFixesManager.getDataFixer(), DefaultTypeReferences.PLAYER, player, 1343); - CompoundNBT attribute = result.getList("Attributes", 10).getCompound(0); + CompoundTag before = player.copy(); + CompoundTag result = LegacyPlayerDataFixes.update(DataFixers.getDataFixer(), DataFixTypes.PLAYER, player, 1343); + CompoundTag attribute = result.getList("Attributes", 10).getCompound(0); assertEquals("forge:swim_speed", attribute.getString("Name")); assertEquals(2D, attribute.getDouble("Base"), 0D); assertEquals(MODIFIER, attribute.getList("Modifiers", 10).getCompound(0).getUUID("UUID")); @@ -207,21 +207,21 @@ public class LegacyEntityAttributeAliasesTest { } @Test public void ownPlayerAttributeCollisionFailsWithoutMutatingCaller() { - CompoundNBT player = player(entity("example:vehicle", true)); - ListNBT attributes = entity("example:attributes", true).getList("Attributes", 10); - CompoundNBT duplicate = attribute(false); duplicate.putString("Name", "forge:swim_speed"); attributes.add(duplicate); + CompoundTag player = player(entity("example:vehicle", true)); + ListTag attributes = entity("example:attributes", true).getList("Attributes", 10); + CompoundTag duplicate = attribute(false); duplicate.putString("Name", "forge:swim_speed"); attributes.add(duplicate); player.put("Attributes", attributes); - CompoundNBT before = player.copy(); + CompoundTag before = player.copy(); try { - LegacyPlayerDataFixes.update(DataFixesManager.getDataFixer(), DefaultTypeReferences.PLAYER, player, 1343); + LegacyPlayerDataFixes.update(DataFixers.getDataFixer(), DataFixTypes.PLAYER, player, 1343); fail("Own player attribute collision accepted"); } catch (IllegalStateException expected) { assertEquals(before, player); } } @Test public void malformedAttributesFailClosedWithoutChangingSource() { - CompoundNBT entity = entity("example:vehicle", false); - ListNBT malformed = new ListNBT(); malformed.add(StringNBT.valueOf("keep")); entity.put("Attributes", malformed); - CompoundNBT root = chunk(entity), before = root.copy(); + CompoundTag entity = entity("example:vehicle", false); + ListTag malformed = new ListTag(); malformed.add(StringTag.valueOf("keep")); entity.put("Attributes", malformed); + CompoundTag root = chunk(entity), before = root.copy(); try { LegacyEntityDataFixes.prepare(root); fail("Malformed attributes accepted"); } catch (IllegalStateException expected) { assertEquals(before, root); } } @@ -231,20 +231,20 @@ public class LegacyEntityAttributeAliasesTest { // Keep constructor-free test setup without linking restricted JDK types. Class allocator = Class.forName("sun.misc.Unsafe"); Field unsafeField = allocator.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); - ServerWorld world = (ServerWorld) allocator.getMethod("allocateInstance", Class.class) - .invoke(unsafeField.get(null), ServerWorld.class); - set(World.class, world, "dimension", World.NETHER); - set(World.class, world, "profiler", (Supplier) () -> EmptyProfiler.INSTANCE); - CompoundNBT source = entity("minecraft:wolf", true); + ServerLevel world = (ServerLevel) allocator.getMethod("allocateInstance", Class.class) + .invoke(unsafeField.get(null), ServerLevel.class); + set(Level.class, world, "dimension", Level.NETHER); + set(Level.class, world, "profiler", (Supplier) () -> InactiveProfiler.INSTANCE); + CompoundTag source = entity("minecraft:wolf", true); source.putFloat("Health", 8F); - CompoundNBT red = (CompoundNBT) DataFixesManager.getDataFixer().update(TypeReferences.ENTITY, - new Dynamic<>(NBTDynamicOps.INSTANCE, source.copy()), 1343, + CompoundTag red = (CompoundTag) DataFixers.getDataFixer().update(References.ENTITY, + new Dynamic<>(NbtOps.INSTANCE, source.copy()), 1343, SharedConstants.getCurrentVersion().getWorldVersion()).getValue(); - WolfEntity before = new WolfEntity(EntityType.WOLF, world); before.load(red); + Wolf before = new Wolf(EntityType.WOLF, world); before.load(red); assertEquals(1D, before.getAttribute(ForgeMod.SWIM_SPEED.get()).getBaseValue(), 0D); - CompoundNBT root = chunk(source); + CompoundTag root = chunk(source); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); - WolfEntity after = new WolfEntity(EntityType.WOLF, world); + Wolf after = new Wolf(EntityType.WOLF, world); after.load(root.getCompound("Level").getList("Entities", 10).getCompound(0)); assertEquals(2D, after.getAttribute(ForgeMod.SWIM_SPEED.get()).getBaseValue(), 0D); assertNotNull(after.getAttribute(ForgeMod.SWIM_SPEED.get()).getModifier(MODIFIER)); @@ -254,41 +254,41 @@ public class LegacyEntityAttributeAliasesTest { } private static void assertCollision(String first, String second) { - CompoundNBT entity = entity("example:vehicle", true); + CompoundTag entity = entity("example:vehicle", true); entity.getList("Attributes", 10).getCompound(0).putString("Name", first); - CompoundNBT duplicate = attribute(false); duplicate.putString("Name", second); + CompoundTag duplicate = attribute(false); duplicate.putString("Name", second); entity.getList("Attributes", 10).add(duplicate); - CompoundNBT root = chunk(entity), before = root.copy(); + CompoundTag root = chunk(entity), before = root.copy(); try { LegacyEntityDataFixes.prepare(root); fail("Alias collision accepted"); } catch (IllegalStateException expected) { assertEquals(before, root); } } - private static CompoundNBT attribute(boolean modifier) { - CompoundNBT attribute = new CompoundNBT(); attribute.putString("Name", "forge.swimSpeed"); + private static CompoundTag attribute(boolean modifier) { + CompoundTag attribute = new CompoundTag(); attribute.putString("Name", "forge.swimSpeed"); attribute.putDouble("Base", 2D); attribute.putString("PrivateNote", "keep-attribute-field"); if (modifier) { - CompoundNBT entry = new CompoundNBT(); entry.putString("Name", "customized-swim"); + CompoundTag entry = new CompoundTag(); entry.putString("Name", "customized-swim"); entry.putLong("UUIDMost", MODIFIER.getMostSignificantBits()); entry.putLong("UUIDLeast", MODIFIER.getLeastSignificantBits()); entry.putDouble("Amount", 0.75D); entry.putInt("Operation", 0); entry.putString("PrivateNote", "keep-modifier-field"); - ListNBT modifiers = new ListNBT(); modifiers.add(entry); attribute.put("Modifiers", modifiers); + ListTag modifiers = new ListTag(); modifiers.add(entry); attribute.put("Modifiers", modifiers); } return attribute; } - private static CompoundNBT entity(String id, boolean attributes) { - CompoundNBT result = new CompoundNBT(); result.putString("id", id); - if (attributes) { ListNBT list = new ListNBT(); list.add(attribute(true)); result.put("Attributes", list); } + private static CompoundTag entity(String id, boolean attributes) { + CompoundTag result = new CompoundTag(); result.putString("id", id); + if (attributes) { ListTag list = new ListTag(); list.add(attribute(true)); result.put("Attributes", list); } return result; } - private static CompoundNBT chunk(CompoundNBT entity) { - CompoundNBT root = new CompoundNBT(); root.putInt("DataVersion", 1343); - CompoundNBT level = new CompoundNBT(); ListNBT entities = new ListNBT(); entities.add(entity); level.put("Entities", entities); + private static CompoundTag chunk(CompoundTag entity) { + CompoundTag root = new CompoundTag(); root.putInt("DataVersion", 1343); + CompoundTag level = new CompoundTag(); ListTag entities = new ListTag(); entities.add(entity); level.put("Entities", entities); root.put("Level", level); return root; } - private static CompoundNBT player(CompoundNBT entity) { - CompoundNBT result = new CompoundNBT(); CompoundNBT vehicle = new CompoundNBT(); + private static CompoundTag player(CompoundTag entity) { + CompoundTag result = new CompoundTag(); CompoundTag vehicle = new CompoundTag(); vehicle.put("Entity", entity); vehicle.putUUID("Attach", new UUID(1L, 2L)); result.put("RootVehicle", vehicle); return result; } @@ -304,10 +304,10 @@ private static void set(Class owner, Object instance, String name, Object val } private static final class RecordingFixer implements DataFixer { - private final UnaryOperator update; - RecordingFixer(UnaryOperator update) { this.update = update; } + private final UnaryOperator update; + RecordingFixer(UnaryOperator update) { this.update = update; } @Override public Dynamic update(DSL.TypeReference type, Dynamic input, int from, int to) { - update.apply((CompoundNBT) input.getValue()); + update.apply((CompoundTag) input.getValue()); return input; } @Override public Schema getSchema(int version) { throw new AssertionError("Not used by the boundary"); } diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyForgeAttributeRegistryTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyForgeAttributeRegistryTest.java similarity index 61% rename from src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyForgeAttributeRegistryTest.java rename to src/test/java/zone/moddev/mc/basepower/migration/LegacyForgeAttributeRegistryTest.java index c4a915c8..140185c6 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyForgeAttributeRegistryTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyForgeAttributeRegistryTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; import static org.junit.Assert.*; @@ -7,21 +7,21 @@ import java.lang.reflect.Method; import java.util.UUID; import java.util.function.Supplier; -import net.minecraft.entity.EntityType; -import net.minecraft.entity.ai.attributes.ModifiableAttributeInstance; -import net.minecraft.entity.passive.WolfEntity; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.NBTDynamicOps; -import net.minecraft.profiler.EmptyProfiler; -import net.minecraft.profiler.IProfiler; -import net.minecraft.util.SharedConstants; -import net.minecraft.util.datafix.DataFixesManager; -import net.minecraft.util.datafix.DefaultTypeReferences; -import net.minecraft.util.datafix.TypeReferences; -import net.minecraft.util.registry.Bootstrap; -import net.minecraft.world.World; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.animal.Wolf; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.util.profiling.InactiveProfiler; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.SharedConstants; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.util.datafix.fixes.References; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.Level; +import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.ForgeMod; import net.minecraftforge.event.RegistryEvent; import net.minecraftforge.registries.DeferredRegister; @@ -48,10 +48,10 @@ public class LegacyForgeAttributeRegistryTest { @Test public void retainsEveryForgeAliasAndForeignPayloadBeforeNativeDfu() { for (String[] alias : ALIASES) { - CompoundNBT source = entity(alias[0], 7.5D), before = source.copy(); - CompoundNBT root = chunk(source); + CompoundTag source = entity(alias[0], 7.5D), before = source.copy(); + CompoundTag root = chunk(source); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); - CompoundNBT expected = before.copy(); + CompoundTag expected = before.copy(); expected.getList("Attributes", 10).getCompound(0).putString("Name", alias[1]); assertEquals(alias[0], expected, fixedEntity(root)); assertEquals(before, source); @@ -62,23 +62,23 @@ public class LegacyForgeAttributeRegistryTest { double[] values = {2D, 12.5D, 0.15D}; Class allocator = Class.forName("sun.misc.Unsafe"); Field unsafe = allocator.getDeclaredField("theUnsafe"); unsafe.setAccessible(true); - ServerWorld world = (ServerWorld) allocator.getMethod("allocateInstance", Class.class) - .invoke(unsafe.get(null), ServerWorld.class); - set(World.class, world, "dimension", World.NETHER); - set(World.class, world, "profiler", (Supplier) () -> EmptyProfiler.INSTANCE); + ServerLevel world = (ServerLevel) allocator.getMethod("allocateInstance", Class.class) + .invoke(unsafe.get(null), ServerLevel.class); + set(Level.class, world, "dimension", Level.NETHER); + set(Level.class, world, "profiler", (Supplier) () -> InactiveProfiler.INSTANCE); for (int index = 0; index < 3; index++) { - CompoundNBT source = entity(ALIASES[index][0], values[index]); source.putString("id", "minecraft:wolf"); - CompoundNBT raw = (CompoundNBT) DataFixesManager.getDataFixer().update(TypeReferences.ENTITY, - new Dynamic<>(NBTDynamicOps.INSTANCE, source.copy()), 1343, + CompoundTag source = entity(ALIASES[index][0], values[index]); source.putString("id", "minecraft:wolf"); + CompoundTag raw = (CompoundTag) DataFixers.getDataFixer().update(References.ENTITY, + new Dynamic<>(NbtOps.INSTANCE, source.copy()), 1343, SharedConstants.getCurrentVersion().getWorldVersion()).getValue(); - WolfEntity negative = new WolfEntity(EntityType.WOLF, world); negative.load(raw); - ModifiableAttributeInstance old = negative.getAttribute(ForgeRegistries.ATTRIBUTES.getValue( - new net.minecraft.util.ResourceLocation(ALIASES[index][1]))); + Wolf negative = new Wolf(EntityType.WOLF, world); negative.load(raw); + AttributeInstance old = negative.getAttribute(ForgeRegistries.ATTRIBUTES.getValue( + new net.minecraft.resources.ResourceLocation(ALIASES[index][1]))); assertNotEquals(ALIASES[index][0], values[index], old.getBaseValue(), 0D); - CompoundNBT root = chunk(source); + CompoundTag root = chunk(source); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); - WolfEntity positive = new WolfEntity(EntityType.WOLF, world); positive.load(fixedEntity(root)); - ModifiableAttributeInstance actual = positive.getAttribute(old.getAttribute()); + Wolf positive = new Wolf(EntityType.WOLF, world); positive.load(fixedEntity(root)); + AttributeInstance actual = positive.getAttribute(old.getAttribute()); assertEquals(ALIASES[index][0], values[index], actual.getBaseValue(), 0D); assertEquals(0.125D, actual.getModifier(new UUID(17L, 19L)).getAmount(), 0D); assertEquals(values[index] + 0.125D, actual.getValue(), 0D); @@ -86,14 +86,14 @@ public class LegacyForgeAttributeRegistryTest { } @Test public void playerReachSurvivesNativePlayerFixingAndNativeAttributeLoading() { - CompoundNBT source = entity("generic.reachDistance", 7.5D), before = source.copy(); - CompoundNBT actual = LegacyPlayerDataFixes.update(DataFixesManager.getDataFixer(), - DefaultTypeReferences.PLAYER, source, 1343); - CompoundNBT value = actual.getList("Attributes", 10).getCompound(0); + CompoundTag source = entity("generic.reachDistance", 7.5D), before = source.copy(); + CompoundTag actual = LegacyPlayerDataFixes.update(DataFixers.getDataFixer(), + DataFixTypes.PLAYER, source, 1343); + CompoundTag value = actual.getList("Attributes", 10).getCompound(0); assertEquals("forge:reach_distance", value.getString("Name")); assertSame(ForgeMod.REACH_DISTANCE.get(), ForgeRegistries.ATTRIBUTES.getValue( - new net.minecraft.util.ResourceLocation(value.getString("Name")))); - ModifiableAttributeInstance nativeAttribute = new ModifiableAttributeInstance(ForgeMod.REACH_DISTANCE.get(), unused -> {}); + new net.minecraft.resources.ResourceLocation(value.getString("Name")))); + AttributeInstance nativeAttribute = new AttributeInstance(ForgeMod.REACH_DISTANCE.get(), unused -> {}); nativeAttribute.load(value); assertEquals(7.5D, nativeAttribute.getBaseValue(), 0D); assertEquals(7.625D, nativeAttribute.getValue(), 0D); @@ -104,9 +104,9 @@ public class LegacyForgeAttributeRegistryTest { @Test public void allAliasCollisionsFailClosedWithoutChangingCaller() { for (String[] alias : ALIASES) for (String[] pair : new String[][] { {alias[0], alias[1]}, {alias[0], alias[0]}, {alias[1], alias[1]}}) { - CompoundNBT source = entity(pair[0], 2D); + CompoundTag source = entity(pair[0], 2D); source.getList("Attributes", 10).add(attribute(pair[1], 3D)); - CompoundNBT root = chunk(source), before = root.copy(); + CompoundTag root = chunk(source), before = root.copy(); try { LegacyEntityDataFixes.prepare(root); fail("Ambiguous attribute accepted: " + alias[0]); } catch (IllegalStateException expected) { assertEquals(before, root); } } @@ -114,18 +114,18 @@ public class LegacyForgeAttributeRegistryTest { @Test public void everyAliasSurvivesPlayerMountsAndPassengersWithoutWalkingPrivateData() { for (String[] alias : ALIASES) { - CompoundNBT source = entity(alias[0], 7.5D); - CompoundNBT vehicle = entity(alias[0], 6.5D); vehicle.putString("id", "minecraft:wolf"); - CompoundNBT passenger = entity(alias[0], 5.5D); passenger.putString("id", "minecraft:wolf"); - ListNBT passengers = new ListNBT(); passengers.add(passenger); vehicle.put("Passengers", passengers); - CompoundNBT rootVehicle = new CompoundNBT(); rootVehicle.put("Entity", vehicle); + CompoundTag source = entity(alias[0], 7.5D); + CompoundTag vehicle = entity(alias[0], 6.5D); vehicle.putString("id", "minecraft:wolf"); + CompoundTag passenger = entity(alias[0], 5.5D); passenger.putString("id", "minecraft:wolf"); + ListTag passengers = new ListTag(); passengers.add(passenger); vehicle.put("Passengers", passengers); + CompoundTag rootVehicle = new CompoundTag(); rootVehicle.put("Entity", vehicle); rootVehicle.putUUID("Attach", new UUID(23L, 29L)); source.put("RootVehicle", rootVehicle); source.put("PrivateData", entity(alias[0], 4.5D)); - CompoundNBT before = source.copy(); - CompoundNBT actual = LegacyPlayerDataFixes.update(DataFixesManager.getDataFixer(), - DefaultTypeReferences.PLAYER, source, 1343); - CompoundNBT fixedVehicle = actual.getCompound("RootVehicle").getCompound("Entity"); - for (CompoundNBT item : new CompoundNBT[] {actual, fixedVehicle, + CompoundTag before = source.copy(); + CompoundTag actual = LegacyPlayerDataFixes.update(DataFixers.getDataFixer(), + DataFixTypes.PLAYER, source, 1343); + CompoundTag fixedVehicle = actual.getCompound("RootVehicle").getCompound("Entity"); + for (CompoundTag item : new CompoundTag[] {actual, fixedVehicle, fixedVehicle.getList("Passengers", 10).getCompound(0)}) { assertEquals(alias[1], item.getList("Attributes", 10).getCompound(0).getString("Name")); assertEquals(0.125D, item.getList("Attributes", 10).getCompound(0) @@ -138,9 +138,9 @@ public class LegacyForgeAttributeRegistryTest { @Test public void everyAliasIsIdempotentWithSavedValuesIntact() { for (String[] alias : ALIASES) { - CompoundNBT root = chunk(entity(alias[0], 7.5D)); + CompoundTag root = chunk(entity(alias[0], 7.5D)); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); - CompoundNBT before = root.copy(); + CompoundTag before = root.copy(); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); assertEquals(before, root); } @@ -148,38 +148,38 @@ public class LegacyForgeAttributeRegistryTest { @Test public void modernChunksAndSimilarUnknownNamesRemainUntouched() { for (String[] alias : ALIASES) { - CompoundNBT root = chunk(entity(alias[0], 7D)); + CompoundTag root = chunk(entity(alias[0], 7D)); root.putInt("DataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); - CompoundNBT before = root.copy(); LegacyEntityDataFixes.prepare(root); assertEquals(before, root); + CompoundTag before = root.copy(); LegacyEntityDataFixes.prepare(root); assertEquals(before, root); root = chunk(entity(alias[0] + ".private", 7D)); before = root.copy(); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); assertEquals(before, root); } } - private static CompoundNBT entity(String name, double base) { - CompoundNBT value = new CompoundNBT(); value.putString("id", "example:vehicle"); - ListNBT attributes = new ListNBT(); attributes.add(attribute(name, base)); value.put("Attributes", attributes); + private static CompoundTag entity(String name, double base) { + CompoundTag value = new CompoundTag(); value.putString("id", "example:vehicle"); + ListTag attributes = new ListTag(); attributes.add(attribute(name, base)); value.put("Attributes", attributes); return value; } - private static CompoundNBT attribute(String name, double base) { - CompoundNBT value = new CompoundNBT(); value.putString("Name", name); value.putDouble("Base", base); + private static CompoundTag attribute(String name, double base) { + CompoundTag value = new CompoundTag(); value.putString("Name", name); value.putDouble("Base", base); value.putString("PrivateNote", "preserve-attribute-payload"); - CompoundNBT modifier = new CompoundNBT(); modifier.putString("Name", "customized-attribute"); + CompoundTag modifier = new CompoundTag(); modifier.putString("Name", "customized-attribute"); modifier.putLong("UUIDMost", 17L); modifier.putLong("UUIDLeast", 19L); modifier.putDouble("Amount", 0.125D); modifier.putInt("Operation", 0); modifier.putString("PrivateNote", "preserve-modifier-payload"); - ListNBT modifiers = new ListNBT(); modifiers.add(modifier); value.put("Modifiers", modifiers); + ListTag modifiers = new ListTag(); modifiers.add(modifier); value.put("Modifiers", modifiers); return value; } - private static CompoundNBT chunk(CompoundNBT entity) { - CompoundNBT value = new CompoundNBT(); value.putInt("DataVersion", 1343); - CompoundNBT level = new CompoundNBT(); ListNBT entities = new ListNBT(); entities.add(entity); + private static CompoundTag chunk(CompoundTag entity) { + CompoundTag value = new CompoundTag(); value.putInt("DataVersion", 1343); + CompoundTag level = new CompoundTag(); ListTag entities = new ListTag(); entities.add(entity); level.put("Entities", entities); value.put("Level", level); return value; } - private static CompoundNBT fixedEntity(CompoundNBT root) { return root.getCompound("Level").getList("Entities", 10).getCompound(0); } + private static CompoundTag fixedEntity(CompoundTag root) { return root.getCompound("Level").getList("Entities", 10).getCompound(0); } private static void set(Class owner, Object instance, String name, Object value) throws Exception { Field field = owner.getDeclaredField(name); field.setAccessible(true); field.set(instance, value); diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyMachineItemsTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java similarity index 72% rename from src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyMachineItemsTest.java rename to src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java index c9a3acf1..8fe79ba9 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyMachineItemsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; import com.mojang.datafixers.DSL; import com.mojang.datafixers.DataFixer; @@ -8,16 +8,16 @@ import java.util.List; import java.util.function.Consumer; import java.util.stream.Stream; -import net.minecraft.block.Blocks; -import net.minecraft.item.Items; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.NBTUtil; -import net.minecraft.nbt.StringNBT; -import net.minecraft.util.SharedConstants; -import net.minecraft.util.datafix.DataFixesManager; -import net.minecraft.util.datafix.DefaultTypeReferences; -import net.minecraft.util.datafix.TypeReferences; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.item.Items; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.StringTag; +import net.minecraft.SharedConstants; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.util.datafix.fixes.References; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.LogEvent; @@ -25,19 +25,19 @@ import org.apache.logging.log4j.core.appender.AbstractAppender; import org.junit.BeforeClass; import org.junit.Test; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityConveyor; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityConveyor; import static org.junit.Assert.*; public class LegacyMachineItemsTest { @BeforeClass public static void bootstrap() { - net.minecraft.util.registry.Bootstrap.bootStrap(); + net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); AdvantageLegacyMigrations.registerPowerAdvantage(); } @Test public void colouredWoolSurvivesAnActualConveyorInventoryLoad() { - CompoundNBT tile = convertedTile(stack("minecraft:wool", 14, 7)); - TileEntityConveyor conveyor = new TileEntityConveyor(); - conveyor.load(Blocks.AIR.defaultBlockState(), tile); + CompoundTag tile = convertedTile(stack("minecraft:wool", 14, 7)); + TileEntityConveyor conveyor = new TileEntityConveyor(net.minecraft.core.BlockPos.ZERO, Blocks.HOPPER.defaultBlockState()); + conveyor.load(tile); assertSame(Items.RED_WOOL, conveyor.getItem(0).getItem()); assertEquals(7, conveyor.getItem(0).getCount()); } @@ -46,7 +46,7 @@ public class LegacyMachineItemsTest { for (String[] row : new String[][] {{"minecraft:coal", "1", "minecraft:charcoal"}, {"minecraft:dye", "4", "minecraft:lapis_lazuli"}, {"minecraft:planks", "1", "minecraft:spruce_planks"}}) { - CompoundNBT item = convertedTile(stack(row[0], Integer.parseInt(row[1]), 7)) + CompoundTag item = convertedTile(stack(row[0], Integer.parseInt(row[1]), 7)) .getList("Items", 10).getCompound(0); assertEquals(row[2], item.getString("id")); assertEquals(7, item.getInt("Count")); @@ -55,16 +55,16 @@ public class LegacyMachineItemsTest { } @Test public void nativeItemFixersPreserveDamageAndEnchantmentEffects() { - CompoundNBT pick = stack("minecraft:diamond_pickaxe", 17, 1); - CompoundNBT tag = new CompoundNBT(); - ListNBT enchantments = new ListNBT(); - CompoundNBT enchantment = new CompoundNBT(); + CompoundTag pick = stack("minecraft:diamond_pickaxe", 17, 1); + CompoundTag tag = new CompoundTag(); + ListTag enchantments = new ListTag(); + CompoundTag enchantment = new CompoundTag(); enchantment.putShort("id", (short) 32); enchantment.putShort("lvl", (short) 3); enchantments.add(enchantment); tag.put("ench", enchantments); pick.put("tag", tag); - CompoundNBT actual = convertedTile(pick).getList("Items", 10).getCompound(0); + CompoundTag actual = convertedTile(pick).getList("Items", 10).getCompound(0); assertEquals(17, actual.getCompound("tag").getInt("Damage")); assertEquals("minecraft:efficiency", actual.getCompound("tag") .getList("Enchantments", 10).getCompound(0).getString("id")); @@ -72,41 +72,41 @@ public class LegacyMachineItemsTest { } @Test public void nestedStoredItemsAreConvertedWithoutChangingMachineState() { - CompoundNBT tile = tile(stack("minecraft:wool", 14, 7)); - CompoundNBT nested = new CompoundNBT(); + CompoundTag tile = tile(stack("minecraft:wool", 14, 7)); + CompoundTag nested = new CompoundTag(); nested.put("Reference", stack("minecraft:dye", 4, 1)); tile.put("Recipe", nested); tile.putFloat("Energy", 19.5F); tile.putString("CustomName", "keep"); - CompoundNBT actual = convert(chunk(tile), 512).getCompound("Level").getList("TileEntities", 10).getCompound(0); + CompoundTag actual = convert(chunk(tile), 512).getCompound("Level").getList("TileEntities", 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")); } @Test public void foreignPrivatePayloadsAreNotInterpretedAsAdvantageInventories() { - CompoundNBT foreign = tile(stack("minecraft:wool", 14, 7)); + CompoundTag foreign = tile(stack("minecraft:wool", 14, 7)); foreign.putString("id", "other:private_machine"); - CompoundNBT expected = foreign.copy(); - CompoundNBT root = chunk(foreign); + CompoundTag expected = foreign.copy(); + CompoundTag root = chunk(foreign); LegacyWorldDataHook.prepareLegacyChunk(root); LegacyWorldDataHook.prepareLegacyEntities(root); assertEquals(expected, root.getCompound("Level").getList("TileEntities", 10).getCompound(0)); } @Test public void currentVersionMachineDataIsNotRewritten() { - CompoundNBT root = chunk(tile(stack("minecraft:red_wool", 0, 7))); + CompoundTag root = chunk(tile(stack("minecraft:red_wool", 0, 7))); root.putInt("DataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); - CompoundNBT expected = root.copy(); + CompoundTag expected = root.copy(); LegacyWorldDataHook.prepareLegacyEntities(root); assertEquals(expected, root); } @Test public void repeatedPreparationDoesNotRepairAStoredTool() { - CompoundNBT root = chunk(tile(stack("minecraft:diamond_pickaxe", 17, 1))); + CompoundTag root = chunk(tile(stack("minecraft:diamond_pickaxe", 17, 1))); LegacyWorldDataHook.prepareLegacyChunk(root); LegacyWorldDataHook.prepareLegacyEntities(root); - CompoundNBT once = root.copy(); + CompoundTag once = root.copy(); LegacyWorldDataHook.prepareLegacyEntities(root); assertEquals(once, root); assertEquals(17, root.getCompound("Level").getList("TileEntities", 10).getCompound(0) @@ -114,66 +114,66 @@ public class LegacyMachineItemsTest { } @Test public void namedBookEnchantmentsAreNotReinterpretedAsProtection() { - CompoundNBT book = stack("minecraft:enchanted_book", 0, 1); - CompoundNBT tag = new CompoundNBT(); - ListNBT list = new ListNBT(); - CompoundNBT named = new CompoundNBT(); + CompoundTag book = stack("minecraft:enchanted_book", 0, 1); + CompoundTag tag = new CompoundTag(); + ListTag list = new ListTag(); + CompoundTag named = new CompoundTag(); named.putString("id", "steamadvantage:steam_capacity"); named.putShort("lvl", (short) 3); list.add(named); tag.put("StoredEnchantments", list); book.put("tag", tag); - CompoundNBT actual = convertedTile(book).getList("Items", 10).getCompound(0); + CompoundTag actual = convertedTile(book).getList("Items", 10).getCompound(0); assertEquals(list, actual.getCompound("tag").getList("StoredEnchantments", 10)); } @Test public void nativeNestedContainerItemsAreFixedExactlyOnce() { - CompoundNBT chest = stack("minecraft:chest", 0, 1); - CompoundNBT blockEntity = new CompoundNBT(); + CompoundTag chest = stack("minecraft:chest", 0, 1); + CompoundTag blockEntity = new CompoundTag(); blockEntity.putString("id", "Chest"); - ListNBT contents = new ListNBT(); + ListTag contents = new ListTag(); contents.add(stack("minecraft:diamond_pickaxe", 17, 1)); contents.add(stack("minecraft:wool", 14, 7)); blockEntity.put("Items", contents); - CompoundNBT tag = new CompoundNBT(); + CompoundTag tag = new CompoundTag(); tag.put("BlockEntityTag", blockEntity); chest.put("tag", tag); - CompoundNBT actual = convertedTile(chest).getList("Items", 10).getCompound(0); - ListNBT fixed = actual.getCompound("tag").getCompound("BlockEntityTag").getList("Items", 10); + CompoundTag actual = convertedTile(chest).getList("Items", 10).getCompound(0); + ListTag fixed = actual.getCompound("tag").getCompound("BlockEntityTag").getList("Items", 10); assertEquals(17, fixed.getCompound(0).getCompound("tag").getInt("Damage")); assertEquals("minecraft:red_wool", fixed.getCompound(1).getString("id")); assertEquals(7, fixed.getCompound(1).getInt("Count")); } @Test public void foreignItemPayloadInsideAnOwnedMachineRemainsVerbatim() { - CompoundNBT foreign = stack("other:component", 9, 1); - CompoundNBT tag = new CompoundNBT(); + CompoundTag foreign = stack("other:component", 9, 1); + CompoundTag tag = new CompoundTag(); tag.put("Private", stack("minecraft:wool", 14, 7)); foreign.put("tag", tag); assertEquals(foreign, convertedTile(foreign).getList("Items", 10).getCompound(0)); } @Test public void nestedNamedEnchantmentsSurviveNativeContainerFixes() { - CompoundNBT book = stack("minecraft:enchanted_book", 0, 1); - CompoundNBT tag = new CompoundNBT(); - ListNBT enchantments = new ListNBT(); - CompoundNBT enchantment = new CompoundNBT(); + CompoundTag book = stack("minecraft:enchanted_book", 0, 1); + CompoundTag tag = new CompoundTag(); + ListTag enchantments = new ListTag(); + CompoundTag enchantment = new CompoundTag(); enchantment.putString("id", "steamadvantage:steam_capacity"); enchantment.putShort("lvl", (short) 3); enchantments.add(enchantment); tag.put("StoredEnchantments", enchantments); book.put("tag", tag); - CompoundNBT root = chunk(tile(carrier(book))); + CompoundTag root = chunk(tile(carrier(book))); root.putInt("DataVersion", 1343); - CompoundNBT actual = convert(root, 1343).getCompound("Level").getList("TileEntities", 10) + CompoundTag actual = convert(root, 1343).getCompound("Level").getList("TileEntities", 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)); } @Test public void nestedForeignItemsRetainTheirPrivateMetadata() { - CompoundNBT foreign = stack("other:component", 9, 1); - CompoundNBT actual = convertedTile(carrier(foreign)).getList("Items", 10).getCompound(0) + CompoundTag foreign = stack("other:component", 9, 1); + CompoundTag actual = convertedTile(carrier(foreign)).getList("Items", 10).getCompound(0) .getCompound("tag").getCompound("BlockEntityTag").getList("Items", 10).getCompound(0); assertEquals(foreign, actual); } @@ -187,13 +187,13 @@ public class LegacyMachineItemsTest { } @Test public void native704EnvelopeUsesACopiedSingletonAndThePassedFixer() { - CompoundNBT source = stack("minecraft:chest", 0, 1); + CompoundTag source = stack("minecraft:chest", 0, 1); source.putIntArray("PrivateArray", new int[] {1, 2}); - CompoundNBT before = source.copy(); - CompoundNBT result = LegacyMachineItemDataFixes.upgradeTo704(source, 512, new EnvelopeFixer(root -> { + CompoundTag before = source.copy(); + CompoundTag result = LegacyMachineItemDataFixes.upgradeTo704(source, 512, new EnvelopeFixer(root -> { assertEquals(1, root.getAllKeys().size()); assertEquals(1, root.getList("Inventory", 10).size()); - CompoundNBT item = root.getList("Inventory", 10).getCompound(0); + CompoundTag item = root.getList("Inventory", 10).getCompound(0); assertEquals(before, item); item.getIntArray("PrivateArray")[0] = 99; item.putString("NativeField", "retained"); @@ -209,8 +209,8 @@ public class LegacyMachineItemsTest { rejectedEnvelope(root -> root.remove("Inventory")); rejectedEnvelope(root -> root.putString("Inventory", "not a list")); rejectedEnvelope(root -> { - ListNBT items = new ListNBT(); - items.add(StringNBT.valueOf("not a stack")); + ListTag items = new ListTag(); + items.add(StringTag.valueOf("not a stack")); root.put("Inventory", items); }); } @@ -237,9 +237,9 @@ public class LegacyMachineItemsTest { } @Test public void native704EnvelopeDoesNotAddASlotToAnUnslottedStack() { - CompoundNBT source = stack("minecraft:chest", 0, 1); + CompoundTag source = stack("minecraft:chest", 0, 1); source.remove("Slot"); - CompoundNBT before = source.copy(); + CompoundTag before = source.copy(); try { LegacyMachineItemDataFixes.upgradeTo704(source, 512, new EnvelopeFixer(root -> envelopeItem(root).putByte("Slot", (byte) 0))); @@ -248,7 +248,7 @@ public class LegacyMachineItemsTest { } @Test public void native704EnvelopeRejectsANonCompoundPlayerResult() { - CompoundNBT source = stack("minecraft:chest", 0, 1), before = source.copy(); + CompoundTag source = stack("minecraft:chest", 0, 1), before = source.copy(); DataFixer fixer = new EnvelopeFixer(root -> {}) { @Override public Dynamic update(DSL.TypeReference type, Dynamic input, int from, int to) { return new Dynamic<>(input.getOps(), input.getOps().createList(Stream.empty())); @@ -260,10 +260,10 @@ public class LegacyMachineItemsTest { } catch (IllegalStateException expected) { assertEquals(before, source); } } - private static CompoundNBT envelopeItem(CompoundNBT root) { return root.getList("Inventory", 10).getCompound(0); } + private static CompoundTag envelopeItem(CompoundTag root) { return root.getList("Inventory", 10).getCompound(0); } - private static void rejectedEnvelope(Consumer mutation) { - CompoundNBT source = stack("minecraft:chest", 0, 1), before = source.copy(); + private static void rejectedEnvelope(Consumer mutation) { + CompoundTag source = stack("minecraft:chest", 0, 1), before = source.copy(); try { LegacyMachineItemDataFixes.upgradeTo704(source, 512, new EnvelopeFixer(mutation)); fail("An unpairable native envelope must fail closed"); @@ -271,13 +271,13 @@ private static void rejectedEnvelope(Consumer mutation) { } private static class EnvelopeFixer implements DataFixer { - private final Consumer mutation; - EnvelopeFixer(Consumer mutation) { this.mutation = mutation; } + private final Consumer mutation; + EnvelopeFixer(Consumer mutation) { this.mutation = mutation; } @Override public Dynamic update(DSL.TypeReference type, Dynamic input, int from, int to) { - assertSame(TypeReferences.PLAYER, type); + assertSame(References.PLAYER, type); assertEquals(512, from); assertEquals(704, to); - mutation.accept((CompoundNBT) input.getValue()); + mutation.accept((CompoundTag) input.getValue()); return input; } @Override public Schema getSchema(int version) { throw new AssertionError("Not used by the envelope"); } @@ -299,33 +299,33 @@ private static final class ErrorCapture extends AbstractAppender implements Auto @Override public void close() { logger.removeAppender(this); stop(); } } - private static CompoundNBT carrier(CompoundNBT item) { - CompoundNBT chest = stack("minecraft:chest", 0, 1); - CompoundNBT blockEntity = new CompoundNBT(); + private static CompoundTag carrier(CompoundTag item) { + CompoundTag chest = stack("minecraft:chest", 0, 1); + CompoundTag blockEntity = new CompoundTag(); blockEntity.putString("id", "minecraft:chest"); - ListNBT contents = new ListNBT(); + ListTag contents = new ListTag(); contents.add(item); blockEntity.put("Items", contents); - CompoundNBT tag = new CompoundNBT(); + CompoundTag tag = new CompoundTag(); tag.put("BlockEntityTag", blockEntity); chest.put("tag", tag); return chest; } - private static CompoundNBT convertedTile(CompoundNBT item) { + private static CompoundTag convertedTile(CompoundTag item) { return convert(chunk(tile(item)), 512).getCompound("Level").getList("TileEntities", 10).getCompound(0); } - private static CompoundNBT convert(CompoundNBT root, int version) { + private static CompoundTag convert(CompoundTag root, int version) { LegacyWorldDataHook.prepareLegacyChunk(root); LegacyWorldDataHook.prepareLegacyEntities(root); - CompoundNBT fixed = NBTUtil.update(DataFixesManager.getDataFixer(), DefaultTypeReferences.CHUNK, + CompoundTag fixed = NbtUtils.update(DataFixers.getDataFixer(), DataFixTypes.CHUNK, root, version, SharedConstants.getCurrentVersion().getWorldVersion()); return LegacyWorldDataHook.finalizeLegacyChunk(fixed); } - private static CompoundNBT stack(String id, int damage, int count) { - CompoundNBT item = new CompoundNBT(); + private static CompoundTag stack(String id, int damage, int count) { + CompoundTag item = new CompoundTag(); item.putString("id", id); item.putByte("Count", (byte) count); item.putByte("Slot", (byte) 0); @@ -333,21 +333,21 @@ private static CompoundNBT stack(String id, int damage, int count) { return item; } - private static CompoundNBT tile(CompoundNBT item) { - CompoundNBT tile = new CompoundNBT(); + private static CompoundTag tile(CompoundTag item) { + CompoundTag tile = new CompoundTag(); tile.putString("id", "poweradvantage:item_conveyor"); tile.putInt("x", 0); tile.putInt("y", 64); tile.putInt("z", 0); - ListNBT items = new ListNBT(); + ListTag items = new ListTag(); items.add(item); tile.put("Items", items); return tile; } - private static CompoundNBT chunk(CompoundNBT tile) { - CompoundNBT root = new CompoundNBT(); + private static CompoundTag chunk(CompoundTag tile) { + CompoundTag root = new CompoundTag(); root.putInt("DataVersion", 512); - CompoundNBT level = new CompoundNBT(); - ListNBT tiles = new ListNBT(); + CompoundTag level = new CompoundTag(); + ListTag tiles = new ListTag(); tiles.add(tile); level.put("TileEntities", tiles); root.put("Level", level); diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyPlayerDataFixesTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixesTest.java similarity index 71% rename from src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyPlayerDataFixesTest.java rename to src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixesTest.java index 767c13f9..5f8a523e 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyPlayerDataFixesTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixesTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; import com.mojang.datafixers.DSL; import com.mojang.datafixers.DataFixer; @@ -9,27 +9,27 @@ import java.util.Collections; import java.util.List; import java.util.function.Consumer; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.util.SharedConstants; -import net.minecraft.util.datafix.DataFixesManager; -import net.minecraft.util.datafix.DefaultTypeReferences; -import net.minecraft.util.datafix.TypeReferences; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.SharedConstants; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.util.datafix.fixes.References; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.*; public class LegacyPlayerDataFixesTest { - @BeforeClass public static void bootstrap() { net.minecraft.util.registry.Bootstrap.bootStrap(); } + @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); } @Test public void savedRegistryBookNamesSurviveBothPlayerInventories() { for (String inventory : new String[] {"Inventory", "EnderItems"}) { for (String name : new String[] {"minecraft:efficiency", "steamadvantage:steam_capacity"}) { - CompoundNBT book = enchanted("minecraft:enchanted_book", "StoredEnchantments", false); + CompoundTag book = enchanted("minecraft:enchanted_book", "StoredEnchantments", false); assertTrue(LegacyWorldDataHook.migrateLegacyEnchantments(book.getCompound("tag"), Collections.singletonMap(32, name))); - CompoundNBT source = player(inventory, book), before = source.copy(); - CompoundNBT result = convert(source, 512); + CompoundTag source = player(inventory, book), before = source.copy(); + CompoundTag result = convert(source, 512); assertEquals(name, enchantment(item(result, inventory, 0), "StoredEnchantments").getString("id")); assertEquals(3, enchantment(item(result, inventory, 0), "StoredEnchantments").getInt("lvl")); assertEquals(before, source); @@ -39,7 +39,7 @@ public class LegacyPlayerDataFixesTest { @Test public void namedOldEnchAndAlreadyModernEnchantmentsSurvive() { for (String key : new String[] {"ench", "Enchantments"}) { - CompoundNBT result = convert(player("Inventory", enchanted("minecraft:diamond_pickaxe", key, true)), 512); + CompoundTag result = convert(player("Inventory", enchanted("minecraft:diamond_pickaxe", key, true)), 512); assertEquals("steamadvantage:steam_capacity", enchantment(item(result, "Inventory", 0), "Enchantments").getString("id")); assertEquals(17, item(result, "Inventory", 0).getCompound("tag").getInt("Damage")); } @@ -47,30 +47,30 @@ public class LegacyPlayerDataFixesTest { @Test public void numericEnchantmentsStillUseTheNativeMapping() { for (String key : new String[] {"ench", "StoredEnchantments"}) { - CompoundNBT result = convert(player("Inventory", enchanted("minecraft:enchanted_book", key, false)), 512); + CompoundTag result = convert(player("Inventory", enchanted("minecraft:enchanted_book", key, false)), 512); String outputKey = key.equals("ench") ? "Enchantments" : key; assertEquals("minecraft:efficiency", enchantment(item(result, "Inventory", 0), outputKey).getString("id")); } } @Test public void nativeContainerBoundaryFixesNestedItemsExactlyOnce() { - CompoundNBT chest = stack("minecraft:chest", 0); - CompoundNBT blockEntity = new CompoundNBT(); + CompoundTag chest = stack("minecraft:chest", 0); + CompoundTag blockEntity = new CompoundTag(); blockEntity.putString("id", "Chest"); - ListNBT contents = new ListNBT(); + ListTag contents = new ListTag(); contents.add(enchanted("minecraft:diamond_pickaxe", "ench", true)); - CompoundNBT wool = stack("minecraft:wool", 14); + CompoundTag wool = stack("minecraft:wool", 14); wool.putByte("Slot", (byte) 1); contents.add(wool); - CompoundNBT book = enchanted("minecraft:enchanted_book", "StoredEnchantments", true); + CompoundTag book = enchanted("minecraft:enchanted_book", "StoredEnchantments", true); book.putByte("Slot", (byte) 2); contents.add(book); blockEntity.put("Items", contents); - CompoundNBT tag = new CompoundNBT(); + CompoundTag tag = new CompoundTag(); tag.put("BlockEntityTag", blockEntity); chest.put("tag", tag); - CompoundNBT source = player("Inventory", chest), before = source.copy(); - ListNBT fixed = item(convert(source, 512), "Inventory", 0).getCompound("tag") + CompoundTag source = player("Inventory", chest), before = source.copy(); + ListTag fixed = item(convert(source, 512), "Inventory", 0).getCompound("tag") .getCompound("BlockEntityTag").getList("Items", 10); assertEquals(17, fixed.getCompound(0).getCompound("tag").getInt("Damage")); assertEquals("minecraft:red_wool", fixed.getCompound(1).getString("id")); @@ -80,25 +80,25 @@ public class LegacyPlayerDataFixesTest { } @Test public void foreignItemPayloadIsNotRestoredOverNativeChanges() { - CompoundNBT foreign = enchanted("other:component", "StoredEnchantments", true); + CompoundTag foreign = enchanted("other:component", "StoredEnchantments", true); foreign.getCompound("tag").putInt("Private", 19); - CompoundNBT actual = item(convert(player("Inventory", foreign), 512), "Inventory", 0); + CompoundTag actual = item(convert(player("Inventory", foreign), 512), "Inventory", 0); assertFalse("Native damage removal must not be undone by a whole-stack overlay", actual.contains("Damage")); assertEquals(19, actual.getCompound("tag").getInt("Private")); assertEquals("steamadvantage:steam_capacity", enchantment(actual, "StoredEnchantments").getString("id")); } @Test public void restorationChangesOnlyTheSavedName() { - CompoundNBT source = player("Inventory", enchanted("other:component", "StoredEnchantments", true)); + CompoundTag source = player("Inventory", enchanted("other:component", "StoredEnchantments", true)); RecordingFixer fixer = new RecordingFixer(root -> { - CompoundNBT book = item(root, "Inventory", 0); + CompoundTag book = item(root, "Inventory", 0); book.putString("id", "other:native_result"); enchantment(book, "StoredEnchantments").putString("id", "minecraft:protection"); enchantment(book, "StoredEnchantments").putShort("lvl", (short) 5); book.getCompound("tag").putInt("NativeField", 42); }); - CompoundNBT result = LegacyPlayerDataFixes.update(fixer, DefaultTypeReferences.PLAYER, source, 704); - CompoundNBT book = item(result, "Inventory", 0); + CompoundTag result = LegacyPlayerDataFixes.update(fixer, DataFixTypes.PLAYER, source, 704); + CompoundTag book = item(result, "Inventory", 0); assertEquals("other:native_result", book.getString("id")); assertEquals(42, book.getCompound("tag").getInt("NativeField")); assertEquals(5, enchantment(book, "StoredEnchantments").getInt("lvl")); @@ -106,15 +106,15 @@ public class LegacyPlayerDataFixesTest { } @Test public void sourceIsDetachedEvenWhenFixerMutatesNestedTagsAndArrays() { - CompoundNBT source = player("Inventory", stack("minecraft:stone", 0)); + CompoundTag source = player("Inventory", stack("minecraft:stone", 0)); source.putIntArray("PrivateArray", new int[] {1, 2}); - CompoundNBT before = source.copy(); + CompoundTag before = source.copy(); RecordingFixer fixer = new RecordingFixer(root -> { root.getIntArray("PrivateArray")[0] = 99; item(root, "Inventory", 0).getCompound("tag").putInt("Native", 1); root.putString("Native", "changed"); }); - CompoundNBT result = LegacyPlayerDataFixes.update(fixer, DefaultTypeReferences.PLAYER, source, 512); + CompoundTag result = LegacyPlayerDataFixes.update(fixer, DataFixTypes.PLAYER, source, 512); assertEquals(before, source); assertNotSame(source, result); item(result, "Inventory", 0).putInt("AfterReturn", 1); @@ -125,7 +125,7 @@ public class LegacyPlayerDataFixesTest { int current = SharedConstants.getCurrentVersion().getWorldVersion(); for (int source : new int[] {-1, 512, 703, 704, 1343, 2230}) { RecordingFixer fixer = new RecordingFixer(root -> {}); - LegacyPlayerDataFixes.update(fixer, DefaultTypeReferences.PLAYER, player("Inventory", stack("minecraft:stone", 0)), source); + LegacyPlayerDataFixes.update(fixer, DataFixTypes.PLAYER, player("Inventory", stack("minecraft:stone", 0)), source); List expected = source < 704 ? Arrays.asList(source + "->704", "704->" + current) : Collections.singletonList(source + "->" + current); assertEquals(expected, fixer.calls); @@ -134,9 +134,9 @@ public class LegacyPlayerDataFixesTest { @Test public void currentOrNewerDataIsCopiedWithoutOldVersionDfu() { for (int version : new int[] {SharedConstants.getCurrentVersion().getWorldVersion(), Integer.MAX_VALUE}) { - CompoundNBT source = player("Inventory", stack("minecraft:diamond_pickaxe", 17)); + CompoundTag source = player("Inventory", stack("minecraft:diamond_pickaxe", 17)); RecordingFixer fixer = new RecordingFixer(root -> fail("Current data must not be reinterpreted")); - CompoundNBT result = LegacyPlayerDataFixes.update(fixer, DefaultTypeReferences.PLAYER, source, version); + CompoundTag result = LegacyPlayerDataFixes.update(fixer, DataFixTypes.PLAYER, source, version); assertEquals(source, result); assertNotSame(source, result); assertTrue(fixer.calls.isEmpty()); @@ -159,11 +159,11 @@ public class LegacyPlayerDataFixesTest { @Test public void addingAnItemToAnEmptyInventoryFailsClosed() { for (String inventory : new String[] {"Inventory", "EnderItems"}) { - CompoundNBT source = new CompoundNBT(); - source.put(inventory, new ListNBT()); + CompoundTag source = new CompoundTag(); + source.put(inventory, new ListTag()); try { LegacyPlayerDataFixes.update(new RecordingFixer(root -> root.getList(inventory, 10) - .add(stack("minecraft:stone", 0))), DefaultTypeReferences.PLAYER, source, 704); + .add(stack("minecraft:stone", 0))), DataFixTypes.PLAYER, source, 704); fail("An empty inventory must not acquire an unpaired item"); } catch (IllegalStateException expected) { assertTrue(source.getList(inventory, 10).isEmpty()); } } @@ -171,8 +171,8 @@ public class LegacyPlayerDataFixesTest { @Test public void post704PlayerBooksRetainNames() { for (int version : new int[] {704, 1343, 1519, 2230}) { - CompoundNBT source = player("EnderItems", enchanted("minecraft:enchanted_book", "StoredEnchantments", true)); - CompoundNBT result = convert(source, version); + CompoundTag source = player("EnderItems", enchanted("minecraft:enchanted_book", "StoredEnchantments", true)); + CompoundTag result = convert(source, version); assertEquals("steamadvantage:steam_capacity", enchantment(item(result, "EnderItems", 0), "StoredEnchantments").getString("id")); } } @@ -188,67 +188,67 @@ public class LegacyPlayerDataFixesTest { @Test public void wrongNativeTypeIsRejected() { try { - LegacyPlayerDataFixes.update(DataFixesManager.getDataFixer(), DefaultTypeReferences.CHUNK, - new CompoundNBT(), 512); + LegacyPlayerDataFixes.update(DataFixers.getDataFixer(), DataFixTypes.CHUNK, + new CompoundTag(), 512); fail("The player-only wrapper must reject a different type"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("PLAYER")); } } - private static void rejected(Consumer mutation) { - CompoundNBT source = player("Inventory", enchanted("minecraft:enchanted_book", "StoredEnchantments", true)); - CompoundNBT before = source.copy(); + private static void rejected(Consumer mutation) { + CompoundTag source = player("Inventory", enchanted("minecraft:enchanted_book", "StoredEnchantments", true)); + CompoundTag before = source.copy(); try { - LegacyPlayerDataFixes.update(new RecordingFixer(mutation), DefaultTypeReferences.PLAYER, source, 704); + LegacyPlayerDataFixes.update(new RecordingFixer(mutation), DataFixTypes.PLAYER, source, 704); fail("An unpairable native inventory result must fail closed"); } catch (IllegalStateException expected) { assertEquals(before, source); } } - private static CompoundNBT convert(CompoundNBT source, int version) { - return LegacyPlayerDataFixes.update(DataFixesManager.getDataFixer(), DefaultTypeReferences.PLAYER, source, version); + private static CompoundTag convert(CompoundTag source, int version) { + return LegacyPlayerDataFixes.update(DataFixers.getDataFixer(), DataFixTypes.PLAYER, source, version); } - static CompoundNBT player(String inventory, CompoundNBT item) { - CompoundNBT player = new CompoundNBT(); + static CompoundTag player(String inventory, CompoundTag item) { + CompoundTag player = new CompoundTag(); player.putInt("DataVersion", 512); - ListNBT items = new ListNBT(); + ListTag items = new ListTag(); items.add(item); player.put(inventory, items); return player; } - static CompoundNBT stack(String id, int damage) { - CompoundNBT item = new CompoundNBT(); + static CompoundTag stack(String id, int damage) { + CompoundTag item = new CompoundTag(); item.putString("id", id); item.putByte("Count", (byte) 1); item.putByte("Slot", (byte) 0); item.putShort("Damage", (short) damage); - item.put("tag", new CompoundNBT()); + item.put("tag", new CompoundTag()); return item; } - static CompoundNBT enchanted(String id, String key, boolean named) { - CompoundNBT item = stack(id, 17); - CompoundNBT entry = new CompoundNBT(); + static CompoundTag enchanted(String id, String key, boolean named) { + CompoundTag item = stack(id, 17); + CompoundTag entry = new CompoundTag(); if (named) entry.putString("id", "steamadvantage:steam_capacity"); else entry.putShort("id", (short) 32); entry.putShort("lvl", (short) 3); - ListNBT enchantments = new ListNBT(); + ListTag enchantments = new ListTag(); enchantments.add(entry); item.getCompound("tag").put(key, enchantments); return item; } - static CompoundNBT item(CompoundNBT root, String inventory, int index) { return root.getList(inventory, 10).getCompound(index); } - static CompoundNBT enchantment(CompoundNBT item, String key) { return item.getCompound("tag").getList(key, 10).getCompound(0); } + static CompoundTag item(CompoundTag root, String inventory, int index) { return root.getList(inventory, 10).getCompound(index); } + static CompoundTag enchantment(CompoundTag item, String key) { return item.getCompound("tag").getList(key, 10).getCompound(0); } private static final class RecordingFixer implements DataFixer { - private final Consumer mutation; + private final Consumer mutation; final List calls = new ArrayList<>(); - RecordingFixer(Consumer mutation) { this.mutation = mutation; } + RecordingFixer(Consumer mutation) { this.mutation = mutation; } @Override public Dynamic update(DSL.TypeReference type, Dynamic input, int from, int to) { - assertSame(TypeReferences.PLAYER, type); + assertSame(References.PLAYER, type); calls.add(from + "->" + to); - mutation.accept((CompoundNBT) input.getValue()); + mutation.accept((CompoundTag) input.getValue()); return input; } @Override public Schema getSchema(int version) { throw new AssertionError("Not used by the wrapper"); } diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyPlayerTransformerTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java similarity index 90% rename from src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyPlayerTransformerTest.java rename to src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java index 06004e9d..05e3e645 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyPlayerTransformerTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; import java.io.InputStream; import java.io.InputStreamReader; @@ -16,15 +16,15 @@ import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.util.datafix.DataFixesManager; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.datafix.DataFixers; import static org.junit.Assert.*; public class LegacyPlayerTransformerTest { - private static final String NATIVE = "net/minecraft/nbt/NBTUtil"; - private static final String WRAPPER = "zone/moddev/mc/poweradvantage/migration/LegacyPlayerDataFixes"; - private static final String DESC = "(Lcom/mojang/datafixers/DataFixer;Lnet/minecraft/util/datafix/DefaultTypeReferences;" - + "Lnet/minecraft/nbt/CompoundNBT;I)Lnet/minecraft/nbt/CompoundNBT;"; + private static final String NATIVE = "net/minecraft/nbt/NbtUtils"; + private static final String WRAPPER = "zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes"; + private static final String DESC = "(Lcom/mojang/datafixers/DataFixer;Lnet/minecraft/util/datafix/DataFixTypes;" + + "Lnet/minecraft/nbt/CompoundTag;I)Lnet/minecraft/nbt/CompoundTag;"; private static final String[][] TARGETS = {{"PlayerData", "legacy_player_file"}, {"ServerWorldInfo", "legacy_embedded_player"}}; @Test public void exactNativeLoosePlayerLoadAndEmbeddedUpdateArePatched() throws Exception { @@ -58,7 +58,7 @@ public class LegacyPlayerTransformerTest { } @Test public void transformedNativeEmbeddedGetterExecutesAndDoesNotReconvert() throws Exception { - net.minecraft.util.registry.Bootstrap.bootStrap(); + net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); ClassNode node = nativeClass("ServerWorldInfo"); transform(node, "legacy_embedded_player"); ClassWriter writer = new ClassWriter(0); @@ -68,11 +68,11 @@ public class LegacyPlayerTransformerTest { Class define() { return defineClass(null, bytes, 0, bytes.length); } }.define(); for (int version : new int[] {512, 1343}) { - CompoundNBT source = LegacyPlayerDataFixesTest.player("Inventory", LegacyPlayerDataFixesTest + CompoundTag source = LegacyPlayerDataFixesTest.player("Inventory", LegacyPlayerDataFixesTest .enchanted("minecraft:enchanted_book", "StoredEnchantments", true)); - CompoundNBT before = source.copy(); + CompoundTag before = source.copy(); Object info = embeddedInfo(type, source, version); - CompoundNBT result = (CompoundNBT) type.getMethod("getLoadedPlayerTag").invoke(info); + CompoundTag result = (CompoundTag) type.getMethod("getLoadedPlayerTag").invoke(info); assertEquals("steamadvantage:steam_capacity", LegacyPlayerDataFixesTest.enchantment( LegacyPlayerDataFixesTest.item(result, "Inventory", 0), "StoredEnchantments").getString("id")); assertSame("The native upgraded flag must prevent repeat conversion", result, @@ -163,13 +163,14 @@ private static int calls(ClassNode node, String owner) { private static ClassNode nativeClass(String name) throws Exception { ClassNode node = new ClassNode(); - try (InputStream input = resource("/net/minecraft/world/storage/" + name + ".class")) { + String nativeName = name.equals("PlayerData") ? "PlayerDataStorage" : "PrimaryLevelData"; + try (InputStream input = resource("/net/minecraft/world/level/storage/" + nativeName + ".class")) { new ClassReader(input).accept(node, 0); } return node; } - private static Object embeddedInfo(Class type, CompoundNBT source, int version) 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. for (Constructor constructor : type.getDeclaredConstructors()) { @@ -182,7 +183,7 @@ private static Object embeddedInfo(Class type, CompoundNBT source, int versio if (types[i] == long.class) args[i] = 0L; if (types[i] == float.class) args[i] = 0F; } - args[0] = DataFixesManager.getDataFixer(); + args[0] = DataFixers.getDataFixer(); args[1] = version; args[2] = source; constructor.setAccessible(true); diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyTransformerOrderTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyTransformerOrderTest.java similarity index 82% rename from src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyTransformerOrderTest.java rename to src/test/java/zone/moddev/mc/basepower/migration/LegacyTransformerOrderTest.java index 5c742c3b..945cf784 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyTransformerOrderTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyTransformerOrderTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; import java.io.InputStream; import java.io.InputStreamReader; @@ -24,9 +24,9 @@ public class LegacyTransformerOrderTest { private static void check(boolean baseFirst, boolean productionNames) throws Exception { String[][] targets = { - {"net/minecraft/world/storage/SaveFormat", "legacy_world_info", "prepareLegacyWorld"}, - {"net/minecraft/world/chunk/storage/ChunkLoader", "legacy_chunk_status", "prepareLegacyChunk"}, - {"net/minecraft/world/gen/WorldGenRegion", "legacy_worldgen_guard", "shouldBlockWorldgenWrite"} + {"net/minecraft/world/level/storage/LevelStorageSource", "legacy_world_info", "prepareLegacyWorld"}, + {"net/minecraft/world/level/chunk/storage/ChunkStorage", "legacy_chunk_status", "prepareLegacyChunk"}, + {"net/minecraft/server/level/WorldGenRegion", "legacy_worldgen_guard", "shouldBlockWorldgenWrite"} }; for (String[] target : targets) { ClassNode node = new ClassNode(); @@ -35,13 +35,13 @@ private static void check(boolean baseFirst, boolean productionNames) throws Exc if (!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, "poweradvantage", target[2])); + assertEquals(target[0], 1, calls(node, "basepower", target[2])); assertEquals(target[0], 1, calls(node, "basemetals", target[2])); if (target[1].equals("legacy_chunk_status")) { - assertEquals(1, calls(node, "poweradvantage", "finalizeLegacyChunk")); + assertEquals(1, calls(node, "basepower", "finalizeLegacyChunk")); assertEquals(1, calls(node, "basemetals", "finalizeLegacyChunk")); - assertEquals(1, calls(node, "poweradvantage", "copyChunkForMigration")); - assertEquals(1, calls(node, "poweradvantage", "prepareLegacyEntities")); + assertEquals(1, calls(node, "basepower", "copyChunkForMigration")); + assertEquals(1, calls(node, "basepower", "prepareLegacyEntities")); assertEntityPreparationOrder(node); } } @@ -62,7 +62,7 @@ private static void assertEntityPreparationOrder(ClassNode node) { assertEquals("Both mod item migrations must precede entity DFU", 2, preparations); entitiesPrepared = true; } - if (call.owner.equals(node.name) && call.desc.equals("(Lnet/minecraft/nbt/CompoundNBT;)I")) { + if (call.owner.equals(node.name) && call.desc.equals("(Lnet/minecraft/nbt/CompoundTag;)I")) { assertTrue("Entity staging must precede native version handling", entitiesPrepared); } } @@ -71,9 +71,9 @@ 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_116_compatibility.js" : "/coremods/poweradvantage_116_migration.js"; + String path = base ? "/coremods/basemetals_117_compatibility.js" : "/coremods/poweradvantage_116_migration.js"; ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); - assertNotNull("Transformer tests require the Java 8 test toolchain", engine); + assertNotNull("Transformer tests require Forge's bundled Nashorn engine", engine); try (InputStreamReader reader = new InputStreamReader(getResource(path), StandardCharsets.UTF_8)) { engine.eval(reader); } engine.put("targetClass", node); assertSame(node, engine.eval("initializeCoreMod()." + mod + "_" + target + ".transformer(targetClass)")); diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyWorldMetadataTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyWorldMetadataTest.java new file mode 100644 index 00000000..0c97f013 --- /dev/null +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyWorldMetadataTest.java @@ -0,0 +1,88 @@ +package zone.moddev.mc.basepower.migration; + +import static org.junit.Assert.assertEquals; + +import java.util.Set; + +import cyano.poweradvantage.api.migration.LegacyMigrationApi; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import org.junit.BeforeClass; +import org.junit.Test; + +public class LegacyWorldMetadataTest { + @BeforeClass public static void registerAliases() { + AdvantageLegacyMigrations.registerPowerAdvantage(); + LegacyMigrationApi.registerNamespaceAlias("test_old_expansion", "test_new_expansion"); + } + + @Test public void migratesSavedModAndPackNamesWithoutTouchingRegistries() { + CompoundTag root = world(mods(mod("poweradvantage", "2.4.1"), mod("unrelated", "1")), + packs("vanilla", "mod:poweradvantage", "file/poweradvantage.zip", "mod:unrelated"), + packs("mod:test_old_expansion")); + CompoundTag expected = world(mods(mod("basepower", "2.4.1"), mod("unrelated", "1")), + packs("vanilla", "mod:basepower", "file/poweradvantage.zip", "mod:unrelated"), + packs("mod:test_new_expansion")); + LegacyWorldMetadata.remap(root, Set.of("basepower", "test_new_expansion")::contains); + assertEquals(expected, root); + LegacyWorldMetadata.remap(root, Set.of("basepower", "test_new_expansion")::contains); + assertEquals(expected, root); + } + + @Test public void keepsTheCurrentPackChoiceAndVersionIfAlreadyRecorded() { + CompoundTag root = world(mods(mod("poweradvantage", "2.4.1"), mod("basepower", "2.4.9")), + packs("vanilla", "mod:poweradvantage"), packs("mod:basepower")); + LegacyWorldMetadata.remap(root, Set.of("basepower")::contains); + assertEquals(world(mods(mod("basepower", "2.4.9")), packs("vanilla"), packs("mod:basepower")), root); + } + + @Test public void leavesUninstalledReplacementsAndStillInstalledLegacyModsVisible() { + CompoundTag root = world(mods(mod("poweradvantage", "2.4.1"), mod("test_old_expansion", "1")), + packs("mod:test_old_expansion", "mod:invalid:id"), packs("mod:poweradvantage")); + CompoundTag original = root.copy(); + LegacyWorldMetadata.remap(root, Set.of("poweradvantage", "basepower")::contains); + assertEquals(original, root); + } + + @Test public void doesNotAddMissingMetadataToPreFlatteningOrNewWorlds() { + CompoundTag root = new CompoundTag(); + root.put("FML", mod("poweradvantage", "2.4.0")); + CompoundTag original = root.copy(); + LegacyWorldMetadata.remap(root, Set.of("basepower")::contains); + assertEquals(original, root); + } + + private static CompoundTag world(ListTag mods, ListTag enabled, ListTag disabled) { + CompoundTag root = new CompoundTag(), fml = new CompoundTag(), data = new CompoundTag(); + CompoundTag registries = new CompoundTag(), packs = new CompoundTag(); + registries.putString("witness", "poweradvantage:fluid_pipe"); + fml.put("LoadingModList", mods); + fml.put("Registries", registries); + packs.put("Enabled", enabled); + packs.put("Disabled", disabled); + data.put("DataPacks", packs); + root.put("fml", fml); + root.put("Data", data); + return root; + } + + private static ListTag packs(String... ids) { + ListTag result = new ListTag(); + for (String id : ids) result.add(StringTag.valueOf(id)); + return result; + } + + private static ListTag mods(CompoundTag... mods) { + ListTag result = new ListTag(); + for (CompoundTag mod : mods) result.add(mod); + return result; + } + + private static CompoundTag mod(String id, String version) { + CompoundTag mod = new CompoundTag(); + mod.putString("ModId", id); + mod.putString("ModVersion", version); + return mod; + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/RetiredProfessionSnapshotTest.java b/src/test/java/zone/moddev/mc/basepower/migration/RetiredProfessionSnapshotTest.java similarity index 64% rename from src/test/java/zone/moddev/mc/poweradvantage/migration/RetiredProfessionSnapshotTest.java rename to src/test/java/zone/moddev/mc/basepower/migration/RetiredProfessionSnapshotTest.java index 179777ed..d3a20a84 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/RetiredProfessionSnapshotTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/RetiredProfessionSnapshotTest.java @@ -1,15 +1,15 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ListNBT; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; import org.junit.Test; import static org.junit.Assert.*; public class RetiredProfessionSnapshotTest { @Test public void removesOnlyRetiredVanillaIndexAndIsIdempotent() { for (String namespace : new String[] {"FML", "fml"}) { - CompoundNBT root = world(namespace, "minecraft:farmer"); - CompoundNBT player = new CompoundNBT(); + CompoundTag root = world(namespace, "minecraft:farmer"); + CompoundTag player = new CompoundTag(); player.putInt("XpLevel", 34); root.put("Player", player); assertTrue(LegacyWorldDataHook.removeRetiredVanillaProfessionRegistry(root)); @@ -20,25 +20,25 @@ public class RetiredProfessionSnapshotTest { } @Test public void preservesCustomProfessionIndexInsteadOfHidingMissingMappings() { - CompoundNBT root = world("fml", "othermod:machinist"); - CompoundNBT before = root.copy(); + CompoundTag root = world("fml", "othermod:machinist"); + CompoundTag before = root.copy(); assertFalse(LegacyWorldDataHook.removeRetiredVanillaProfessionRegistry(root)); assertEquals(before, root); } - private static CompoundNBT world(String namespace, String profession) { - CompoundNBT entry = new CompoundNBT(); + private static CompoundTag world(String namespace, String profession) { + CompoundTag entry = new CompoundTag(); entry.putString("K", profession); entry.putInt("V", 1); - ListNBT ids = new ListNBT(); + ListTag ids = new ListTag(); ids.add(entry); - CompoundNBT snapshot = new CompoundNBT(); + CompoundTag snapshot = new CompoundTag(); snapshot.put("ids", ids); - CompoundNBT registries = new CompoundNBT(); + CompoundTag registries = new CompoundTag(); registries.put("minecraft:villagerprofessions", snapshot); - CompoundNBT fml = new CompoundNBT(); + CompoundTag fml = new CompoundTag(); fml.put("Registries", registries); - CompoundNBT root = new CompoundNBT(); + CompoundTag root = new CompoundTag(); root.put(namespace, fml); return root; } diff --git a/src/test/java/zone/moddev/mc/basepower/migration/SeparateEntityStorageTest.java b/src/test/java/zone/moddev/mc/basepower/migration/SeparateEntityStorageTest.java new file mode 100644 index 00000000..29932dd3 --- /dev/null +++ b/src/test/java/zone/moddev/mc/basepower/migration/SeparateEntityStorageTest.java @@ -0,0 +1,68 @@ +package zone.moddev.mc.basepower.migration; + +import java.util.UUID; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +public class SeparateEntityStorageTest { + @BeforeClass public static void bootstrap() { + net.minecraft.SharedConstants.tryDetectVersion(); + net.minecraft.server.Bootstrap.bootStrap(); + AdvantageLegacyMigrations.registerPowerAdvantage(); + } + + @Test public void separateEntityStoragePreservesForeignPayloadsAndPasseningTreesOnACopy() { + CompoundTag source = fixture(); + CompoundTag before = source.copy(); + CompoundTag staged = LegacyWorldDataHook.prepareEntityChunkForMigration(source); + assertEquals(before, source); + assertNotSame(source, staged); + assertEquals(0, staged.getList("Entities", 10).size()); + LegacyWorldDataHook.finalizeLegacyChunk(staged); + CompoundTag entity = staged.getList("Entities", 10).getCompound(0); + assertEquals("foreign:machine_witness", entity.getString("id")); + assertEquals("keep this payload", entity.getString("ForeignState")); + assertEquals(new UUID(0, 17), entity.getUUID("UUID")); + assertEquals(new UUID(9, 0), entity.getList("Passengers", 10).getCompound(0).getUUID("UUID")); + assertArrayEquals(new int[] {7, -8}, staged.getIntArray("Position")); + assertFalse(staged.contains("AdvantageLegacyEntityDataFix")); + assertEquals(before, source); + } + + @Test(expected = IllegalStateException.class) public void missingChunkCoordinatesFailClosed() { + CompoundTag source = fixture(); + source.remove("Position"); + LegacyWorldDataHook.prepareEntityChunkForMigration(source); + } + + @Test(expected = IllegalStateException.class) public void malformedChunkCoordinatesFailClosed() { + CompoundTag source = fixture(); + source.putIntArray("Position", new int[] {7}); + LegacyWorldDataHook.prepareEntityChunkForMigration(source); + } + + private static CompoundTag fixture() { + CompoundTag root = new CompoundTag(); + root.putInt("DataVersion", 2586); + root.putIntArray("Position", new int[] {7, -8}); + CompoundTag entity = new CompoundTag(); + entity.putString("id", "foreign:machine_witness"); + entity.putString("ForeignState", "keep this payload"); + entity.putLong("UUIDMost", 0); + entity.putLong("UUIDLeast", 17); + CompoundTag passenger = new CompoundTag(); + passenger.putString("id", "foreign:passenger"); + passenger.putLong("UUIDMost", 9); + passenger.putLong("UUIDLeast", 0); + ListTag riders = new ListTag(); + riders.add(passenger); + entity.put("Passengers", riders); + ListTag entities = new ListTag(); + entities.add(entity); + root.put("Entities", entities); + return root; + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/VanillaRegistryMappingEventTest.java b/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryMappingEventTest.java similarity index 98% rename from src/test/java/zone/moddev/mc/poweradvantage/migration/VanillaRegistryMappingEventTest.java rename to src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryMappingEventTest.java index 49ed2a40..02340093 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/VanillaRegistryMappingEventTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryMappingEventTest.java @@ -1,8 +1,8 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; import java.lang.reflect.Proxy; import java.util.Collections; -import net.minecraft.util.ResourceLocation; +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.event.RegistryEvent.MissingMappings; import net.minecraftforge.registries.ForgeRegistryEntry; import net.minecraftforge.registries.IForgeRegistry; diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/VanillaRegistryRemapsTest.java b/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemapsTest.java similarity index 73% rename from src/test/java/zone/moddev/mc/poweradvantage/migration/VanillaRegistryRemapsTest.java rename to src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemapsTest.java index c064ad30..90991ef3 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/VanillaRegistryRemapsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemapsTest.java @@ -1,10 +1,20 @@ -package zone.moddev.mc.poweradvantage.migration; +package zone.moddev.mc.basepower.migration; -import net.minecraft.util.ResourceLocation; +import net.minecraft.resources.ResourceLocation; import org.junit.Test; import static org.junit.Assert.*; public class VanillaRegistryRemapsTest { + @Test public void retainsBothGrassPathRegistrySnapshots() { + expect("block", "grass_path", "dirt_path"); + expect("item", "grass_path", "dirt_path"); + assertNull(VanillaRegistryRemaps.replacement(id("entity_type"), id("grass_path"))); + } + + @Test public void retainsTheRenamedBerryPickingSound() { + expect("sound_event", "item.sweet_berries.pick_from_bush", "block.sweet_berry_bush.pick_berries"); + assertNull(VanillaRegistryRemaps.replacement(id("item"), id("item.sweet_berries.pick_from_bush"))); + } @Test public void retainsTheRenamedPigmanEntity() { expect("entity_type", "zombie_pigman", "zombified_piglin"); } diff --git a/src/test/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPackTest.java b/src/test/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPackTest.java new file mode 100644 index 00000000..74be58b5 --- /dev/null +++ b/src/test/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPackTest.java @@ -0,0 +1,59 @@ +package zone.moddev.mc.basepower.recipe; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import org.junit.Test; +import static org.junit.Assert.*; + +public class MaterialPolicyPackTest { + @Test public void enabledPolicyAddsOnlyTheHistoricalAliases() throws Exception { + MaterialPolicyPack pack = new MaterialPolicyPack(true, true); + for (String path : new String[] {"rubber", "ingots/rubber"}) { + JsonObject tag = read(pack, path); + assertFalse(tag.get("replace").getAsBoolean()); + assertEquals(1, tag.getAsJsonArray("values").size()); + assertEquals("basepower:bioplastic_ingot", tag.getAsJsonArray("values").get(0).getAsString()); + } + JsonObject solder = read(pack, "solder"); + assertEquals(3, solder.getAsJsonArray("values").size()); + int index = 0; + for (String metal : new String[] {"lead", "tin", "silver"}) { + JsonObject entry = solder.getAsJsonArray("values").get(index++).getAsJsonObject(); + assertEquals("#forge:ingots/" + metal, entry.get("id").getAsString()); + assertFalse(entry.get("required").getAsBoolean()); + } + } + + @Test public void disabledPolicyDoesNotReplacePlayerTagsOrSupplyAliases() throws Exception { + MaterialPolicyPack pack = new MaterialPolicyPack(false, false); + for (String path : new String[] {"rubber", "ingots/rubber", "solder"}) { + JsonObject tag = read(pack, path); + assertFalse(tag.get("replace").getAsBoolean()); + assertEquals(0, tag.getAsJsonArray("values").size()); + } + } + + @Test public void policyOnlySuppliesServerDataAndHonoursResourceDiscovery() { + MaterialPolicyPack pack = new MaterialPolicyPack(true, false); + ResourceLocation rubber = new ResourceLocation("forge", "tags/items/rubber.json"); + assertTrue(pack.hasResource(PackType.SERVER_DATA, rubber)); + assertFalse(pack.hasResource(PackType.CLIENT_RESOURCES, rubber)); + assertEquals(3, pack.getResources(PackType.SERVER_DATA, "forge", "tags/items", 99, + name -> name.endsWith(".json")).size()); + assertTrue(pack.getResources(PackType.SERVER_DATA, "basepower", "tags/items", 99, + name -> true).isEmpty()); + assertTrue(pack.getResources(PackType.CLIENT_RESOURCES, "forge", "tags/items", 99, + name -> true).isEmpty()); + } + + private static JsonObject read(MaterialPolicyPack pack, String path) throws Exception { + try (InputStream stream = pack.getResource(PackType.SERVER_DATA, + new ResourceLocation("forge", "tags/items/" + path + ".json"))) { + return new JsonParser().parse(new String(stream.readAllBytes(), StandardCharsets.UTF_8)).getAsJsonObject(); + } + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/recipe/MaterialTagsTest.java b/src/test/java/zone/moddev/mc/basepower/recipe/MaterialTagsTest.java similarity index 70% rename from src/test/java/zone/moddev/mc/poweradvantage/recipe/MaterialTagsTest.java rename to src/test/java/zone/moddev/mc/basepower/recipe/MaterialTagsTest.java index 36964876..767638bb 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/recipe/MaterialTagsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/recipe/MaterialTagsTest.java @@ -1,27 +1,27 @@ -package zone.moddev.mc.poweradvantage.recipe; +package zone.moddev.mc.basepower.recipe; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; -import net.minecraft.item.Item; -import net.minecraft.item.Items; -import net.minecraft.tags.ITag; -import net.minecraft.tags.ITagCollection; -import net.minecraft.util.ResourceLocation; +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 org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.*; public class MaterialTagsTest { - @BeforeClass public static void bootstrap() { net.minecraft.util.registry.Bootstrap.bootStrap(); } + @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); } @Test public void updatesNativeImmutableCollectionWithoutLosingUserEntries() { - ITagCollection tags = fixture(); + 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) { } - ITagCollection original = tags; + 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)); @@ -36,23 +36,23 @@ public class MaterialTagsTest { } @Test public void disabledOptionsDoNotAddAliases() { - ITagCollection tags = fixture(); + 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)); } - private static ITag tag(ITagCollection tags, String name) { return tags.getTagOrEmpty(new ResourceLocation(name)); } + private static Tag tag(TagCollection tags, String name) { return tags.getTagOrEmpty(new ResourceLocation(name)); } - private static ITagCollection fixture() { - Map> tags = new LinkedHashMap<>(); + 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 ITagCollection.of(tags); + return TagCollection.of(tags); } - private static ITag singleton(Item item) { return ITag.fromSet(java.util.Collections.singleton(item)); } + private static Tag singleton(Item item) { return Tag.fromSet(java.util.Collections.singleton(item)); } } diff --git a/src/test/java/zone/moddev/mc/poweradvantage/recipe/PartsLootTest.java b/src/test/java/zone/moddev/mc/basepower/recipe/PartsLootTest.java similarity index 60% rename from src/test/java/zone/moddev/mc/poweradvantage/recipe/PartsLootTest.java rename to src/test/java/zone/moddev/mc/basepower/recipe/PartsLootTest.java index fac61954..b83c8057 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/recipe/PartsLootTest.java +++ b/src/test/java/zone/moddev/mc/basepower/recipe/PartsLootTest.java @@ -1,7 +1,7 @@ -package zone.moddev.mc.poweradvantage.recipe; +package zone.moddev.mc.basepower.recipe; -import net.minecraft.util.ResourceLocation; -import net.minecraft.loot.LootTable; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.storage.loot.LootTable; import org.junit.Test; import static org.junit.Assert.*; @@ -10,31 +10,31 @@ public class PartsLootTest { LootTable table = LootTable.lootTable().build(); ResourceLocation chest = new ResourceLocation("minecraft", "chests/simple_dungeon"); PartsLoot.addPools(table, chest, true, mod -> true); - Object original = table.getPool("steamadvantage:legacy_parts"); + Object original = table.getPool("basesteam:legacy_parts"); assertNotNull(original); PartsLoot.addPools(table, chest, true, mod -> true); - assertSame(original, table.getPool("steamadvantage:legacy_parts")); - assertNotNull(table.getPool("poweradvantage:legacy_parts")); - assertNotNull(table.getPool("electricadvantage:legacy_parts")); + assertSame(original, table.getPool("basesteam:legacy_parts")); + assertNotNull(table.getPool("basepower:legacy_parts")); + assertNotNull(table.getPool("baseelectric:legacy_parts")); } @Test public void keepsEndCityExceptionAndAbsentMods() { LootTable table = LootTable.lootTable().build(); PartsLoot.addPools(table, new ResourceLocation("minecraft", "chests/end_city_treasure"), true, - mod -> !mod.equals("steamadvantage")); - assertNull(table.getPool("poweradvantage:legacy_parts")); - assertNull(table.getPool("steamadvantage:legacy_parts")); - assertNotNull(table.getPool("electricadvantage:legacy_parts")); + mod -> !mod.equals("basesteam")); + assertNull(table.getPool("basepower:legacy_parts")); + assertNull(table.getPool("basesteam:legacy_parts")); + assertNotNull(table.getPool("baseelectric:legacy_parts")); } @Test public void leavesUnrelatedTablesAndDisabledLootUnchanged() { for (String id : new String[] {"minecraft:chests/desert_pyramid", "othermod:chests/simple_dungeon"}) { LootTable table = LootTable.lootTable().build(); PartsLoot.addPools(table, new ResourceLocation(id), true, mod -> true); - assertNull(table.getPool("poweradvantage:legacy_parts")); + assertNull(table.getPool("basepower:legacy_parts")); } LootTable table = LootTable.lootTable().build(); PartsLoot.addPools(table, new ResourceLocation("minecraft", "chests/simple_dungeon"), false, mod -> true); - assertNull(table.getPool("poweradvantage:legacy_parts")); + assertNull(table.getPool("basepower:legacy_parts")); } } diff --git a/src/test/java/zone/moddev/mc/basepower/registry/MachineGuiTitleTest.java b/src/test/java/zone/moddev/mc/basepower/registry/MachineGuiTitleTest.java new file mode 100644 index 00000000..8f403e25 --- /dev/null +++ b/src/test/java/zone/moddev/mc/basepower/registry/MachineGuiTitleTest.java @@ -0,0 +1,33 @@ +package zone.moddev.mc.basepower.registry; + +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.TranslatableComponent; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +public class MachineGuiTitleTest { + @BeforeClass public static void bootstrap() { + net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); + } + + @Test public void defaultTitleUsesTheBlockTranslationInsteadOfAnInternalInventoryName() { + assertEquals("block.minecraft.chest", ((TranslatableComponent) MachineGUIRegistry.menuTitle( + new ChestBlockEntity(net.minecraft.core.BlockPos.ZERO, Blocks.CHEST.defaultBlockState()), Blocks.CHEST)).getKey()); + } + + @Test public void renamedMachinesKeepThePlayersFullName() { + ChestBlockEntity tile = new ChestBlockEntity(net.minecraft.core.BlockPos.ZERO, Blocks.CHEST.defaultBlockState()); + tile.setCustomName(new TextComponent("John's oil tank")); + assertEquals("John's oil tank", MachineGUIRegistry.menuTitle(tile, Blocks.CHEST).getString()); + } + + @Test public void unnamedTilesWithoutInventoryNamesStillHaveATranslatableTitle() { + BlockEntity tile = new BlockEntity(BlockEntityType.CHEST, net.minecraft.core.BlockPos.ZERO, Blocks.CHEST.defaultBlockState()) {}; + assertEquals("block.minecraft.chest", ((TranslatableComponent) MachineGUIRegistry.menuTitle(tile, Blocks.CHEST)).getKey()); + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/BuildWorkflowContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java similarity index 87% rename from src/test/java/zone/moddev/mc/poweradvantage/resources/BuildWorkflowContractTest.java rename to src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java index 7352aff0..87a359b8 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/BuildWorkflowContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.resources; +package zone.moddev.mc.basepower.resources; import org.junit.Test; @@ -33,13 +33,14 @@ public class BuildWorkflowContractTest { 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: 995f6cc4dd2d5eecc628a42e5c43fa5390c24bca")); - assertTrue(name, workflow.contains("BaseMetals-3.1.1.116051-deobf.jar")); - assertTrue(name, workflow.contains("ref: a113e5d767169189f0e35ade1e173a12d550b00e")); - assertTrue(name, workflow.contains("master-1.16.5")); + 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("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'")); 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/poweradvantage/resources/ClientResourceContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/ClientResourceContractTest.java similarity index 92% rename from src/test/java/zone/moddev/mc/poweradvantage/resources/ClientResourceContractTest.java rename to src/test/java/zone/moddev/mc/basepower/resources/ClientResourceContractTest.java index 64247224..7a836406 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/ClientResourceContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/ClientResourceContractTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.resources; +package zone.moddev.mc.basepower.resources; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -20,7 +20,7 @@ public class ClientResourceContractTest { private static final Path ROOT = Paths.get("src/main/resources"); - private static final Path ASSETS = ROOT.resolve("assets/poweradvantage"); + private static final Path ASSETS = ROOT.resolve("assets/basepower"); @Test public void blockstatesResolveLiteralModelPaths() throws IOException { for (Path file : jsonFiles(ASSETS.resolve("blockstates"))) { @@ -62,7 +62,7 @@ public class ClientResourceContractTest { JsonObject locale = read(file); for (Map.Entry entry : locale.entrySet()) { String key = entry.getKey(); - if ((key.startsWith("tile.poweradvantage.") || key.startsWith("item.poweradvantage.")) + if ((key.startsWith("tile.basepower.") || key.startsWith("item.basepower.")) && key.endsWith(".name")) { String modern = key.substring(0, key.length() - ".name".length()); if (modern.startsWith("tile.")) modern = "block." + modern.substring("tile.".length()); @@ -76,10 +76,10 @@ public class ClientResourceContractTest { JsonObject locale = read(ASSETS.resolve("lang/en_us.json")); for (Path file : jsonFiles(ASSETS.resolve("blockstates"))) { String id = file.getFileName().toString().replace(".json", ""); - assertTrue("Missing block name: " + id, locale.has("block.poweradvantage." + id)); + assertTrue("Missing block name: " + id, locale.has("block.basepower." + id)); } for (String id : new String[] {"crude_oil", "refined_oil"}) { - assertEquals(locale.get("item.bucket." + id), locale.get("item.poweradvantage." + id + "_bucket")); + assertEquals(locale.get("item.bucket." + id), locale.get("item.basepower." + id + "_bucket")); } } @@ -101,7 +101,7 @@ private static void verifyReference(String reference, String directory, String s String path = parts.length == 2 ? parts[1] : parts[0]; if (directory.equals("models") && namespace.equals("minecraft") && path.startsWith("builtin/")) return; String resource = "assets/" + namespace + "/" + directory + "/" + path + suffix; - boolean found = namespace.equals("poweradvantage") ? Files.isRegularFile(ROOT.resolve(resource)) + boolean found = namespace.equals("basepower") ? Files.isRegularFile(ROOT.resolve(resource)) : ClientResourceContractTest.class.getClassLoader().getResource(resource) != null; assertTrue(file + " references missing resource " + resource, found); } diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/ConverterEnergyDirectionTest.java b/src/test/java/zone/moddev/mc/basepower/resources/ConverterEnergyDirectionTest.java similarity index 97% rename from src/test/java/zone/moddev/mc/poweradvantage/resources/ConverterEnergyDirectionTest.java rename to src/test/java/zone/moddev/mc/basepower/resources/ConverterEnergyDirectionTest.java index 5739ba8e..67c985cf 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/ConverterEnergyDirectionTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/ConverterEnergyDirectionTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.resources; +package zone.moddev.mc.basepower.resources; import java.io.InputStream; import org.junit.Test; diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/CreativeTabInitializationTest.java b/src/test/java/zone/moddev/mc/basepower/resources/CreativeTabInitializationTest.java similarity index 80% rename from src/test/java/zone/moddev/mc/poweradvantage/resources/CreativeTabInitializationTest.java rename to src/test/java/zone/moddev/mc/basepower/resources/CreativeTabInitializationTest.java index b9609164..f23a617d 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/CreativeTabInitializationTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/CreativeTabInitializationTest.java @@ -1,8 +1,8 @@ -package zone.moddev.mc.poweradvantage.resources; +package zone.moddev.mc.basepower.resources; import java.lang.reflect.Modifier; import org.junit.Test; -import zone.moddev.mc.poweradvantage.init.ItemGroups; +import zone.moddev.mc.basepower.init.ItemGroups; import static org.junit.Assert.assertTrue; public class CreativeTabInitializationTest { diff --git a/src/test/java/zone/moddev/mc/basepower/resources/NativeBlockEntityFactoryTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativeBlockEntityFactoryTest.java new file mode 100644 index 00000000..2f4dffde --- /dev/null +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativeBlockEntityFactoryTest.java @@ -0,0 +1,67 @@ +package zone.moddev.mc.basepower.resources; + +import cyano.poweradvantage.api.GUIBlock; +import java.io.InputStream; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Material; +import org.junit.BeforeClass; +import org.junit.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import zone.moddev.mc.basepower.init.Entities; +import zone.moddev.mc.basepower.machines.conveyors.BlockConveyor; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityConveyor; +import static org.junit.Assert.*; + +public class NativeBlockEntityFactoryTest { + @BeforeClass public static void bootstrap() { + net.minecraft.SharedConstants.tryDetectVersion(); + net.minecraft.server.Bootstrap.bootStrap(); + } + + @Test public void nativeFactoriesKeepTheActualPositionAndFacing() { + BlockConveyor block = new BlockConveyor(Material.PISTON, 0.75F); + BlockEntityType old = Entities.CONVEYOR; + try { + Entities.CONVEYOR = BlockEntityType.Builder.of(TileEntityConveyor::new, block).build(null); + BlockPos position = new BlockPos(37, 83, -41); + BlockState state = block.defaultBlockState().setValue(BlockConveyor.FACING, net.minecraft.core.Direction.UP); + TileEntityConveyor placed = (TileEntityConveyor)block.newBlockEntity(position, state); + TileEntityConveyor restored = Entities.CONVEYOR.create(position, state); + for (TileEntityConveyor tile : new TileEntityConveyor[] {placed, restored}) { + assertEquals(position, tile.getBlockPos()); + assertSame(state, tile.getBlockState()); + assertSame(Entities.CONVEYOR, tile.getType()); + assertNotNull(block.getTicker(null, state, Entities.CONVEYOR)); + net.minecraft.nbt.CompoundTag saved = new net.minecraft.nbt.CompoundTag(); + saved.putInt("x", 0); + tile.load(saved); + assertEquals("NBT cannot replace the native factory's final position", position, tile.getBlockPos()); + } + assertNull(block.getTicker(null, state, BlockEntityType.CHEST)); + } finally { Entities.CONVEYOR = old; } + } + + @Test public void nativeTickersActuallyDispatchBothMachineFamilies() throws Exception { + ClassNode node = new ClassNode(); + try (InputStream input = GUIBlock.class.getResourceAsStream("/cyano/poweradvantage/api/GUIBlock.class")) { + assertNotNull(input); + new ClassReader(input).accept(node, 0); + } + int powered = 0, conveyors = 0; + for (MethodNode method : node.methods) for (AbstractInsnNode instruction : method.instructions.toArray()) { + if (!(instruction instanceof MethodInsnNode)) continue; + MethodInsnNode call = (MethodInsnNode)instruction; + if (!call.name.equals("tick") || !call.desc.equals("()V")) continue; + if (call.owner.equals("cyano/poweradvantage/api/PoweredEntity")) powered++; + if (call.owner.equals("zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyor")) conveyors++; + } + assertEquals(1, powered); + assertEquals(1, conveyors); + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/NativeLootContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativeLootContractTest.java similarity index 76% rename from src/test/java/zone/moddev/mc/poweradvantage/resources/NativeLootContractTest.java rename to src/test/java/zone/moddev/mc/basepower/resources/NativeLootContractTest.java index d69b782f..3c52aa1f 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/NativeLootContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativeLootContractTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.resources; +package zone.moddev.mc.basepower.resources; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -13,10 +13,10 @@ public class NativeLootContractTest { private static final Path ROOT = Paths.get("src/main/resources"); @Test public void everyRegisteredBlockHasANativeLootTable() throws Exception { - try (java.util.stream.Stream states = Files.list(ROOT.resolve("assets/poweradvantage/blockstates"))) { + try (java.util.stream.Stream states = Files.list(ROOT.resolve("assets/basepower/blockstates"))) { for (Path file : (Iterable) states.filter(p -> p.toString().endsWith(".json"))::iterator) { String id = file.getFileName().toString(); - JsonObject table = read(ROOT.resolve("data/poweradvantage/loot_tables/blocks").resolve(id)); + JsonObject table = read(ROOT.resolve("data/basepower/loot_tables/blocks").resolve(id)); assertEquals(id, "minecraft:block", table.get("type").getAsString()); if (id.equals("crude_oil.json") || id.equals("refined_oil.json")) assertEquals(0, table.getAsJsonArray("pools").size()); else assertEquals(id, 1, table.getAsJsonArray("pools").size()); @@ -25,12 +25,12 @@ public class NativeLootContractTest { } @Test public void portableTankCopiesTheExistingItemKeysAndTerminalReturnsAPlainPipe() throws Exception { - JsonObject table = read(ROOT.resolve("data/poweradvantage/loot_tables/blocks/fluid_storage_tank.json")); + JsonObject table = read(ROOT.resolve("data/basepower/loot_tables/blocks/fluid_storage_tank.json")); String serialized = table.toString(); assertTrue(serialized.contains("\"source\":\"Tank.FluidName\",\"target\":\"FluidID\"")); assertTrue(serialized.contains("\"source\":\"Tank.Amount\",\"target\":\"Volume\"")); - JsonObject pipe = read(ROOT.resolve("data/poweradvantage/loot_tables/blocks/fluid_pipe_terminal.json")); - assertEquals("poweradvantage:fluid_pipe", pipe.getAsJsonArray("pools").get(0).getAsJsonObject() + JsonObject pipe = read(ROOT.resolve("data/basepower/loot_tables/blocks/fluid_pipe_terminal.json")); + assertEquals("basepower:fluid_pipe", pipe.getAsJsonArray("pools").get(0).getAsJsonObject() .getAsJsonArray("entries").get(0).getAsJsonObject().get("name").getAsString()); } diff --git a/src/test/java/zone/moddev/mc/basepower/resources/NativeMachineDirtyStateTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativeMachineDirtyStateTest.java new file mode 100644 index 00000000..cfd0530b --- /dev/null +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativeMachineDirtyStateTest.java @@ -0,0 +1,58 @@ +package zone.moddev.mc.basepower.resources; + +import static org.junit.Assert.*; +import java.io.InputStream; +import org.junit.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +/** Wiring guard; stopped-region regression tests prove actual machine persistence. */ +public class NativeMachineDirtyStateTest { + private ClassNode production() throws Exception { + ClassNode node = new ClassNode(); + try (InputStream input = getClass().getResourceAsStream("/cyano/poweradvantage/api/PoweredEntity.class")) { + assertNotNull(input); + new ClassReader(input).accept(node, 0); + } + return node; + } + + private int calls(MethodNode method, String owner, String name) { + int count = 0; + for (AbstractInsnNode instruction : method.instructions.toArray()) { + if (instruction instanceof MethodInsnNode) { + MethodInsnNode call = (MethodInsnNode) instruction; + if (call.owner.equals(owner) && call.name.equals(name)) count++; + } + } + return count; + } + + @Test public void timersAndExternalEnergyTransfersInvalidateTheSavedChunk() throws Exception { + int tick = 0, transfers = 0; + for (MethodNode method : production().methods) { + int marks = calls(method, "cyano/poweradvantage/api/PoweredEntity", "markMachineStateUnsaved"); + if (method.name.equals("tick")) tick += marks; + if (method.name.equals("addEnergy")) transfers += marks; + } + assertEquals("A native machine tick must retain progress even without a GUI sync", 1, tick); + assertEquals("All three energy-transfer return paths must retain the changed buffer", 3, transfers); + } + + @Test public void dirtyMarkUsesOnlyAnAlreadyLoadedChunkWithoutComparatorNotifications() throws Exception { + MethodNode mark = null; + for (MethodNode method : production().methods) if (method.name.equals("markMachineStateUnsaved")) mark = method; + assertNotNull("Missing native save invalidation", mark); + assertEquals(1, calls(mark, "net/minecraft/server/level/ServerChunkCache", "getChunkNow")); + assertEquals(1, calls(mark, "net/minecraft/world/level/chunk/LevelChunk", "setUnsaved")); + for (AbstractInsnNode instruction : mark.instructions.toArray()) if (instruction instanceof MethodInsnNode) { + String name = ((MethodInsnNode) instruction).name; + assertFalse("Dirty marking must not load chunks or send comparator/client updates: " + name, + name.equals("getChunk") || name.equals("getChunkAt") || name.equals("setChanged") + || name.equals("sendBlockUpdated") || name.equals("updateNeighbourForOutputSignal")); + } + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/NativeMachinePropertyTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativeMachinePropertyTest.java similarity index 73% rename from src/test/java/zone/moddev/mc/poweradvantage/resources/NativeMachinePropertyTest.java rename to src/test/java/zone/moddev/mc/basepower/resources/NativeMachinePropertyTest.java index 1b916bf5..941ffe0e 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/NativeMachinePropertyTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativeMachinePropertyTest.java @@ -1,18 +1,18 @@ -package zone.moddev.mc.poweradvantage.resources; +package zone.moddev.mc.basepower.resources; import cyano.poweradvantage.api.GUIBlock; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.simple.BlockSimplePowerConduit; -import net.minecraft.block.Block; -import net.minecraft.block.material.Material; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.registry.Bootstrap; -import net.minecraft.world.IBlockReader; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.material.Material; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.BlockGetter; import org.junit.BeforeClass; import org.junit.Test; -import zone.moddev.mc.poweradvantage.machines.conveyors.BlockConveyor; -import zone.moddev.mc.poweradvantage.machines.conveyors.BlockConveyorFilter; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityConveyor; +import zone.moddev.mc.basepower.machines.conveyors.BlockConveyor; +import zone.moddev.mc.basepower.machines.conveyors.BlockConveyorFilter; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityConveyor; import static org.junit.Assert.*; public class NativeMachinePropertyTest { @@ -36,7 +36,7 @@ public class NativeMachinePropertyTest { private static final class MaterialMachine extends GUIBlock { MaterialMachine() { super(Material.PISTON, 0.75F); } - @Override public TileEntity newBlockEntity(IBlockReader world) { return null; } + @Override public BlockEntity newBlockEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { return null; } } @Test public void conduitSubclassesCanChooseNativePropertiesWithoutReplacingNetworkBehavior() { @@ -50,7 +50,7 @@ private static final class MaterialMachine extends GUIBlock { private static final class PropertyMachine extends GUIBlock { PropertyMachine(Block.Properties properties) { super(properties); } - @Override public TileEntity newBlockEntity(IBlockReader world) { return null; } + @Override public BlockEntity newBlockEntity(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state) { return null; } } private static final class PropertyConduit extends BlockSimplePowerConduit { diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/NativePlatformContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativePlatformContractTest.java similarity index 84% rename from src/test/java/zone/moddev/mc/poweradvantage/resources/NativePlatformContractTest.java rename to src/test/java/zone/moddev/mc/basepower/resources/NativePlatformContractTest.java index cedbf671..e5ac92f8 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/NativePlatformContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativePlatformContractTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.resources; +package zone.moddev.mc.basepower.resources; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -26,18 +26,18 @@ public class NativePlatformContractTest { } @Test public void pinnedForgeFiresTagEventAfterBindingNativeCollections() throws Exception { - ClassNode node = read("net/minecraft/tags/ITagCollectionSupplier"); + ClassNode node = read("net/minecraft/tags/TagContainer"); 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/TagRegistryManager") + if (call.owner.equals("net/minecraft/tags/StaticTags") && call.name.equals("resetAll")) installed = true; } if (instruction instanceof TypeInsnNode - && ((TypeInsnNode) instruction).desc.equals("net/minecraftforge/event/TagsUpdatedEvent$VanillaTagTypes")) { + && ((TypeInsnNode) instruction).desc.equals("net/minecraftforge/event/TagsUpdatedEvent")) { assertTrue("Bind tags before notifying observers", installed); events++; } @@ -47,16 +47,16 @@ public class NativePlatformContractTest { } @Test public void nativeLootLoaderCallsForgeJsonHookAndNotTheRemovedAdvantageBridge() throws Exception { - ClassNode node = read("net/minecraft/loot/LootTableManager"); + ClassNode node = read("net/minecraft/world/level/storage/loot/LootTables"); int calls = 0; for (MethodNode method : node.methods) for (AbstractInsnNode instruction : method.instructions.toArray()) { if (!(instruction instanceof MethodInsnNode)) continue; MethodInsnNode call = (MethodInsnNode) instruction; - assertNotEquals("zone/moddev/mc/poweradvantage/recipe/PartsLoot", call.owner); + assertNotEquals("zone/moddev/mc/basepower/recipe/PartsLoot", call.owner); if (call.owner.equals("net/minecraftforge/common/ForgeHooks") && call.name.equals("loadLootTable")) { - assertEquals("(Lcom/google/gson/Gson;Lnet/minecraft/util/ResourceLocation;" - + "Lcom/google/gson/JsonElement;ZLnet/minecraft/loot/LootTableManager;)" - + "Lnet/minecraft/loot/LootTable;", call.desc); + assertEquals("(Lcom/google/gson/Gson;Lnet/minecraft/resources/ResourceLocation;" + + "Lcom/google/gson/JsonElement;ZLnet/minecraft/world/level/storage/loot/LootTables;)" + + "Lnet/minecraft/world/level/storage/loot/LootTable;", call.desc); calls++; } } diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/NativeTagReaderContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativeTagReaderContractTest.java similarity index 58% rename from src/test/java/zone/moddev/mc/poweradvantage/resources/NativeTagReaderContractTest.java rename to src/test/java/zone/moddev/mc/basepower/resources/NativeTagReaderContractTest.java index b1d682a3..1118ef2a 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/NativeTagReaderContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativeTagReaderContractTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.resources; +package zone.moddev.mc.basepower.resources; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -6,10 +6,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; -import net.minecraft.item.Item; -import net.minecraft.item.Items; -import net.minecraft.tags.ITag; -import net.minecraft.util.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import net.minecraft.tags.Tag; +import net.minecraft.resources.ResourceLocation; import org.junit.BeforeClass; import org.junit.Test; import org.objectweb.asm.ClassReader; @@ -20,40 +20,36 @@ import static org.junit.Assert.*; public class NativeTagReaderContractTest { - @BeforeClass public static void bootstrap() { net.minecraft.util.registry.Bootstrap.bootStrap(); } + @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); } @Test public void optionalSteelBarsUseTheNativeParserWithAndWithoutBaseMetals() throws Exception { JsonObject json = new JsonParser().parse(new String(Files.readAllBytes(Paths.get( "src/main/resources/data/forge/tags/items/bars/steel.json")), StandardCharsets.UTF_8)).getAsJsonObject(); assertFalse("Do not silently ignore an obsolete optional array", json.has("optional")); - ITag.Builder builder = ITag.Builder.tag().addFromJson(json, "test"); - ITag absent = builder.build(id -> null, id -> null).get(); + Tag.Builder builder = Tag.Builder.tag().addFromJson(json, "test"); + Tag absent = builder.build(id -> null, id -> null).right().get(); assertTrue(absent.getValues().isEmpty()); - ITag present = builder.build(id -> null, id -> - id.equals(new ResourceLocation("basemetals:steel_bars")) ? Items.IRON_BARS : null).get(); + Tag present = builder.build(id -> null, id -> + id.equals(new ResourceLocation("basemetals:steel_bars")) ? Items.IRON_BARS : null).right().get(); assertEquals(java.util.Collections.singletonList(Items.IRON_BARS), present.getValues()); } - @Test public void configuredAliasesAreAppliedInsideTheNativeReaderWithoutGlobalMutation() throws Exception { + @Test public void configuredAliasesUseANativeDataPackWithoutGlobalTagMutation() throws Exception { ClassNode node = new ClassNode(); try (InputStream input = getClass().getResourceAsStream( - "/zone/moddev/mc/poweradvantage/recipe/MaterialTags$1.class")) { + "/zone/moddev/mc/basepower/recipe/MaterialTags.class")) { assertNotNull(input); new ClassReader(input).accept(node, 0); } int policies = 0; for (MethodNode method : node.methods) { - boolean nativeLoaded = false; for (AbstractInsnNode instruction : method.instructions.toArray()) { if (!(instruction instanceof MethodInsnNode)) continue; MethodInsnNode call = (MethodInsnNode) instruction; - assertNotEquals("net/minecraft/tags/TagCollectionManager", call.owner); - assertNotEquals("net/minecraft/tags/TagRegistryManager", call.owner); - if (call.owner.equals("net/minecraft/tags/TagCollectionReader") && call.name.equals("load")) - nativeLoaded = true; - if (call.owner.equals("zone/moddev/mc/poweradvantage/recipe/MaterialTags") - && call.name.equals("applyPolicy")) { - assertTrue("Preserve and resolve data-pack entries before adding aliases", nativeLoaded); + assertNotEquals("net/minecraft/tags/SerializationTags", call.owner); + assertNotEquals("net/minecraft/tags/StaticTags", call.owner); + if (call.owner.equals("net/minecraft/server/packs/repository/Pack") + && call.name.equals("create")) { policies++; } } diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/PersistenceRegistrationThreadTest.java b/src/test/java/zone/moddev/mc/basepower/resources/PersistenceRegistrationThreadTest.java similarity index 92% rename from src/test/java/zone/moddev/mc/poweradvantage/resources/PersistenceRegistrationThreadTest.java rename to src/test/java/zone/moddev/mc/basepower/resources/PersistenceRegistrationThreadTest.java index 11537adb..b32e9f5a 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/PersistenceRegistrationThreadTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/PersistenceRegistrationThreadTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.resources; +package zone.moddev.mc.basepower.resources; import java.io.InputStream; import org.junit.Test; @@ -14,11 +14,11 @@ public class PersistenceRegistrationThreadTest { @Test public void sharedForgeHookListIsOnlyUpdatedThroughQueuedSetup() throws Exception { ClassNode node = new ClassNode(); - try (InputStream input = getClass().getResourceAsStream("/zone/moddev/mc/poweradvantage/PowerAdvantage.class")) { + try (InputStream input = getClass().getResourceAsStream("/zone/moddev/mc/basepower/PowerAdvantage.class")) { assertNotNull(input); new ClassReader(input).accept(node, 0); } - String hook = "zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook"; + String hook = "zone/moddev/mc/basepower/migration/LegacyWorldDataHook"; int queuedRegistrations = 0; for (MethodNode method : node.methods) { boolean hookRunnable = false; diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/RecipeRegistrationThreadTest.java b/src/test/java/zone/moddev/mc/basepower/resources/RecipeRegistrationThreadTest.java similarity index 92% rename from src/test/java/zone/moddev/mc/poweradvantage/resources/RecipeRegistrationThreadTest.java rename to src/test/java/zone/moddev/mc/basepower/resources/RecipeRegistrationThreadTest.java index 1b1a7f12..2e727047 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/RecipeRegistrationThreadTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/RecipeRegistrationThreadTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.resources; +package zone.moddev.mc.basepower.resources; import java.io.InputStream; import org.junit.Test; @@ -14,11 +14,11 @@ public class RecipeRegistrationThreadTest { @Test public void sharedForgeConditionsAreRegisteredAfterParallelSetup() throws Exception { ClassNode node = new ClassNode(); - try (InputStream input = getClass().getResourceAsStream("/zone/moddev/mc/poweradvantage/PowerAdvantage.class")) { + try (InputStream input = getClass().getResourceAsStream("/zone/moddev/mc/basepower/PowerAdvantage.class")) { assertNotNull(input); new ClassReader(input).accept(node, 0); } - String conditions = "zone/moddev/mc/poweradvantage/recipe/RecipeConditions"; + String conditions = "zone/moddev/mc/basepower/recipe/RecipeConditions"; int queued = 0; for (MethodNode method : node.methods) { boolean registrationRunnable = false; diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/RecipeUnlockContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/RecipeUnlockContractTest.java similarity index 80% rename from src/test/java/zone/moddev/mc/poweradvantage/resources/RecipeUnlockContractTest.java rename to src/test/java/zone/moddev/mc/basepower/resources/RecipeUnlockContractTest.java index 6c026f38..5106191c 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/RecipeUnlockContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/RecipeUnlockContractTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.resources; +package zone.moddev.mc.basepower.resources; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -21,7 +21,7 @@ import static org.junit.Assert.*; public class RecipeUnlockContractTest { - private static final String MOD = "poweradvantage"; + private static final String MOD = "basepower"; private static final Path DATA = Paths.get("src/main/resources/data", MOD); private static final Set IGNORED = new LinkedHashSet<>(java.util.Arrays.asList( "item:minecraft:stick", "item:minecraft:string", "item:minecraft:feather", "tag:forge:rods/wooden")); @@ -54,7 +54,7 @@ public class RecipeUnlockContractTest { assertEquals("Finding ONE ingredient must be enough: " + file, 1, items.size()); JsonObject predicate = items.get(0).getAsJsonObject(); assertEquals("Only an item or tag predicate is expected", 1, predicate.size()); - actual.add(predicateKey(predicate)); + actual.add(unlockPredicateKey(predicate)); } List choices = new ArrayList<>(); if (recipe.has("key")) { @@ -111,6 +111,28 @@ private static String predicateKey(JsonObject predicate) { return type + ":" + predicate.get(type).getAsString(); } + private static String unlockPredicateKey(JsonObject predicate) { + assertEquals("One explicit native ingredient predicate is required", 1, predicate.size()); + if (predicate.has("tag")) return "tag:" + predicate.get("tag").getAsString(); + assertTrue("Minecraft 1.17 ignores the old singular item advancement field", predicate.has("items")); + JsonArray items = predicate.getAsJsonArray("items"); + assertEquals("Each criterion discovers exactly one recipe ingredient", 1, items.size()); + return "item:" + items.get(0).getAsString(); + } + + @Test public void nativeParserDoesNotUnlockStarchForAnUnrelatedItem() throws IOException { + net.minecraft.SharedConstants.tryDetectVersion(); + net.minecraft.server.Bootstrap.bootStrap(); + JsonObject criterion = read(DATA.resolve("advancements/recipes/starch_from_potato.json")) + .getAsJsonObject("criteria").getAsJsonObject("has_ingredient_1"); + JsonObject predicate = criterion.getAsJsonObject("conditions").getAsJsonArray("items").get(0).getAsJsonObject(); + net.minecraft.advancements.critereon.ItemPredicate nativePredicate = + net.minecraft.advancements.critereon.ItemPredicate.fromJson(predicate); + assertTrue(nativePredicate.matches(new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.POTATO))); + assertFalse("An unrelated dirt stack must not satisfy the real native predicate", + nativePredicate.matches(new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.DIRT))); + } + private static void collect(JsonElement element, List result) { if (element.isJsonArray()) for (JsonElement child : element.getAsJsonArray()) collect(child, result); else result.add(element.getAsJsonObject()); diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/StarchRecipeTest.java b/src/test/java/zone/moddev/mc/basepower/resources/StarchRecipeTest.java similarity index 82% rename from src/test/java/zone/moddev/mc/poweradvantage/resources/StarchRecipeTest.java rename to src/test/java/zone/moddev/mc/basepower/resources/StarchRecipeTest.java index dab10709..4a6e7691 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/StarchRecipeTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/StarchRecipeTest.java @@ -1,4 +1,4 @@ -package zone.moddev.mc.poweradvantage.resources; +package zone.moddev.mc.basepower.resources; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -19,7 +19,7 @@ public class StarchRecipeTest { JsonArray inputs = recipe.getAsJsonArray("ingredients"); assertEquals(1, inputs.size()); assertEquals(potatoes(), items(inputs.get(0).getAsJsonArray())); - assertEquals("poweradvantage:starch", recipe.getAsJsonObject("result").get("item").getAsString()); + assertEquals("basepower:starch", recipe.getAsJsonObject("result").get("item").getAsString()); assertEquals(1, recipe.getAsJsonObject("result").get("count").getAsInt()); } @@ -27,13 +27,13 @@ public class StarchRecipeTest { JsonObject recipe = read("crushing_potato_starch"); assertEquals("basemetals:crushing", recipe.get("type").getAsString()); assertEquals(potatoes(), items(recipe.getAsJsonArray("ingredient"))); - assertEquals("poweradvantage:starch", recipe.getAsJsonObject("result").get("item").getAsString()); + assertEquals("basepower:starch", recipe.getAsJsonObject("result").get("item").getAsString()); assertEquals(1, recipe.getAsJsonObject("result").get("count").getAsInt()); } private static JsonObject read(String name) throws Exception { return new JsonParser().parse(new String(Files.readAllBytes(Paths.get( - "src/main/resources/data/poweradvantage/recipes/" + name + ".json")), StandardCharsets.UTF_8)).getAsJsonObject(); + "src/main/resources/data/basepower/recipes/" + name + ".json")), StandardCharsets.UTF_8)).getAsJsonObject(); } private static Set potatoes() { return new HashSet<>(java.util.Arrays.asList("minecraft:potato", "minecraft:poisonous_potato")); diff --git a/src/test/java/zone/moddev/mc/poweradvantage/util/ForgeFluidAdapterTest.java b/src/test/java/zone/moddev/mc/basepower/util/ForgeFluidAdapterTest.java similarity index 91% rename from src/test/java/zone/moddev/mc/poweradvantage/util/ForgeFluidAdapterTest.java rename to src/test/java/zone/moddev/mc/basepower/util/ForgeFluidAdapterTest.java index 254ac9fa..fbf679b3 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/util/ForgeFluidAdapterTest.java +++ b/src/test/java/zone/moddev/mc/basepower/util/ForgeFluidAdapterTest.java @@ -1,13 +1,13 @@ -package zone.moddev.mc.poweradvantage.util; +package zone.moddev.mc.basepower.util; import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.FluidTank; import cyano.poweradvantage.api.fluid.FluidTankInfo; import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; -import net.minecraft.fluid.Fluid; -import net.minecraft.fluid.WaterFluid; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.util.Direction; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.WaterFluid; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.core.Direction; import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction; import org.junit.Test; import org.junit.BeforeClass; @@ -17,8 +17,8 @@ public class ForgeFluidAdapterTest { private static Fluid WATER; @BeforeClass public static void bootstrapNativeRegistries() { - net.minecraft.util.registry.Bootstrap.bootStrap(); - WATER = net.minecraft.fluid.Fluids.WATER; + net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); + WATER = net.minecraft.world.level.material.Fluids.WATER; } @Test public void simulatesWithoutChangingTankOrInputThenExecutesExactAmount() { @@ -26,7 +26,7 @@ public class ForgeFluidAdapterTest { int[] changes = {0}; ForgeFluidAdapter adapter = new ForgeFluidAdapter(tank, Direction.NORTH, () -> changes[0]++); net.minecraftforge.fluids.FluidStack offered = new net.minecraftforge.fluids.FluidStack(WATER, 1500); - CompoundNBT tag = new CompoundNBT(); + CompoundTag tag = new CompoundTag(); tag.putString("Owner", "preserve-me"); offered.setTag(tag); assertEquals(1000, adapter.fill(offered, FluidAction.SIMULATE)); @@ -75,7 +75,7 @@ public class ForgeFluidAdapterTest { @Test public void copiesBothDirectionsIncludingNbt() { FluidStack source = new FluidStack(WATER, 700); - source.tag = new CompoundNBT(); + source.tag = new CompoundTag(); source.tag.putInt("batch", 7); net.minecraftforge.fluids.FluidStack nativeStack = ForgeFluidAdapter.toForge(source); FluidStack copy = ForgeFluidAdapter.fromForge(nativeStack); diff --git a/src/test/java/zone/moddev/mc/poweradvantage/util/MachineCapabilitiesLifecycleTest.java b/src/test/java/zone/moddev/mc/basepower/util/MachineCapabilitiesLifecycleTest.java similarity index 73% rename from src/test/java/zone/moddev/mc/poweradvantage/util/MachineCapabilitiesLifecycleTest.java rename to src/test/java/zone/moddev/mc/basepower/util/MachineCapabilitiesLifecycleTest.java index 2da028dc..48145f3d 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/util/MachineCapabilitiesLifecycleTest.java +++ b/src/test/java/zone/moddev/mc/basepower/util/MachineCapabilitiesLifecycleTest.java @@ -1,11 +1,11 @@ -package zone.moddev.mc.poweradvantage.util; +package zone.moddev.mc.basepower.util; import java.lang.reflect.Constructor; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicBoolean; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.INBT; -import net.minecraft.util.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.core.Direction; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.fluids.capability.CapabilityFluidHandler; @@ -17,30 +17,15 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.MetalTankTileEntity; +import zone.moddev.mc.basepower.machines.fluidmachines.MetalTankTileEntity; import static org.junit.Assert.*; public class MachineCapabilitiesLifecycleTest { - private static Capability oldFluid; - private static Capability oldItem; - private static Capability oldEnergy; - @BeforeClass public static void nativeCapabilityTokens() throws Exception { - net.minecraft.util.registry.Bootstrap.bootStrap(); - oldFluid = CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY; - oldItem = CapabilityItemHandler.ITEM_HANDLER_CAPABILITY; - oldEnergy = CapabilityEnergy.ENERGY; - // Forge injects these in a real process. Headless tests install distinct real - // Capability tokens without running a mod loader or changing registrations. - CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY = token(IFluidHandler.class.getName()); - CapabilityItemHandler.ITEM_HANDLER_CAPABILITY = token(IItemHandler.class.getName()); - CapabilityEnergy.ENERGY = token(IEnergyStorage.class.getName()); - } - - @AfterClass public static void restoreTokens() { - CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY = oldFluid; - CapabilityItemHandler.ITEM_HANDLER_CAPABILITY = oldItem; - CapabilityEnergy.ENERGY = oldEnergy; + net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); + assertNotNull(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY); + assertNotNull(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY); + assertNotNull(CapabilityEnergy.ENERGY); } @Test public void chunkUnloadCannotRecreateAdaptersOnAnUnremovedTile() { @@ -70,7 +55,7 @@ public class MachineCapabilitiesLifecycleTest { @Test public void clearRemovedCreatesFreshOptionalsWithoutRevivingOldOnes() { TestTank tank = new TestTank(); - tank.getTank().setFluid(new cyano.poweradvantage.api.fluid.FluidStack(net.minecraft.fluid.Fluids.WATER, 500)); + tank.getTank().setFluid(new cyano.poweradvantage.api.fluid.FluidStack(net.minecraft.world.level.material.Fluids.WATER, 500)); LazyOptional old = tank.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, null); tank.onChunkUnloaded(); tank.clearRemoved(); @@ -106,8 +91,8 @@ public class MachineCapabilitiesLifecycleTest { } @Test public void conveyorsFollowTheSameUnloadAndRevivalContract() { - zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityConveyor conveyor = - new zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityConveyor(); + zone.moddev.mc.basepower.machines.conveyors.TileEntityConveyor conveyor = + new zone.moddev.mc.basepower.machines.conveyors.TileEntityConveyor(net.minecraft.core.BlockPos.ZERO, net.minecraft.world.level.block.Blocks.HOPPER.defaultBlockState()); LazyOptional old = conveyor.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, null); assertTrue(old.isPresent()); conveyor.onChunkUnloaded(); @@ -148,24 +133,14 @@ public class MachineCapabilitiesLifecycleTest { assertFalse(reacquired.get()); } - @SuppressWarnings("unchecked") - private static Capability token(String name) throws Exception { - Constructor constructor = Capability.class.getDeclaredConstructor(String.class, Capability.IStorage.class, Callable.class); - constructor.setAccessible(true); - Capability.IStorage storage = new Capability.IStorage() { - @Override public INBT writeNBT(Capability cap, T instance, Direction side) { return new CompoundNBT(); } - @Override public void readNBT(Capability cap, T instance, Direction side, INBT data) {} - }; - return (Capability) constructor.newInstance(name, storage, (Callable) () -> null); - } - private static final class TestTank extends MetalTankTileEntity { + TestTank() { super(net.minecraft.core.BlockPos.ZERO, net.minecraft.world.level.block.Blocks.IRON_BLOCK.defaultBlockState()); } void invalidateNow() { invalidateCaps(); } void reviveNow() { reviveCaps(); } } private static final class TestConverter extends cyano.poweradvantage.api.modsupport.rf.TileEntityRFConverter { - TestConverter() { super(null, new cyano.poweradvantage.api.ConduitType("test-power"), 100, 1000, "test"); } + TestConverter() { super(null, net.minecraft.core.BlockPos.ZERO, net.minecraft.world.level.block.Blocks.IRON_BLOCK.defaultBlockState(), new cyano.poweradvantage.api.ConduitType("test-power"), 100, 1000, "test"); } void reviveNow() { reviveCaps(); } } } diff --git a/src/test/java/zone/moddev/mc/poweradvantage/registry/MachineGuiTitleTest.java b/src/test/java/zone/moddev/mc/poweradvantage/registry/MachineGuiTitleTest.java deleted file mode 100644 index c066f9c9..00000000 --- a/src/test/java/zone/moddev/mc/poweradvantage/registry/MachineGuiTitleTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package zone.moddev.mc.poweradvantage.registry; - -import net.minecraft.block.Blocks; -import net.minecraft.tileentity.ChestTileEntity; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.tileentity.TileEntityType; -import net.minecraft.util.text.StringTextComponent; -import net.minecraft.util.text.TranslationTextComponent; -import org.junit.BeforeClass; -import org.junit.Test; -import static org.junit.Assert.*; - -public class MachineGuiTitleTest { - @BeforeClass public static void bootstrap() { - net.minecraft.util.registry.Bootstrap.bootStrap(); - } - - @Test public void defaultTitleUsesTheBlockTranslationInsteadOfAnInternalInventoryName() { - assertEquals("block.minecraft.chest", ((TranslationTextComponent) MachineGUIRegistry.menuTitle( - new ChestTileEntity(), Blocks.CHEST)).getKey()); - } - - @Test public void renamedMachinesKeepThePlayersFullName() { - ChestTileEntity tile = new ChestTileEntity(); - tile.setCustomName(new StringTextComponent("John's oil tank")); - assertEquals("John's oil tank", MachineGUIRegistry.menuTitle(tile, Blocks.CHEST).getString()); - } - - @Test public void unnamedTilesWithoutInventoryNamesStillHaveATranslatableTitle() { - TileEntity tile = new TileEntity(TileEntityType.CHEST) {}; - assertEquals("block.minecraft.chest", ((TranslationTextComponent) MachineGUIRegistry.menuTitle(tile, Blocks.CHEST)).getKey()); - } -} diff --git a/test-rig/NATIVE117.md b/test-rig/NATIVE117.md new file mode 100644 index 00000000..a459e28d --- /dev/null +++ b/test-rig/NATIVE117.md @@ -0,0 +1,126 @@ +# Advantage Works For Minecraft 1.17.1 + +Use this guide for the PA117 checkout. The retained `README.md`, `NATIVE116.md` +and older adapters describe historical versions; they are not current runtime +instructions. See the [test record](../docs/1.17/CURRENT-QUALIFICATION.md) for +completed checks, exact artifacts, retained failures and manual gaps. + +The native target is Forge `37.1.1`, official `1.17.1` mappings, Java 16 and +harness `1.1.0.117011`. Gradle runs on Java 17. Some inherited command-line flags +are still named `--java8`; in every `native117` runner they take the **Java 16 +executable**, not a Java 8 installation. + +## Safety + +These are destructive construction tests for disposable worlds only. Never add +their authorization markers to a valuable world. Keep the harness, probe jars, +worlds, logs and generated reports out of gameplay distributions. + +Every run needs a new output directory under `test-rig/build/`. Existing outputs +are not overwritten. Retained source worlds are fingerprinted, copied and checked +again after use. They are never launched in place. Allocate one unused localhost +port per running campaign, including gaps between its server boots. + +Do not edit a running campaign's source, fixture, manifest or artifacts. A passing +machine result cannot rescue a failed source-custody check. Keep failures and use +a new output name after a justified correction. + +## Build And Offline Checks + +Build the three production mods first using their pinned dependencies and normal +release audits. From Power's root, build the separate harness with: + +```text +gradlew.bat -p test-rig clean check assemble +``` + +The harness compiles against the pinned Power deobf jar. Build the production +mods with their declared API pins first; runtime profiles stage their dependency +jars separately. The harness release goes to `test-rig/build/gradle/libs/`, never +Power's release jar. + +Use Python 3.11 or later with `test-rig/migration/requirements.txt`. For example: + +```powershell +python -m venv test-rig/migration/.venv +& ./test-rig/migration/.venv/Scripts/python.exe -m pip install -r test-rig/migration/requirements.txt +& ./test-rig/migration/.venv/Scripts/python.exe -m unittest discover -s test-rig/migration -p 'test_native117*.py' +``` + +Run the separate shared controls from `test-rig/migration` too: + +```text +python -m unittest test_broad_campaign_status test_audit_native117_partial +``` + +The historical all-version Python discovery command is not a native 1.17 gate: +several older tests deliberately require their original engine or source hashes. +Do not relax those checks or relabel their failures to qualify this port. + +The production Eclipse compiler check is documented +[separately](../tools/verification/native117/README.md). The graphical checks are +in [client-resources/README.md](client-resources/README.md). + +## Pin Inputs Before Running + +Destination profiles are in `profiles/native117/`. They describe inputs, not +automatic acceptance. Populate every declared artifact pin, then use: + +```text +python test-rig/migration/native117.py pin-target --profile --server-seed --output test-rig/build/ +``` + +The installed server must have the exact verified Forge code inventory. The +native launcher uses `@win_args.txt` on Windows, not `-jar win_args.txt`. The Java +Works runner uses its separately pinned files-only seed receipt; it must not +rewrite the canonical Python inventory. + +Keep the resulting manifest immutable. A harness correction needs a new manifest +and a separate report; it does not replace earlier evidence. Use each runner's +`--help` for its required source receipts, output, port and Java parameters. + +## Runtime Coverage + +| Runner | Purpose | +| --- | --- | +| `native117_machine_operations.py` | Finite measured jobs, exact cold restart and raw region-file corroboration | +| `native117_steam_control_audit.py` | Read-only replay of comparator and first-loaded-tick Steam controls | +| `native117_campaign.py` | Hash-protected frozen and connected migration route matrices | +| `native117_operating_import.py` | Preserve the accepted 132-machine 1.16 fixture without reconstructing it | +| `native117_actors.py` and `native117_audit_actors.py` | Players, nested items, entities, dimensions, ownership, real ticking and saved entity regions | +| `native117_player_layout.py` | Exact player bag and Ender storage layouts | +| `native117_respawn.py` and its auditor | Death, drops, retained inventories and respawn through native server handlers | +| `native117_transformer_order.py` and its auditor | Actual Base Metals/Power transformer order, emitted hooks and migration semantics | +| `native117_lifecycle.py` | Moving drills, actual chunk callbacks, mining and restarts | +| `native117_holder_networks.py` | Independent machine/supply or bridge unloading beyond the native ticket halo | +| `native117_endurance.py` and its auditor | Repeated bounded load for 30 minutes, shutdown, restart and resumed processing | +| `native117_recipes.py` | Ingredient triggers, recipe modes and manufacture from available materials | +| `audit_native117_hammer.py` | Independently replay exact query-visible hammer inputs, native recipe selection, one callback, consumption, durability and output quantities | +| `native117_harvest.py` | Real survival-tool, mining-tier and dropped-item checks | +| `native117_interop.py` | Fluid and energy capability acceptance and conservation | +| `native117_oil.py` | Bucket placement, drains, pipes, tanks, Mineralogy oil and full restarts | +| `native117_broad.py` and its auditor | All walkable bays before and after restart, with manual procedures still open | +| `native117_redstone_reload.py` | Naturally earned mid-block drill state and the first native ticks after restart | + +Hammer telemetry records the exact source object in the native query before one +callback. It does not require that a visible item has already ticked. Observer +errors and invisible or contaminated inputs fail before the action; they are +not missing-recipe passes. A later successful action cannot repair a failed +receipt. Retain historical failures whose action-time conditions were not +recorded rather than guessing their cause. + +Use holder-aware layouts for partial-network isolation. Adjacent forced chunks +can keep a short bridge loaded; that layout cannot prove unloading regardless of +how long the runner sleeps. Assertions require real unload/load callbacks and +unchanged endpoint identities, not an assumed absence. + +## Interpret Results Literally + +`PASS` applies only to the stated scope. A frozen import is not machine operation; +progress is not a completed drop; a built bay is not its whole manual checklist. +`INCOMPLETE`, `SKIP` and `MANUAL_REQUIRED` are not passing gameplay tests. + +Native platform differences must remain separate from strict comparisons and +need exact independent evidence. For example, a native non-persistent leash +helper's reconstruction is not UUID persistence in an entity-region file. +Unknown differences, missing quantities and missing evidence remain failures. diff --git a/test-rig/build.gradle b/test-rig/build.gradle index be3bcb39..53cb7e69 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(8) + toolchain.languageVersion = JavaLanguageVersion.of(16) } tasks.withType(JavaCompile).configureEach { javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(8) + languageVersion = JavaLanguageVersion.of(16) } - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_16 + targetCompatibility = JavaVersion.VERSION_16 options.encoding = 'UTF-8' } @@ -66,7 +66,7 @@ repositories { } def powerCompileJar = file(project.findProperty('powerAdvantageDeobfJar') - ?: '../build/libs-dev/PowerAdvantage-2.4.7.116051-deobf.jar') + ?: '../build/libs-dev/basePower-2.4.9.117011-deobf.jar') dependencies { implementation minecraft.dependency( @@ -77,6 +77,23 @@ dependencies { compileOnly files(powerCompileJar) } +def compileUnitBootstrap = tasks.register('compileUnitBootstrap', JavaCompile) { + source fileTree('unit-bootstrap') + classpath = files() + destinationDirectory = layout.buildDirectory.dir('unit-bootstrap/classes') +} +def unitBootstrapJar = tasks.register('unitBootstrapJar', Jar) { + dependsOn compileUnitBootstrap + archiveFileName = 'forge-native-unit-bootstrap.jar' + destinationDirectory = layout.buildDirectory.dir('unit-bootstrap') + from compileUnitBootstrap.flatMap { it.destinationDirectory } + manifest.attributes('Premain-Class': 'zone.moddev.mc.advantageworks.unit.ForgeCapabilityBootstrap') +} +tasks.withType(Test).configureEach { + dependsOn unitBootstrapJar + doFirst { jvmArgs "-javaagent:${unitBootstrapJar.get().archiveFile.get().asFile}" } +} + tasks.register('verifyWorksCompileInputPin') { group = 'verification' description = 'Checks the exact parent-supplied Power API artifact before harness compilation.' @@ -84,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 native116 Power compile input: ${powerCompileJar}") + if (!powerCompileJar.isFile()) throw new GradleException("Missing native117 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("Native116 Power compile input changed: ${actual}; obtain a new parent pin") + throw new GradleException("Native117 Power compile input changed: ${actual}; obtain a new parent pin") } - if (project.poweradvantage_source_commit != 'bcb75bfa91c14751ebf1e31865e1ec0ffa82c0b7') { - throw new GradleException('Native116 Power source pin changed; obtain a new parent candidate') + if (project.poweradvantage_source_commit != '308d42a89483d56d0fad4805cdb65dac483d9e31') { + throw new GradleException('Native117 Power source pin changed; obtain a new parent candidate') } } } @@ -157,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/native116/${profileName}.json").absolutePath + args '--profile', file("profiles/native117/${profileName}.json").absolutePath if (automated) args '--automated' if (project.hasProperty('worksStations')) { args '--stations', project.property('worksStations').toString() @@ -172,7 +189,7 @@ def registerWorksRunner = { String taskName, String profileName, boolean automat } registerWorksRunner('runWorksDev', 'dev-core', false, - 'Builds and launches a disposable interactive 1.16.5 Advantage Works development server.') + 'Builds and launches a disposable interactive 1.17.1 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, @@ -196,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('verifyWorksNative116Inputs', JavaExec) { +tasks.register('verifyWorksNative117Inputs', JavaExec) { group = 'verification' - description = 'Checks native116 candidate identities and hashes without creating or opening a world.' + description = 'Checks native117 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/native116/${project.findProperty('worksProfile') ?: 'packaged-current'}.json").absolutePath, + args '--profile', file("profiles/native117/${project.findProperty('worksProfile') ?: 'packaged-current'}.json").absolutePath, '--validate-only' } @@ -211,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/PowerAdvantage-2.4.7.116051.jar') + ?: '../build/libs/basePower-2.4.9.117011.jar') if (!powerJar.isFile()) { throw new GradleException("Build PowerAdvantage first: ${powerJar}") } @@ -231,3 +248,37 @@ tasks.register('verifyHarnessIsolation') { tasks.named('check') { dependsOn tasks.named('verifyHarnessIsolation') } + +def nativeControlClasspath = configurations.compileClasspath.minus(files(powerCompileJar)) +def compilePlatformControls = tasks.register('compilePlatformControls', JavaCompile) { + source fileTree('platform-controls') + classpath = nativeControlClasspath + destinationDirectory = layout.buildDirectory.dir('platform-controls/classes') +} +tasks.register('verifyNativeWolfPlatform', JavaExec) { + group = 'verification' + dependsOn compilePlatformControls, unitBootstrapJar + classpath = files(compilePlatformControls.flatMap { it.destinationDirectory }) + nativeControlClasspath + mainClass = 'NativeWolfPlatformControl' + javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(16) } + doFirst { + def output = providers.gradleProperty('nativeControlOutput').orNull + if (!output || file(output).exists()) throw new GradleException('Supply a new -PnativeControlOutput directory') + args file(output).absolutePath + jvmArgs "-javaagent:${unitBootstrapJar.get().archiveFile.get().asFile}" + } +} + +tasks.register('verifyNativeLeashKnotPlatform', JavaExec) { + group = 'verification' + dependsOn compilePlatformControls, unitBootstrapJar + classpath = files(compilePlatformControls.flatMap { it.destinationDirectory }) + nativeControlClasspath + mainClass = 'NativeLeashKnotPlatformControl' + javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(16) } + doFirst { + def output = providers.gradleProperty('nativeControlOutput').orNull + if (!output || file(output).exists()) throw new GradleException('Supply a new -PnativeControlOutput directory') + args file(output).absolutePath + jvmArgs "-javaagent:${unitBootstrapJar.get().archiveFile.get().asFile}" + } +} diff --git a/test-rig/client-resources/README.md b/test-rig/client-resources/README.md index 574368da..4b8de01c 100644 --- a/test-rig/client-resources/README.md +++ b/test-rig/client-resources/README.md @@ -1,62 +1,88 @@ # Native Client Resource Check -This build-only Minecraft 1.16.5 / Forge 36.2.34 probe checks PowerAdvantage, or all three Advantage mods in the combined profile, after Minecraft bakes its models and stitches its textures. It uses official 1.16.5 mappings with MCP `20210115.111550`. It registers no gameplay content, opens no world, and shuts down its own disposable client when finished. It is not included in release jars or ordinary Eclipse launches. - -Compilation and static preservation checks pass. The new native graphical run -and its fresh recipe fixtures are still pending. The accepted 1.15 source is -hash-checked; all original assertions and the complete recipe/tank captures are -compared after an explicit native API-name translation. Four negative controls -prove that weaker pixel, tank, bucket and missing-model checks are rejected. -See [the 1.16 status](../../docs/1.16/PORT-STATUS.md). -Use matching native jars/hashes. Run Gradle only when the shared cache is free -of other builds; start the client only when authorized. - -For native recipe-book grouping, supply a **new native 1.16.5 server export** -containing `recipes-{normal,tech,apocalyptic}-sweep.bin`, their matching successful -JSON reports with `bookRecipes`, and `AdvantageWorksMigration/level.dat`. -The copied convenience `migration/recipe_unlocks.py` still targets 1.13.2; do -not use its outputs or historical qualification as 1.16.5 evidence. Native -fixture generation uses the separate server harness. -The separate native campaign retains the five unlock/persistence boots and all -18 progression checks. It reads recipes and advancements from the packaged -candidate jars, validates target hashes, creates new loopback runtimes only, -and fingerprints the installed server seed before and after. It needs an -existing Python 3.11+ environment with `migration/requirements.txt`, plus Java 8; -it does not invoke Gradle: +This disposable Minecraft 1.17.1 / Forge 37.1.1 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. +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. + +## Build And Run ```powershell - -B test-rig/migration/native116_recipes.py --target-manifest '' --java8 '/bin/java.exe' --port 25577 --output test-rig/build/native116-recipes-new +./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' ``` -`--campaign unlock` or `--campaign progression` selects a single campaign. -Native vanilla logs saved recipe IDs removed by a recipe-mode change at ERROR -level. The unlock audit checks the exact set against the difference between -the preceding and current actual recipe packets, retains all log events, and -fails on any unrelated error or a mismatched removal set. It never erases a -saved book to avoid that persistence check. +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` +before regenerating it. Omitting the combined profile retains Power-only +scope. Normal Eclipse launches contain production code only. + +Each run has a new directory under `run-client-resource-runs/`. It retains +the launch, production-file manifest, exact dependency and candidate hashes, +logs, framebuffer images and `client-resources.json`. Missing output, failed +assertions and Advantage model/texture errors fail the run. A runner failure +is retained separately without replacing partial native results. Existing +saves and earlier reports are untouched. `-PreflightOnly` starts no client and +does not claim a graphical pass. + +The default `-OutputSource Gradle` snapshots compiled classes and processed +resources. `-OutputSource Eclipse` copies `bin/main` without overlaying Gradle +resources. `-PowerCandidate` selects an explicit native deobf jar; its hash +is provenance, not proof that the selected loose output equals that jar. -After native exports are available, pass the campaign's `unlock` subdirectory: +## Recipe Fixtures + +Generate fresh exports from the packaged 1.17 candidates, not a historical run: ```powershell -./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -RecipeBookFixtures '/unlock' + -B test-rig/migration/native117_recipes.py --target-manifest '' --java16 '/bin/java.exe' --port 25577 --output test-rig/build/native117-recipes-new ``` -The server exports its actual recipe packets and matching metadata. The client -imports the generated world's Forge registry snapshot before decoding those -packets, reproducing the item-ID synchronization normally supplied by Forge's -connection handshake. The saved header must identify Minecraft 1.16.5 and -Forge 36.2.34; a legacy fixture is rejected. It then rebuilds native -`ClientRecipeBook`/`RecipeList` -cells for Normal, Tech and Apocalyptic modes: each cell must contain exactly one -output identity, all loaded variants must remain accessible, and same-output -alternatives must not split into extra cells. A framebuffer image, -`recipe-book-cells.png`, renders the actual Minecraft recipe buttons for the -four electronics and nine conveyor/filter outputs reported hidden by users. -The probe reads only the disposable fixture's saved registry data; it never -opens that world or a user's world in Minecraft. This is a native cell-model and -button-rendering check, not manual navigation or notification acceptance. - -The static checks do not require artifacts, Gradle, Java or a graphical client: +Use an existing Python 3.11+ environment with `migration/requirements.txt`. +This runs five unlock/persistence boots and 18 progression checks across all +recipe modes, with rubber enabled and disabled. `--campaign unlock` or +`--campaign progression` selects one part. Mode changes legitimately prune +removed recipe IDs from saved books: the audit retains those log events and +requires their exact set to match the preceding and current recipe packets. +Unrelated errors still fail. + +Pass the resulting `unlock` directory to the client. It must contain the three +`recipes-*-sweep.bin` packets, matching successful JSON reports with +`bookRecipes` and the generated world's `level.dat`. The client reads its +registry snapshot before decoding the packets; it does not open the world. +Historical or mismatched headers fail. Native recipe-book collections must +group only identical outputs, retain every variant and render thirteen separate +example cells with more than 1,000 colored framebuffer pixels. Without fixtures +this is explicitly `NOT_RUN_NO_NATIVE_FIXTURES`. + +## What Is Checked + +- Every registered Advantage item and valid block state: geometry, missing + models/sprites, English names, creative-tab contents and GUI translation keys. + Native fluid-block models are intentionally particle-only. +- Actual tank screen rendering, full/empty 10,000 mB update packets, fluid + capability simulation/execution and neighbour-face culling. The water-gauge + pixel at `(88,39)` must satisfy `blue > red + 20` and `blue > green + 10`. +- Native bucket fluids and fluid-renderer vertices for crude/refined oil, + with water/lava controls, actual atlas sprites and tint. +- With `-MachineGuiLayouts`, real screen rendering at nine window/GUI-scale + combinations, caption pixels, slot/frame placement and deliberately broken + controls. Screenshots are retained before assertions. +- With `-ChildRendererGeometry`, registered drill-bit, beam and turret geometry + 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: ```powershell ./test-rig/client-resources/test-launcher.ps1 -StaticOnly @@ -66,44 +92,6 @@ The static checks do not require artifacts, Gradle, Java or a graphical client: ./test-rig/client-resources/test-probe-contract-negative-controls.ps1 ``` -Before running Gradle, check that the shared `D:/GradleCaches/minecraft-mods-shared` -cache is free of other builds. Use the repository's Gradle 9.6.1 / ForgeGradle 7 setup with Java 17 -for Gradle and the configured Java 8 compilation toolchain. Supply Java 8 to -the disposable launcher. Do not launch the actual client until authorized: - -```powershell -./gradlew.bat check clientResourceProbeJar verifyEclipseProductionClasspath -./gradlew.bat prepareCombinedEclipseLaunch -./test-rig/client-resources/test-launcher.ps1 -./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -RecipeBookFixtures '/unlock' -PreflightOnly -# Only after authorization for the graphical run: -./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -RecipeBookFixtures '/unlock' -``` - -Each run gets a new directory under `run-client-resource-runs/`. The runner snapshots production classes/resources, uses the audited native launch dependencies, and loads only the probe as an additional mod. Helpers launch hidden, with separate stdout/stderr logs and a bounded timeout. It retains logs, images and `client-resources.json`; missing reports, failed assertions, and model/texture errors fail the run. A runner failure gets a separate `runner-failure.json`, without replacing partial native results. Existing saves, configuration, and prior reports are untouched. `-PreflightOnly` creates an input snapshot without starting Java or claiming a graphical pass. - -New GUI captures are kept outside Gradle's `build/` directory so `clean` cannot remove them. This does not recreate any historical captures already lost from the former build-based location. - -The default `-OutputSource Gradle` merges Gradle's classes and processed resources. To test the actual Eclipse output instead, pass `-OutputSource Eclipse`; this copies `bin/main` verbatim and does not overlay Gradle resources. `production-files.json` hashes every selected class/resource. `client-input.json` records that manifest's hash, the Java binary, probe, launch, production metadata, every dependency archive, expansion candidate hashes and optional fixture hashes. The current Power deobf candidate is recorded separately, defaulting to `build/libs-dev/PowerAdvantage-2.4.6.115021-deobf.jar`; `-PowerCandidate` selects an explicit native candidate. That jar hash is provenance, not a claim that Eclipse/Gradle output equals the jar. Missing/unexpanded metadata or a legacy target fails preflight. The native result records actual discovered mod versions and the exact game/Forge target. Refresh candidate hashes after any production rebuild before final evidence is taken. The [historical Eclipse metadata regression](../../docs/1.13/ECLIPSE-METADATA.md) explains the original failure, not native 1.15 acceptance. - -For the combined run, use `gradlew.bat prepareCombinedEclipseLaunch`, then add `-LaunchProfile runClientAllAdvantage.launch -RequireExtensions` to the probe command. The report's `advantage_mods` must list all three mods. With this flag, resource and item-name assertions cover Power, Steam and Electric, and model/blockstate errors from any of their namespaces fail the run. Without it, the original Power-only scope is unchanged. Power must be `2.4.6.115021`; Steam and Electric must each be `2.2.7.115021`. Pin every rebuilt expansion jar in the native116 profiles before regenerating the combined launch. Recipe grouping requires that combined scope; without fixtures it explicitly reports `NOT_RUN_NO_NATIVE_FIXTURES`, not a full combined resource/recipe pass. - -Assertions cover every registered item and valid block state in the selected Advantage namespaces, nonempty item/machine geometry, missing-model and missing-texture sprites, English item names, the shared creative tab and its icon, and agreement between Power's modern block and legacy GUI translation keys. Native fluid block models are intentionally particle-only. This test does not certify machine operation, animated entity rendering, or third-party resource packs. - -The `ClientResourceContractTest` checks cover literal model paths, removed legacy blockstate selectors, model parents/textures (including vanilla assets), oil item models, locale aliases, and English block/bucket coverage. Their historical 1.13 red/green results do not qualify this destination. - -Tank visual regressions use Minecraft's actual neighbour-face culling beneath both tanks, native Forge fluid capability simulation/execution, full 10,000 mB and empty tank update packets, and a native registered `ContainerType` screen initialized without a player/world. The actual filtered-tank gauge and frame render into an OpenGL framebuffer through the production GUI. The pixel at `(88,39)` must remain blue with the original thresholds (`blue > red + 20`, `blue > green + 10`); `tank-water-gauge.png` is written before assertions. Historical hidden-floor/untinted `ADADAD` observations remain 1.13 evidence only. No test-world loading or fluid-network changes are required. - -Oil regressions verify both buckets through native `BucketItem.getFluid()` and Forge's fluid-item capability, then call the initialized native `BlockRendererDispatcher` / `FluidBlockRenderer` against a read-only synthetic `ILightReader`. Still and flowing crude/refined oil must emit vertices using their own atlas sprites and `FFFFFF` top-face tint. Vanilla water and lava remain controls. Forge 31 reads sprites/tint from `FluidAttributes`; no removed oil transformer or 1.13 hook is assumed. Completed vertices are inspected through `BufferBuilder.getNextBuffer()` and the actual native format element offsets; atlas lookup uses `getAtlasSpriteGetter(AtlasTexture.LOCATION_BLOCKS_TEXTURE)`. The probe temporarily supplies native vanilla water/lava tags at the main menu and restores them afterward; no production tags or saves are written. `fluid_rendering` retains case results. The historical water-sprite/`3F76E4` failure is not native 1.15 evidence. - -The native recipe image still requires exactly thirteen separate cells and more -than 1,000 colored framebuffer pixels. A blank image is retained before failure. -Compilation and input preflight are not graphical acceptance, recipe discovery -acceptance, manual GUI navigation or server-to-client menu-handshake acceptance. - -The optional `-ChildRendererGeometry` combined-run switch adds actual registered -Steam drill-bit and Electric beam/turret buffer checks after these primary -oracles. It does not alter their capture logic or thresholds. Hydroturbine -registration/texture are checked, but orientation/spin geometry remains an -explicit disposable-world gap. See [Additional Renderer Geometry](../../docs/1.15/CLIENT-RESOURCES.md#additional-renderer-geometry) -for scope, the static wiring test and the completed native run evidence. +These checks do not certify machine operation, live animated entities, +multiplayer menu handshakes, manual recipe discovery or third-party resource +packs. Those remain separate from compilation and graphical qualification. diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ChildRendererGeometryProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ChildRendererGeometryProbe.java index 76c9382a..2e735683 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ChildRendererGeometryProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ChildRendererGeometryProbe.java @@ -2,28 +2,28 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import com.mojang.blaze3d.matrix.MatrixStack; -import com.mojang.blaze3d.vertex.IVertexBuilder; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; import com.mojang.datafixers.util.Pair; import cyano.poweradvantage.api.PoweredEntity; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.BufferBuilder; -import net.minecraft.client.renderer.IRenderTypeBuffer; -import net.minecraft.client.renderer.RenderState; +import com.mojang.blaze3d.vertex.BufferBuilder; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderStateShard; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.entity.EntityRenderer; import net.minecraft.client.renderer.texture.OverlayTexture; -import net.minecraft.client.renderer.tileentity.TileEntityRenderer; -import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher; -import net.minecraft.client.renderer.vertex.DefaultVertexFormats; -import net.minecraft.client.renderer.vertex.VertexFormat; -import net.minecraft.client.renderer.vertex.VertexFormatElement; -import net.minecraft.entity.EntityType; -import net.minecraft.resources.IResource; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.tileentity.TileEntityType; -import net.minecraft.util.Direction; -import net.minecraft.util.ResourceLocation; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.VertexFormat; +import com.mojang.blaze3d.vertex.VertexFormatElement; +import net.minecraft.world.entity.EntityType; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.registries.ForgeRegistries; import javax.imageio.ImageIO; @@ -45,10 +45,10 @@ final class ChildRendererGeometryProbe { private static final float EPSILON = 0.0001F; private static final int LIGHT = 0xF000F0; - private static final String STEAM_RENDERER = "zone.moddev.mc.steamadvantage.graphics.DrillBitRenderer"; - private static final String DRILL_RENDERER = "zone.moddev.mc.electricadvantage.graphics.LaserDrillRenderer"; - private static final String TURRET_RENDERER = "zone.moddev.mc.electricadvantage.graphics.LaserTurretRenderer"; - private static final String TURBINE_RENDERER = "zone.moddev.mc.electricadvantage.graphics.HydroturbineRenderer"; + private static final String STEAM_RENDERER = "zone.moddev.mc.basesteam.graphics.DrillBitRenderer"; + private static final String DRILL_RENDERER = "zone.moddev.mc.baseelectric.graphics.LaserDrillRenderer"; + private static final String TURRET_RENDERER = "zone.moddev.mc.baseelectric.graphics.LaserTurretRenderer"; + private static final String TURBINE_RENDERER = "zone.moddev.mc.baseelectric.graphics.HydroturbineRenderer"; static void verify(Minecraft minecraft, JsonObject result) { JsonObject report = new JsonObject(); @@ -63,13 +63,13 @@ static void verify(Minecraft minecraft, JsonObject result) { drill(minecraft, cases); turret(minecraft, cases); run(cases, "hydroturbine-registration", row -> { - EntityType type = ForgeRegistries.ENTITIES.getValue(new ResourceLocation("electricadvantage:hydroturbine")); + EntityType type = ForgeRegistries.ENTITIES.getValue(new ResourceLocation("baseelectric:hydroturbine")); require(type != null, "Missing registered hydroturbine type"); EntityRenderer renderer = minecraft.getEntityRenderDispatcher().renderers.get(type); require(renderer != null && TURBINE_RENDERER.equals(renderer.getClass().getName()), "Missing production hydroturbine renderer"); row.addProperty("renderer", renderer.getClass().getName()); ResourceLocation texture = (ResourceLocation) valueOfType(renderer, ResourceLocation.class); - require(new ResourceLocation("electricadvantage:textures/entity/hydroturbine.png").equals(texture), "Wrong hydroturbine texture"); + require(new ResourceLocation("baseelectric:textures/entity/hydroturbine.png").equals(texture), "Wrong hydroturbine texture"); texture(minecraft, texture, row); row.addProperty("status", "MANUAL_GAP_WORLD_REQUIRED"); row.addProperty("gap", "HydroturbineEntity constructor reads World.rand; orientation/spin vertices require a fresh disposable client world. No entity was constructed, no render invoked, and no geometry pass is claimed."); @@ -104,11 +104,11 @@ private static void steam(Minecraft minecraft, JsonArray cases) { for (Direction facing : Direction.values()) { final List> frames = new ArrayList<>(); for (float partial : new float[]{0F, 0.5F}) run(cases, "steam-drillbit-" + facing + "-" + partial, row -> { - TileEntity tile = tile("steamadvantage:drillbit"); + BlockEntity tile = tile("basesteam:drillbit"); tile.getClass().getMethod("setDirection", Direction.class).invoke(tile, facing); set(tile, "rotation", 0F); List vertices = render(minecraft, tile, STEAM_RENDERER, partial, - "steamadvantage:textures/entity/drill_bit.png", row); + "basesteam:textures/entity/drill_bit.png", row); mesh(vertices, 24); int axis = facing.getAxis() == Direction.Axis.X ? 0 : facing.getAxis() == Direction.Axis.Y ? 1 : 2; for (float[] vertex : vertices) for (int i = 0; i < 3; i++) require(vertex[i] >= -EPSILON && vertex[i] <= 1 + EPSILON, "Drill bit escaped its block"); @@ -122,14 +122,14 @@ private static void steam(Minecraft minecraft, JsonArray cases) { private static void drill(Minecraft minecraft, JsonArray cases) { for (Direction facing : Direction.values()) run(cases, "electric-drill-" + facing, row -> { - TileEntity tile = tile("electricadvantage:electric_drill"); + BlockEntity tile = tile("baseelectric:electric_drill"); // These are the production tile's synchronized renderer inputs, not replacement methods. set(tile, "facingCache", facing); set(tile, "laserLength", 4); require(tile.getClass().getMethod("getFacing").invoke(tile) == facing, "Drill fixture facing was not consumed"); require(((Number) tile.getClass().getMethod("getDrillLength").invoke(tile)).intValue() == 4, "Drill fixture length was not consumed"); List vertices = render(minecraft, tile, DRILL_RENDERER, 0.5F, - "electricadvantage:textures/materials/laser_beam.png", row); + "baseelectric:textures/materials/laser_beam.png", row); mesh(vertices, 16); beam(vertices, facing, new float[]{0.5F, 0.5F, 0.5F}, 4F); for (float[] vertex : vertices) require(close(vertex[3], 0) && close(vertex[4], 1), "Drill beam UV layout changed"); @@ -138,11 +138,11 @@ private static void drill(Minecraft minecraft, JsonArray cases) { private static void turret(Minecraft minecraft, JsonArray cases) { run(cases, "turret-idle", row -> { - TileEntity tile = tile("electricadvantage:laser_turret"); + BlockEntity tile = tile("baseelectric:laser_turret"); set(tile, "rotYaw", -90F); set(tile, "rotOldYaw", -90F); List vertices = render(minecraft, tile, TURRET_RENDERER, 0.5F, - "electricadvantage:textures/entity/laser_turret.png", row); + "baseelectric:textures/entity/laser_turret.png", row); mesh(vertices, 24); require(close(min(vertices, 0), 0.25F) && close(max(vertices, 0), 0.75F) && close(min(vertices, 1), 0.5F) && close(max(vertices, 1), 1F) @@ -150,7 +150,7 @@ && close(min(vertices, 1), 0.5F) && close(max(vertices, 1), 1F) for (int i = 0; i < 4; i++) require(vertices.get(i)[4] >= 0.5F - EPSILON, "Idle turret uses the active optic UVs"); }); for (Direction facing : Direction.values()) run(cases, "turret-target-" + facing, row -> { - TileEntity tile = tile("electricadvantage:laser_turret"); + BlockEntity tile = tile("baseelectric:laser_turret"); PoweredEntity machine = (PoweredEntity) tile; machine.setEnergy(2000F, machine.getTypes()[0]); float yaw = facing == Direction.EAST ? 0F : facing == Direction.SOUTH ? 90F : facing == Direction.WEST ? 180F : -90F; @@ -165,7 +165,7 @@ && close(min(vertices, 1), 0.5F) && close(max(vertices, 1), 1F) require(Boolean.TRUE.equals(tile.getClass().getMethod("isActive").invoke(tile)) && Boolean.TRUE.equals(tile.getClass().getMethod("showLaserLine").invoke(tile)), "Turret fixture did not enable the production target branch"); List vertices = render(minecraft, tile, TURRET_RENDERER, 0.5F, - "electricadvantage:textures/entity/laser_turret.png", row); + "baseelectric:textures/entity/laser_turret.png", row); mesh(vertices, 40); beam(vertices.subList(24, 40), facing, new float[]{0.5F, 0.75F, 0.5F}, 4F); for (float[] vertex : vertices.subList(0, 24)) for (int i = 0; i < 3; i++) @@ -174,24 +174,27 @@ && close(min(vertices, 1), 0.5F) && close(max(vertices, 1), 1F) }); } - private static TileEntity tile(String id) { - TileEntityType type = ForgeRegistries.TILE_ENTITIES.getValue(new ResourceLocation(id)); + private static BlockEntity tile(String id) { + BlockEntityType type = ForgeRegistries.BLOCK_ENTITIES.getValue(new ResourceLocation(id)); require(type != null, "Missing registered tile type " + id); - TileEntity tile = type.create(); + net.minecraft.world.level.block.state.BlockState state = ForgeRegistries.BLOCKS.getValues().stream() + .map(net.minecraft.world.level.block.Block::defaultBlockState).filter(type::isValid).findFirst() + .orElseThrow(() -> new IllegalStateException("No valid native block state for " + id)); + BlockEntity tile = type.create(net.minecraft.core.BlockPos.ZERO, state); require(tile != null && tile.getLevel() == null, "Tile factory must create a worldless native fixture"); return tile; } - private static List render(Minecraft minecraft, TileEntity tile, String expectedRenderer, + private static List render(Minecraft minecraft, BlockEntity tile, String expectedRenderer, float partial, String expectedTexture, JsonObject row) throws Exception { - TileEntityRenderer renderer = TileEntityRendererDispatcher.instance.getRenderer(tile); + BlockEntityRenderer renderer = minecraft.getBlockEntityRenderDispatcher().getRenderer(tile); require(renderer != null && expectedRenderer.equals(renderer.getClass().getName()), "Dispatcher has no expected production renderer: " + expectedRenderer); row.addProperty("renderer", renderer.getClass().getName()); row.addProperty("partial_tick", partial); - MatrixStack matrices = new MatrixStack(); - MatrixStack.Entry original = matrices.last(); - net.minecraft.util.math.vector.Matrix4f position = original.pose().copy(); - net.minecraft.util.math.vector.Matrix3f normal = original.normal().copy(); + PoseStack matrices = new PoseStack(); + PoseStack.Pose original = matrices.last(); + com.mojang.math.Matrix4f position = original.pose().copy(); + com.mojang.math.Matrix3f normal = original.normal().copy(); Capture buffers = new Capture(minecraft, new ResourceLocation(expectedTexture), row); boolean balanced; Throwable renderFailure = null; @@ -218,7 +221,7 @@ private static List render(Minecraft minecraft, TileEntity tile, String return buffers.vertices; } - private static final class Capture implements IRenderTypeBuffer { + private static final class Capture implements MultiBufferSource { private final Minecraft minecraft; private final ResourceLocation expectedTexture; private final JsonArray batches = new JsonArray(); @@ -231,8 +234,8 @@ private static final class Capture implements IRenderTypeBuffer { row.add("native_buffers", batches); } - @Override public IVertexBuilder getBuffer(RenderType type) { - require(type.mode() == 7 && type.format().equals(DefaultVertexFormats.NEW_ENTITY), "Unexpected production renderer vertex format/mode"); + @Override public VertexConsumer getBuffer(RenderType type) { + require(type.mode() == com.mojang.blaze3d.vertex.VertexFormat.Mode.QUADS && type.format().equals(DefaultVertexFormat.NEW_ENTITY), "Unexpected production renderer vertex format/mode"); BufferBuilder buffer = buffers.get(type); if (buffer == null) { require(buffers.size() < 8, "Renderer requested too many buffers"); @@ -275,8 +278,8 @@ void finish() throws Exception { raw.add(values); } // Inspect the texture actually selected by the production RenderType, not a substitute type. - Object state = valueOfType(entry.getKey(), RenderType.State.class); - Object textureState = valueOfType(state, RenderState.TextureState.class); + Object state = valueOfType(entry.getKey(), RenderType.CompositeState.class); + Object textureState = valueOfType(state, RenderStateShard.EmptyTextureStateShard.class); Optional selected = (Optional) valueOfType(textureState, Optional.class); require(selected.isPresent() && selected.get() instanceof ResourceLocation, "Production render type has no texture"); ResourceLocation selectedTexture = (ResourceLocation) selected.get(); @@ -319,7 +322,7 @@ private static void beam(List vertices, Direction facing, float[] origi } private static void texture(Minecraft minecraft, ResourceLocation location, JsonObject row) throws IOException { - try (IResource resource = minecraft.getResourceManager().getResource(location)) { + try (Resource resource = minecraft.getResourceManager().getResource(location)) { BufferedImage image = ImageIO.read(resource.getInputStream()); require(image != null && image.getWidth() > 0 && image.getHeight() > 0, "Renderer texture is not a readable image: " + location); row.addProperty("texture", location.toString()); 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 2a38d605..b9a5a361 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 @@ -1,24 +1,24 @@ package zone.moddev.mc.advantageworks.client; import com.google.gson.JsonObject; -import net.minecraft.block.Block; -import net.minecraft.block.BlockState; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.model.BakedQuad; -import net.minecraft.client.renderer.model.IBakedModel; -import net.minecraft.client.resources.I18n; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.util.Direction; -import net.minecraft.util.NonNullList; -import net.minecraft.util.ResourceLocation; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.event.TickEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.registries.ForgeRegistries; import org.apache.logging.log4j.LogManager; -import zone.moddev.mc.poweradvantage.init.ItemGroups; +import zone.moddev.mc.basepower.init.ItemGroups; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -45,14 +45,14 @@ 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.16.5".equals(result.get("minecraft").getAsString()) - && "36.2.34".equals(result.get("forge").getAsString()), "Wrong native client target"); + require("1.17.1".equals(result.get("minecraft").getAsString()) + && "37.1.1".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); com.google.gson.JsonArray mods = new com.google.gson.JsonArray(); JsonObject versions = new JsonObject(); - for (String id : new String[]{"poweradvantage", "steamadvantage", "electricadvantage"}) { + for (String id : new String[]{"basepower", "basesteam", "baseelectric"}) { boolean loaded = net.minecraftforge.fml.ModList.get().isLoaded(id); if (loaded) { mods.add(id); @@ -63,24 +63,24 @@ public final class ClientResourceProbe { } result.add("advantage_mods", mods); result.add("mod_versions", versions); - require(versions.has("poweradvantage") && zone.moddev.mc.poweradvantage.PowerAdvantage.VERSION - .equals(versions.get("poweradvantage").getAsString()), "Power metadata does not match the compiled version"); - require("2.4.7.116051".equals(versions.get("poweradvantage").getAsString()), "Wrong native Power candidate"); - for (String id : new String[]{"steamadvantage", "electricadvantage"}) { - if (versions.has(id)) require("2.2.8.116051".equals(versions.get(id).getAsString()), "Wrong native expansion candidate: " + id); + 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"); + 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); } require(minecraft.getModelManager().getMissingModel() != null, "Timed out before model baking"); require(minecraft.getOverlay() == null, "Timed out before native resource loading completed"); int items = 0; int states = 0; - IBakedModel missing = minecraft.getModelManager().getMissingModel(); + BakedModel missing = minecraft.getModelManager().getMissingModel(); for (Item item : ForgeRegistries.ITEMS) { ResourceLocation id = item.getRegistryName(); if (id == null || !included(id.getNamespace())) continue; ItemStack stack = new ItemStack(item); String key = item.getDescriptionId(stack); require(!I18n.get(key).equals(key), "Untranslated item: " + id + " -> " + key); - IBakedModel model = minecraft.getItemRenderer().getModel(stack, null, null); + BakedModel model = minecraft.getItemRenderer().getModel(stack, null, null, 0); verifyModel(model, missing, null, id.toString(), true); items++; } @@ -88,8 +88,8 @@ public final class ClientResourceProbe { ResourceLocation id = block.getRegistryName(); if (id == null || !included(id.getNamespace())) continue; for (BlockState state : block.getStateDefinition().getPossibleStates()) { - IBakedModel model = minecraft.getModelManager().getBlockModelShaper().getBlockModel(state); - verifyModel(model, missing, state, state.toString(), !(block instanceof net.minecraft.block.FlowingFluidBlock)); + BakedModel model = minecraft.getModelManager().getBlockModelShaper().getBlockModel(state); + verifyModel(model, missing, state, state.toString(), !(block instanceof net.minecraft.world.level.block.LiquidBlock)); states++; } } @@ -98,7 +98,7 @@ public final class ClientResourceProbe { require(items > 0 && states > 0, "Power Advantage content is not registered"); require(!contents.isEmpty(), "Power Advantage creative tab is empty"); require(!ItemGroups.tab_powerAdvantage.getIconItem().isEmpty(), "Power Advantage creative tab has no icon"); - require(I18n.get("block.poweradvantage.still").equals(I18n.get("tile.poweradvantage.still.name")), + require(I18n.get("block.basepower.still").equals(I18n.get("tile.basepower.still.name")), "Legacy GUI translation differs from modern block name"); TankVisualProbe.verify(minecraft, result); MachineGuiLayoutProbe.verify(minecraft, result); @@ -124,14 +124,14 @@ public final class ClientResourceProbe { } private static boolean included(String namespace) { - return "poweradvantage".equals(namespace) || (Boolean.getBoolean("advantageworks.requireExtensions") - && ("steamadvantage".equals(namespace) || "electricadvantage".equals(namespace))); + return "basepower".equals(namespace) || (Boolean.getBoolean("advantageworks.requireExtensions") + && ("basesteam".equals(namespace) || "baseelectric".equals(namespace))); } private static void verifyNetworkBootstrap(JsonObject result) { try { - Class.forName("net.minecraft.network.play.client.CMoveVehiclePacket"); - Class protocol = Class.forName("net.minecraft.network.ProtocolType"); + Class.forName("net.minecraft.network.protocol.game.ServerboundMoveVehiclePacket"); + Class protocol = Class.forName("net.minecraft.network.ConnectionProtocol"); require(protocol.isEnum() && protocol.getEnumConstants().length > 0, "Vanilla network protocols did not initialise"); result.addProperty("network_bootstrap_status", "PASS"); @@ -140,9 +140,9 @@ private static void verifyNetworkBootstrap(JsonObject result) { } } - private static void verifyModel(IBakedModel model, IBakedModel missing, BlockState state, String id, boolean requireQuads) { + private static void verifyModel(BakedModel model, BakedModel missing, BlockState state, String id, boolean requireQuads) { require(model != missing, "Missing baked model: " + id); - require(!"missingno".equals(model.getParticleTexture(net.minecraftforge.client.model.data.EmptyModelData.INSTANCE).getName().getPath()), "Missing particle texture: " + id); + require(!"missingno".equals(model.getParticleIcon(net.minecraftforge.client.model.data.EmptyModelData.INSTANCE).getName().getPath()), "Missing particle texture: " + id); Random random = new Random(42L); List quads = new ArrayList<>(model.getQuads(state, null, random)); for (Direction side : Direction.values()) quads.addAll(model.getQuads(state, side, random)); 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 6f4918a4..577261cf 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 @@ -3,7 +3,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import com.mojang.blaze3d.matrix.MatrixStack; +import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.systems.RenderSystem; import cyano.poweradvantage.api.GUIBlock; import cyano.poweradvantage.api.PoweredEntity; @@ -23,20 +23,20 @@ import java.util.List; import java.util.Map; import javax.imageio.ImageIO; -import net.minecraft.block.Block; +import net.minecraft.world.level.block.Block; import net.minecraft.client.Minecraft; -import net.minecraft.client.entity.player.ClientPlayerEntity; -import net.minecraft.client.shader.Framebuffer; -import net.minecraft.client.world.ClientWorld; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.entity.player.PlayerInventory; -import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.container.Slot; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.text.TranslationTextComponent; +import net.minecraft.client.player.LocalPlayer; +import com.mojang.blaze3d.pipeline.RenderTarget; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.Container; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.TranslatableComponent; import net.minecraftforge.registries.ForgeRegistries; -import zone.moddev.mc.poweradvantage.registry.MachineGUIRegistry; +import zone.moddev.mc.basepower.registry.MachineGUIRegistry; /** Renders every registered machine screen, not just its detached decorations. */ final class MachineGuiLayoutProbe { @@ -49,27 +49,27 @@ static void verify(Minecraft minecraft, JsonObject result) { result.addProperty("machine_gui_text_spec", 2); int textChecks = 0, negativeControls = 0; List problems = new ArrayList<>(); - ClientPlayerEntity originalPlayer = minecraft.player; + LocalPlayer originalPlayer = minecraft.player; try { JsonObject expectations; try (InputStream stream = MachineGuiLayoutProbe.class.getResourceAsStream("/gui-label-expectations.json")) { require(stream != null, "Missing independent GUI label expectations"); expectations = new JsonParser().parse(new InputStreamReader(stream, StandardCharsets.UTF_8)).getAsJsonObject(); } - require("Inventory".equals(net.minecraft.client.resources.I18n.get("container.inventory")), "GUI label audit requires en_us"); + require("Inventory".equals(net.minecraft.client.resources.language.I18n.get("container.inventory")), "GUI label audit requires en_us"); // ContainerScreen needs a carried-item inventory, but no world or connection. Class allocator = Class.forName("sun.misc.Unsafe"); Field singleton = allocator.getDeclaredField("theUnsafe"); singleton.setAccessible(true); - ClientPlayerEntity player = (ClientPlayerEntity) allocator.getMethod("allocateInstance", Class.class) - .invoke(singleton.get(null), ClientPlayerEntity.class); - PlayerInventory inventory = new PlayerInventory(player); - Field inventoryField = PlayerEntity.class.getDeclaredField("inventory"); + LocalPlayer player = (LocalPlayer) allocator.getMethod("allocateInstance", Class.class) + .invoke(singleton.get(null), LocalPlayer.class); + Inventory inventory = new Inventory(player); + Field inventoryField = Player.class.getDeclaredField("inventory"); inventoryField.setAccessible(true); inventoryField.set(player, inventory); - Field cooldowns = PlayerEntity.class.getDeclaredField("cooldowns"); + Field cooldowns = Player.class.getDeclaredField("cooldowns"); cooldowns.setAccessible(true); - cooldowns.set(player, new net.minecraft.util.CooldownTracker()); + cooldowns.set(player, new net.minecraft.world.item.ItemCooldowns()); minecraft.player = player; Field definitionsField = MachineGUIRegistry.class.getDeclaredField("DEFINITIONS"); definitionsField.setAccessible(true); @@ -89,8 +89,8 @@ static void verify(Minecraft minecraft, JsonObject result) { require(definition instanceof SimpleMachineGUI, "No shared screen definition"); Field textureField = SimpleMachineGUI.class.getDeclaredField("guiDisplayImage"); textureField.setAccessible(true); - net.minecraft.util.ResourceLocation texture = (net.minecraft.util.ResourceLocation) textureField.get(definition); - try (net.minecraft.resources.IResource resource = minecraft.getResourceManager().getResource(texture)) { + net.minecraft.resources.ResourceLocation texture = (net.minecraft.resources.ResourceLocation) textureField.get(definition); + try (net.minecraft.server.packs.resources.Resource resource = minecraft.getResourceManager().getResource(texture)) { BufferedImage artwork = ImageIO.read(resource.getInputStream()); // The GUI UVs use a logical 256-square atlas; Electric supplies 2x artwork. require(artwork != null && artwork.getWidth() >= 256 && artwork.getWidth() % 256 == 0 @@ -99,21 +99,21 @@ static void verify(Minecraft minecraft, JsonObject result) { row.addProperty("texture_pixels", artwork.getWidth()); } row.addProperty("texture", texture.toString()); - TileEntity tile = block.createTileEntity(block.defaultBlockState(), null); - require(tile instanceof IInventory, "GUI block has no inventory tile"); + BlockEntity tile = ((net.minecraft.world.level.block.EntityBlock)block).newBlockEntity(BlockPos.ZERO, block.defaultBlockState()); + require(tile instanceof Container, "GUI block has no inventory tile"); GuiWorld world = (GuiWorld) allocator.getMethod("allocateInstance", Class.class) .invoke(singleton.get(null), GuiWorld.class); world.state = block.defaultBlockState(); - Field clientSide = net.minecraft.world.World.class.getDeclaredField("isClientSide"); + Field clientSide = net.minecraft.world.level.Level.class.getDeclaredField("isClientSide"); clientSide.setAccessible(true); clientSide.setBoolean(world, true); - tile.setLevelAndPosition(world, BlockPos.ZERO); - SimpleMachineGUI.Container menu = ((SimpleMachineGUI) definition).createMenu(1, inventory, (IInventory) tile); + tile.setLevel(world); + SimpleMachineGUI.Container menu = ((SimpleMachineGUI) definition).createMenu(1, inventory, (Container) tile); require(expectations.getAsJsonObject("machines").has(id), "No human-facing label expectation for " + id); String expectedTitle = expectations.getAsJsonObject("machines").get(id).getAsString(); - Method titleFactory = MachineGUIRegistry.class.getDeclaredMethod("menuTitle", TileEntity.class, Block.class); + Method titleFactory = MachineGUIRegistry.class.getDeclaredMethod("menuTitle", BlockEntity.class, Block.class); titleFactory.setAccessible(true); - net.minecraft.util.text.ITextComponent title = (net.minecraft.util.text.ITextComponent) titleFactory.invoke(null, tile, block); + net.minecraft.network.chat.Component title = (net.minecraft.network.chat.Component) titleFactory.invoke(null, tile, block); RecordingScreen screen = new RecordingScreen((SimpleMachineGUI) definition, menu, inventory, title, expectedTitle, expectations.get("inventory").getAsString()); row.addProperty("definition", definition.getClass().getName()); @@ -125,9 +125,9 @@ static void verify(Minecraft minecraft, JsonObject result) { screen.init(minecraft, viewport[0], viewport[1]); verifySlots(screen, menu); for (int fill = 0; fill <= 2; fill++) { - for (net.minecraft.state.Property property : block.defaultBlockState().getProperties()) { - if (property instanceof net.minecraft.state.BooleanProperty && "active".equals(property.getName())) { - world.state = block.defaultBlockState().setValue((net.minecraft.state.BooleanProperty) property, fill > 0); + for (net.minecraft.world.level.block.state.properties.Property property : block.defaultBlockState().getProperties()) { + if (property instanceof net.minecraft.world.level.block.state.properties.BooleanProperty && "active".equals(property.getName())) { + world.state = block.defaultBlockState().setValue((net.minecraft.world.level.block.state.properties.BooleanProperty) property, fill > 0); } } populate(tile, fill * 0.5F); @@ -156,18 +156,18 @@ static void verify(Minecraft minecraft, JsonObject result) { negativeControls++; } } - inventory.setItem(0, new net.minecraft.item.ItemStack(net.minecraft.item.Items.DIAMOND, 2)); + inventory.setItem(0, new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.DIAMOND, 2)); screen.init(minecraft, 320, 240); BufferedImage tooltip = render(minecraft, screen, VIEWPORTS[0], false, true); require(screen.itemTooltipCalls == 1, "Hovered inventory item has no native tooltip"); require(screen.hovered() == menu.slots.get(menu.slots.size() - 9), "Inventory hitbox is misaligned"); ImageIO.write(tooltip, "png", new File("machine-gui-layouts/" + id.replace(':', '-') + "-tooltip.png")); - inventory.setItem(0, net.minecraft.item.ItemStack.EMPTY); - if (block == zone.moddev.mc.poweradvantage.init.Blocks.metal_storage_tank) { + inventory.setItem(0, net.minecraft.world.item.ItemStack.EMPTY); + if (block == zone.moddev.mc.basepower.init.Blocks.metal_storage_tank) { negativeControls += verifyMissingLabelControls(minecraft, screen); verifyLongName(minecraft, (SimpleMachineGUI) definition, menu, inventory); } - if (block == zone.moddev.mc.poweradvantage.init.Blocks.metal_storage_tank) { + if (block == zone.moddev.mc.basepower.init.Blocks.metal_storage_tank) { verifyHelperLimits(screen); } row.addProperty("renders", renders); @@ -176,7 +176,7 @@ static void verify(Minecraft minecraft, JsonObject result) { row.addProperty("blank_label_controls_rejected", 9); row.addProperty("status", "PASS"); } catch (Exception | LinkageError failure) { - inventory.setItem(0, net.minecraft.item.ItemStack.EMPTY); + inventory.setItem(0, net.minecraft.world.item.ItemStack.EMPTY); org.apache.logging.log4j.LogManager.getLogger("advantageclientresources").error("GUI fixture {} failed", id, failure); row.addProperty("status", "FAIL"); row.addProperty("error", failure.toString()); @@ -217,7 +217,7 @@ private static void verifySlots(SimpleMachineGUI.GUIContainer screen, SimpleMach } } - private static void populate(TileEntity tile, float fraction) throws IllegalAccessException { + private static void populate(BlockEntity tile, float fraction) throws IllegalAccessException { if (tile instanceof PoweredEntity) { PoweredEntity powered = (PoweredEntity) tile; for (cyano.poweradvantage.api.ConduitType type : powered.getTypes()) { @@ -230,7 +230,7 @@ private static void populate(TileEntity tile, float fraction) throws IllegalAcce field.setAccessible(true); FluidTank tank = (FluidTank) field.get(tile); if (tank != null) tank.setFluid(fraction == 0 ? null - : new FluidStack(net.minecraft.fluid.Fluids.WATER, (int) (tank.getCapacity() * fraction))); + : new FluidStack(net.minecraft.world.level.material.Fluids.WATER, (int) (tank.getCapacity() * fraction))); } } } @@ -243,30 +243,24 @@ private static BufferedImage render(Minecraft minecraft, RecordingScreen screen, private static BufferedImage render(Minecraft minecraft, RecordingScreen screen, int[] viewport, boolean backgroundOnly, boolean hover) throws Exception { int width = viewport[0] * viewport[2], height = viewport[1] * viewport[2]; - Framebuffer target = new Framebuffer(width, height, true, Minecraft.ON_OSX); + RenderTarget target = new com.mojang.blaze3d.pipeline.TextureTarget(width, height, true, Minecraft.ON_OSX); target.bindWrite(true); - RenderSystem.matrixMode(5889); - RenderSystem.pushMatrix(); RenderSystem.loadIdentity(); - RenderSystem.ortho(0, viewport[0], viewport[1], 0, 1000, 3000); - RenderSystem.matrixMode(5888); - RenderSystem.pushMatrix(); RenderSystem.loadIdentity(); - RenderSystem.translatef(0, 0, -2000); - RenderSystem.enableAlphaTest(); + NativeGuiProjection projection = new NativeGuiProjection(viewport[0], viewport[1], 1000, 3000, -2000); RenderSystem.enableDepthTest(); RenderSystem.depthFunc(515); RenderSystem.disableBlend(); - RenderSystem.color4f(1, 1, 1, 1); + RenderSystem.setShaderColor(1, 1, 1, 1); try { RenderSystem.clearColor(0.12F, 0.18F, 0.23F, 1); RenderSystem.clear(16384 | 256, Minecraft.ON_OSX); if (backgroundOnly) { LabelMode original = screen.labels; screen.labels = LabelMode.HIDDEN; - screen.render(new MatrixStack(), -100, -100, 0F); + screen.render(new PoseStack(), -100, -100, 0F); screen.labels = original; } else { Slot hotbar = screen.getMenu().slots.get(screen.getMenu().slots.size() - 9); - screen.render(new MatrixStack(), screen.hoverTitle ? screen.width / 2 : hover ? screen.getLeft() + hotbar.x + 1 : -100, + screen.render(new PoseStack(), screen.hoverTitle ? screen.width / 2 : hover ? screen.getLeft() + hotbar.x + 1 : -100, screen.hoverTitle ? screen.getTop() + 4 : hover ? screen.getTop() + hotbar.y + 1 : -100, 0F); } ByteBuffer pixels = ByteBuffer.allocateDirect(width * height * 4); @@ -279,13 +273,11 @@ private static BufferedImage render(Minecraft minecraft, RecordingScreen screen, } return image; } finally { - RenderSystem.popMatrix(); - RenderSystem.matrixMode(5889); RenderSystem.popMatrix(); - RenderSystem.matrixMode(5888); + projection.close(); target.destroyBuffers(); minecraft.getMainRenderTarget().bindWrite(true); RenderSystem.enableDepthTest(); - RenderSystem.color4f(1, 1, 1, 1); + RenderSystem.setShaderColor(1, 1, 1, 1); } } @@ -340,10 +332,10 @@ private static int verifyMissingLabelControls(Minecraft minecraft, RecordingScre } private static void verifyLongName(Minecraft minecraft, SimpleMachineGUI definition, SimpleMachineGUI.Container menu, - PlayerInventory inventory) throws Exception { + Inventory inventory) throws Exception { String name = String.join(" / ", java.util.Collections.nCopies(16, "John's oil reserve")); RecordingScreen screen = new RecordingScreen(definition, menu, inventory, - new net.minecraft.util.text.StringTextComponent(name), name, "Inventory"); + new net.minecraft.network.chat.TextComponent(name), name, "Inventory"); screen.init(minecraft, 320, 240); BufferedImage actual = render(minecraft, screen, VIEWPORTS[0], false); BufferedImage hidden = render(minecraft, screen, VIEWPORTS[0], true); @@ -363,10 +355,10 @@ private static void require(boolean value, String message) { } private static void verifyHelperLimits(RecordingScreen screen) throws Exception { - for (String name : new String[]{"zone.moddev.mc.electricadvantage.gui.GUIHelper", - "zone.moddev.mc.steamadvantage.gui.GUIHelper"}) { - if (!net.minecraftforge.fml.ModList.get().isLoaded(name.contains("electricadvantage") ? "electricadvantage" : "steamadvantage")) continue; - String[] methods = name.contains("electricadvantage") ? new String[]{"drawProgressBar"} + for (String name : new String[]{"zone.moddev.mc.baseelectric.gui.GUIHelper", + "zone.moddev.mc.basesteam.gui.GUIHelper"}) { + if (!net.minecraftforge.fml.ModList.get().isLoaded(name.contains("baseelectric") ? "baseelectric" : "basesteam")) continue; + String[] methods = name.contains("baseelectric") ? new String[]{"drawProgressBar"} : new String[]{"drawDownArrowProgress", "drawFlameProgress"}; for (String method : methods) { Method draw = Class.forName(name).getMethod(method, int.class, int.class, float.class, SimpleMachineGUI.GUIContainer.class); @@ -379,13 +371,13 @@ private static void verifyHelperLimits(RecordingScreen screen) throws Exception } } - private static final class GuiWorld extends ClientWorld { - private net.minecraft.block.BlockState state; + private static final class GuiWorld extends ClientLevel { + private net.minecraft.world.level.block.state.BlockState state; private GuiWorld() { - super(null, null, net.minecraft.world.World.OVERWORLD, null, 0, () -> net.minecraft.profiler.EmptyProfiler.INSTANCE, null, false, 0); + super(null, null, net.minecraft.world.level.Level.OVERWORLD, null, 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.block.BlockState getBlockState(BlockPos pos) { return state; } + @Override public net.minecraft.world.level.block.state.BlockState getBlockState(BlockPos pos) { return state; } } private static final class RecordingScreen extends SimpleMachineGUI.GUIContainer { @@ -396,13 +388,13 @@ private static final class RecordingScreen extends SimpleMachineGUI.GUIContainer private final String expectedTitle, expectedInventory; private boolean hoverTitle; private String titleTooltip; - RecordingScreen(SimpleMachineGUI definition, SimpleMachineGUI.Container menu, PlayerInventory inventory, - net.minecraft.util.text.ITextComponent title, String expectedTitle, String expectedInventory) { + RecordingScreen(SimpleMachineGUI definition, SimpleMachineGUI.Container menu, Inventory inventory, + net.minecraft.network.chat.Component title, String expectedTitle, String expectedInventory) { definition.super(menu, inventory, title); this.expectedTitle = expectedTitle; this.expectedInventory = expectedInventory; } - @Override protected void renderLabels(MatrixStack matrices, int x, int y) { + @Override protected void renderLabels(PoseStack matrices, int x, int y) { if (labels == LabelMode.PRODUCTION) { super.renderLabels(matrices, x, y); return; } if (labels == LabelMode.HIDDEN) return; if (labels != LabelMode.INVENTORY_ONLY) { @@ -416,19 +408,19 @@ private static final class RecordingScreen extends SimpleMachineGUI.GUIContainer } if (labels != LabelMode.TITLE_ONLY) font.drawShadow(matrices, expectedInventory, 8, 140, 0xFFFFFF); } - @Override public void render(MatrixStack matrices, int x, int y, float ticks) { + @Override public void render(PoseStack matrices, int x, int y, float ticks) { draws.clear(); tooltipCalls = 0; itemTooltipCalls = 0; titleTooltip = null; super.render(matrices, x, y, ticks); } - @Override protected void renderTooltip(MatrixStack matrices, int x, int y) { + @Override protected void renderTooltip(PoseStack matrices, int x, int y) { tooltipCalls++; super.renderTooltip(matrices, x, y); } - @Override public void renderTooltip(MatrixStack matrices, net.minecraft.item.ItemStack item, int x, int y) { + @Override public void renderTooltip(PoseStack matrices, net.minecraft.world.item.ItemStack item, int x, int y) { itemTooltipCalls++; super.renderTooltip(matrices, item, x, y); } - @Override public void renderTooltip(MatrixStack matrices, net.minecraft.util.text.ITextComponent text, int x, int y) { + @Override public void renderTooltip(PoseStack matrices, net.minecraft.network.chat.Component text, int x, int y) { titleTooltip = text.getString(); super.renderTooltip(matrices, text, x, y); } diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeGuiProjection.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeGuiProjection.java new file mode 100644 index 00000000..1e7d354d --- /dev/null +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeGuiProjection.java @@ -0,0 +1,26 @@ +package zone.moddev.mc.advantageworks.client; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Matrix4f; + +/** Scoped native shader projection for framebuffer-based screen tests. */ +final class NativeGuiProjection implements AutoCloseable { + private final Matrix4f projection = RenderSystem.getProjectionMatrix().copy(); + private final PoseStack modelView = RenderSystem.getModelViewStack(); + + NativeGuiProjection(float width, float height, float near, float far, float depth) { + // Native Matrix4f takes top/bottom, unlike the old OpenGL bottom/top call. + RenderSystem.setProjectionMatrix(Matrix4f.orthographic(0, width, 0, height, near, far)); + modelView.pushPose(); + modelView.setIdentity(); + modelView.translate(0, 0, depth); + RenderSystem.applyModelViewMatrix(); + } + + @Override public void close() { + modelView.popPose(); + RenderSystem.applyModelViewMatrix(); + RenderSystem.setProjectionMatrix(projection); + } +} 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 302e14ba..680e310e 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 @@ -2,28 +2,28 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import net.minecraft.block.BlockState; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferBuilder; import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.client.renderer.texture.AtlasTexture; -import net.minecraft.client.renderer.vertex.DefaultVertexFormats; -import net.minecraft.client.renderer.vertex.VertexFormatElement; -import net.minecraft.fluid.Fluid; -import net.minecraft.fluid.FlowingFluid; -import net.minecraft.fluid.FluidState; -import net.minecraft.item.BucketItem; -import net.minecraft.item.ItemStack; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.Direction; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.IBlockDisplayReader; -import net.minecraft.world.LightType; +import net.minecraft.client.renderer.texture.TextureAtlas; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.VertexFormatElement; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FlowingFluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.item.BucketItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.LightLayer; import net.minecraft.world.level.ColorResolver; -import net.minecraft.world.lighting.WorldLightManager; -import net.minecraft.tileentity.TileEntity; -import zone.moddev.mc.poweradvantage.init.Fluids; -import zone.moddev.mc.poweradvantage.init.Items; +import net.minecraft.world.level.lighting.LevelLightEngine; +import net.minecraft.world.level.block.entity.BlockEntity; +import zone.moddev.mc.basepower.init.Fluids; +import zone.moddev.mc.basepower.init.Items; import java.nio.ByteBuffer; import java.util.HashMap; @@ -36,19 +36,23 @@ 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.ITagCollectionSupplier previousTags = net.minecraft.tags.TagCollectionManager.getInstance(); - net.minecraft.tags.ITagCollection previousFluids = net.minecraft.tags.FluidTags.getAllTags(); - net.minecraft.tags.ITagCollectionSupplier restore = net.minecraft.tags.ITagCollectionSupplier.of( - previousTags.getBlocks(), previousTags.getItems(), previousFluids, previousTags.getEntityTypes()); - net.minecraft.tags.TagRegistry fluidRegistry = net.minecraft.tags.TagRegistryManager.get(new ResourceLocation("fluid")); - Map> tags = new HashMap<>(previousFluids.getAllTags()); - tags.put(new ResourceLocation("minecraft", "water"), net.minecraft.tags.ITag.fromSet( - new java.util.HashSet<>(java.util.Arrays.asList(net.minecraft.fluid.Fluids.WATER, net.minecraft.fluid.Fluids.FLOWING_WATER)))); - tags.put(new ResourceLocation("minecraft", "lava"), net.minecraft.tags.ITag.fromSet( - new java.util.HashSet<>(java.util.Arrays.asList(net.minecraft.fluid.Fluids.LAVA, net.minecraft.fluid.Fluids.FLOWING_LAVA)))); - net.minecraft.tags.ITagCollectionSupplier temporary = net.minecraft.tags.ITagCollectionSupplier.of( - previousTags.getBlocks(), previousTags.getItems(), net.minecraft.tags.ITagCollection.of(tags), previousTags.getEntityTypes()); - net.minecraft.tags.TagCollectionManager.bind(temporary); + 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); @@ -60,21 +64,21 @@ static void verify(Minecraft minecraft, JsonObject result) { JsonArray cases = new JsonArray(); result.add("fluid_rendering", cases); boolean valid = true; - valid &= check(minecraft, Fluids.crude_oil.defaultFluidState(), "poweradvantage:blocks/crude_oil_still", false, cases); - valid &= check(minecraft, Fluids.flowing_crude_oil.getFlowing(5, false), "poweradvantage:blocks/crude_oil_flow", true, cases); - valid &= check(minecraft, Fluids.refined_oil.defaultFluidState(), "poweradvantage:blocks/refined_oil_still", false, cases); - valid &= check(minecraft, Fluids.flowing_refined_oil.getFlowing(5, false), "poweradvantage:blocks/refined_oil_flow", true, cases); - valid &= check(minecraft, net.minecraft.fluid.Fluids.WATER.defaultFluidState(), "minecraft:block/water_still", false, cases); - valid &= check(minecraft, net.minecraft.fluid.Fluids.LAVA.defaultFluidState(), "minecraft:block/lava_still", false, cases); + valid &= check(minecraft, Fluids.crude_oil.defaultFluidState(), "basepower:blocks/crude_oil_still", false, cases); + valid &= check(minecraft, Fluids.flowing_crude_oil.getFlowing(5, false), "basepower:blocks/crude_oil_flow", true, cases); + valid &= check(minecraft, Fluids.refined_oil.defaultFluidState(), "basepower:blocks/refined_oil_still", false, cases); + valid &= check(minecraft, Fluids.flowing_refined_oil.getFlowing(5, false), "basepower:blocks/refined_oil_flow", true, cases); + valid &= check(minecraft, net.minecraft.world.level.material.Fluids.WATER.defaultFluidState(), "minecraft:block/water_still", false, cases); + 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.TagCollectionManager.bind(previousTags); + net.minecraft.tags.SerializationTags.bind(previousTags); require(net.minecraft.tags.FluidTags.getAllTags() == previousFluids, "Synthetic fluid tags were not restored"); } } - private static void requireBucket(net.minecraft.item.Item bucket, Fluid expected) { + 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())); if (!(bucket instanceof BucketItem) || ((BucketItem) bucket).getFluid() != expected @@ -85,25 +89,27 @@ private static void requireBucket(net.minecraft.item.Item bucket, Fluid expected private static boolean check(Minecraft minecraft, FluidState state, String texture, boolean flowing, JsonArray cases) { // A tiny read-only neighbourhood supplies a slope for flowing sprites; no save is opened. - IBlockDisplayReader world = new IBlockDisplayReader() { + BlockAndTintGetter world = new BlockAndTintGetter() { + @Override public int getHeight() { return 256; } + @Override public int getMinBuildHeight() { return 0; } @Override public float getShade(Direction side, boolean shade) { return 1.0F; } - @Override public TileEntity getBlockEntity(BlockPos pos) { return null; } + @Override public BlockEntity getBlockEntity(BlockPos pos) { return null; } @Override public BlockState getBlockState(BlockPos pos) { return blockAt(state, flowing, pos); } @Override public FluidState getFluidState(BlockPos pos) { return getBlockState(pos).getFluidState(); } - @Override public int getBrightness(LightType type, BlockPos pos) { return 15; } - @Override public WorldLightManager getLightEngine() { throw new UnsupportedOperationException("Synthetic light is supplied by getLightFor"); } + @Override public int getBrightness(LightLayer type, BlockPos pos) { return 15; } + @Override public LevelLightEngine getLightEngine() { throw new UnsupportedOperationException("Synthetic light is supplied by getLightFor"); } @Override public int getBlockTint(BlockPos pos, ColorResolver resolver) { - return resolver.getColor(net.minecraft.util.registry.WorldGenRegistries.BIOME.get( - net.minecraft.world.biome.Biomes.PLAINS), pos.getX(), pos.getZ()); + return resolver.getColor(net.minecraft.data.BuiltinRegistries.BIOME.get( + net.minecraft.world.level.biome.Biomes.PLAINS), pos.getX(), pos.getZ()); } }; BufferBuilder buffer = new BufferBuilder(4096); - buffer.begin(7, DefaultVertexFormats.BLOCK); + buffer.begin(com.mojang.blaze3d.vertex.VertexFormat.Mode.QUADS, DefaultVertexFormat.BLOCK); boolean rendered = minecraft.getBlockRenderer().renderLiquid(BlockPos.ZERO, world, buffer, state); buffer.end(); com.mojang.datafixers.util.Pair built = buffer.popNextBuffer(); int vertices = built.getFirst().vertexCount(); - TextureAtlasSprite sprite = minecraft.getTextureAtlas(AtlasTexture.LOCATION_BLOCKS).apply(new ResourceLocation(texture)); + TextureAtlasSprite sprite = minecraft.getTextureAtlas(TextureAtlas.LOCATION_BLOCKS).apply(new ResourceLocation(texture)); require(rendered && vertices >= 4, "Native fluid renderer emitted no top face for " + state); ByteBuffer data = built.getSecond(); int uvOffset = elementOffset(VertexFormatElement.Usage.UV, 0); @@ -126,9 +132,9 @@ private static boolean check(Minecraft minecraft, FluidState state, String textu } private static int elementOffset(VertexFormatElement.Usage usage, int index) { - for (int i = 0; i < DefaultVertexFormats.BLOCK.getElements().size(); i++) { - VertexFormatElement element = DefaultVertexFormats.BLOCK.getElements().get(i); - if (element.getUsage() == usage && element.getIndex() == index) return DefaultVertexFormats.BLOCK.getOffset(i); + for (int i = 0; i < DefaultVertexFormat.BLOCK.getElements().size(); i++) { + VertexFormatElement element = DefaultVertexFormat.BLOCK.getElements().get(i); + if (element.getUsage() == usage && element.getIndex() == index) return DefaultVertexFormat.BLOCK.getOffset(i); } throw new IllegalStateException("Native block vertex format lacks " + usage + " " + index); } @@ -138,7 +144,7 @@ private static BlockState blockAt(FluidState state, boolean flowing, BlockPos po if (flowing && BlockPos.ZERO.east().equals(pos)) { return ((FlowingFluid) state.getType()).getFlowing(3, false).createLegacyBlock(); } - return (pos.getY() < 0 ? net.minecraft.block.Blocks.STONE : net.minecraft.block.Blocks.AIR).defaultBlockState(); + return (pos.getY() < 0 ? net.minecraft.world.level.block.Blocks.STONE : net.minecraft.world.level.block.Blocks.AIR).defaultBlockState(); } private static void require(boolean condition, String message) { 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 b1b8a455..d4d5e2d6 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 @@ -22,17 +22,17 @@ import java.util.stream.Stream; import javax.imageio.ImageIO; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.recipebook.RecipeWidget; -import net.minecraft.client.gui.recipebook.RecipeList; +import net.minecraft.client.gui.screens.recipebook.RecipeButton; +import net.minecraft.client.gui.screens.recipebook.RecipeCollection; import com.mojang.blaze3d.systems.RenderSystem; -import net.minecraft.client.shader.Framebuffer; -import net.minecraft.client.util.ClientRecipeBook; -import net.minecraft.item.crafting.IRecipe; -import net.minecraft.item.crafting.RecipeBook; -import net.minecraft.item.crafting.RecipeItemHelper; -import net.minecraft.item.crafting.RecipeManager; -import net.minecraft.network.PacketBuffer; -import net.minecraft.network.play.server.SUpdateRecipesPacket; +import com.mojang.blaze3d.pipeline.RenderTarget; +import net.minecraft.client.ClientRecipeBook; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.stats.RecipeBook; +import net.minecraft.world.entity.player.StackedContents; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.game.ClientboundUpdateRecipesPacket; /** Decodes actual server recipe packets and exercises Minecraft's real cell grouping. */ final class RecipeBookGroupingProbe { @@ -49,20 +49,20 @@ static void verify(Minecraft minecraft, JsonObject report) { try (Stream files = Files.list(root)) { // A separate main-menu client has not performed Forge's registry handshake. // Import only the generated server fixture's ID snapshot, never a user world. - Map registry = + Map registry = net.minecraftforge.registries.RegistryManager.ACTIVE.takeSnapshot(false); - net.minecraft.nbt.CompoundNBT saved; + net.minecraft.nbt.CompoundTag saved; try (java.io.InputStream input = Files.newInputStream(root.resolve("server-registries.dat"))) { - net.minecraft.nbt.CompoundNBT level = net.minecraft.nbt.CompressedStreamTools.readCompressed(input); - require("1.16.5".equals(level.getCompound("Data").getCompound("Version").getString("Name")), - "Recipe fixture is not a native 1.16.5 server export"); - net.minecraft.nbt.ListNBT mods = level.getCompound("fml").getList("LoadingModList", 10); + 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"); + 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.CompoundNBT mod = mods.getCompound(i); - if ("forge".equals(mod.getString("ModId"))) nativeForge = "36.2.34".equals(mod.getString("ModVersion")); + net.minecraft.nbt.CompoundTag mod = mods.getCompound(i); + if ("forge".equals(mod.getString("ModId"))) nativeForge = "37.1.1".equals(mod.getString("ModVersion")); } - require(nativeForge, "Recipe fixture was not saved by Forge 36.2.34"); + require(nativeForge, "Recipe fixture was not saved by Forge 37.1.1"); saved = level.getCompound("fml").getCompound("Registries"); } String itemRegistry = net.minecraftforge.registries.ForgeRegistries.ITEMS.getRegistryName().toString(); @@ -70,7 +70,7 @@ static void verify(Minecraft minecraft, JsonObject report) { require(saved.contains(itemRegistry, 10) && saved.contains(blockRegistry, 10) && !saved.getCompound(itemRegistry).getList("ids", 10).isEmpty() && !saved.getCompound(blockRegistry).getList("ids", 10).isEmpty(), "Missing native server registry snapshot"); - for (String key : saved.getAllKeys()) registry.put(new net.minecraft.util.ResourceLocation(key), + for (String key : saved.getAllKeys()) registry.put(new net.minecraft.resources.ResourceLocation(key), net.minecraftforge.registries.ForgeRegistry.Snapshot.read(saved.getCompound(key))); require(net.minecraftforge.registries.GameData.injectSnapshot(registry, false, false).isEmpty(), "Client lacks a server registry entry"); @@ -83,20 +83,20 @@ static void verify(Minecraft minecraft, JsonObject report) { Map expected = new HashMap<>(); manifest.getAsJsonArray("bookRecipes").forEach(row -> require(expected.put(row.getAsJsonObject().get("id").getAsString(), row.getAsJsonObject()) == null, "Duplicate recipe in packet manifest")); - SUpdateRecipesPacket packet = new SUpdateRecipesPacket(); - PacketBuffer buffer = new PacketBuffer(Unpooled.wrappedBuffer(Files.readAllBytes(file))); - try { packet.read(buffer); require(!buffer.isReadable(), "Trailing recipe packet bytes"); } + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.wrappedBuffer(Files.readAllBytes(file))); + ClientboundUpdateRecipesPacket packet; + try { packet = new ClientboundUpdateRecipesPacket(buffer); require(!buffer.isReadable(), "Trailing recipe packet bytes"); } finally { buffer.release(); } RecipeManager manager = new RecipeManager(); Set cellKeys = new HashSet<>(); - for (IRecipe recipe : packet.getRecipes()) { + for (Recipe recipe : packet.getRecipes()) { JsonObject row = expected.get(recipe.getId().toString()); require(row != null, "Unexpected network recipe " + recipe.getId()); String output = recipe.getResultItem().getItem().getRegistryName().toString(); require(output.equals(row.get("output").getAsString()) && recipe.getResultItem().getCount() == row.get("count").getAsInt(), "Client/server item registry mismatch for " + recipe.getId()); require(recipe.getGroup().equals(row.get("group").getAsString()) && recipe.getGroup().equals(output), "Group changed in recipe packet"); - String type = net.minecraft.util.registry.Registry.RECIPE_TYPE.getKey(recipe.getType()).toString(); + String type = net.minecraft.core.Registry.RECIPE_TYPE.getKey(recipe.getType()).toString(); require(type.equals(row.get("type").getAsString()), "Recipe type changed in packet"); cellKeys.add(type + "|" + output); } @@ -106,13 +106,13 @@ static void verify(Minecraft minecraft, JsonObject report) { manager.getRecipes().forEach(book::add); book.setupCollections(manager.getRecipes()); int counted = 0; - for (RecipeList cell : book.getCollections()) { + for (RecipeCollection cell : book.getCollections()) { Set outputs = cell.getRecipes().stream().map(recipe -> recipe.getResultItem().getItem().getRegistryName().toString()) .collect(Collectors.toSet()); require(outputs.size() == 1 && cell.hasSingleResultItem(), "Mixed items share a native recipe-book cell: " + outputs); counted += cell.getRecipes().size(); cell.updateKnownRecipes(book); - cell.canCraft(new RecipeItemHelper(), 3, 3, book); + cell.canCraft(new StackedContents(), 3, 3, book); require(cell.getRecipes(false).size() == cell.getRecipes().size(), "Unlocked variants hidden in native cell"); } require(counted == expected.size() && book.getCollections().size() == cellKeys.size(), "Missing or split native cells"); @@ -129,47 +129,42 @@ static void verify(Minecraft minecraft, JsonObject report) { } private static void render(Minecraft minecraft, ClientRecipeBook book) throws Exception { - List shown = new ArrayList<>(); - Set selected = new HashSet<>(java.util.Arrays.asList("electricadvantage:blank_circuit_board", - "electricadvantage:integrated_circuit", "electricadvantage:control_circuit", "electricadvantage:psu", "poweradvantage:item_conveyor")); - for (RecipeList cell : book.getCollections()) { - IRecipe recipe = cell.getRecipes().get(0); + List shown = new ArrayList<>(); + Set selected = new HashSet<>(java.util.Arrays.asList("baseelectric:blank_circuit_board", + "baseelectric:integrated_circuit", "baseelectric:control_circuit", "baseelectric:psu", "basepower:item_conveyor")); + for (RecipeCollection cell : book.getCollections()) { + Recipe recipe = cell.getRecipes().get(0); String id = recipe.getResultItem().getItem().getRegistryName().toString(); - if (selected.contains(id) || id.startsWith("poweradvantage:item_filter_")) shown.add(cell); + if (selected.contains(id) || id.startsWith("basepower:item_filter_")) shown.add(cell); } shown.sort(java.util.Comparator.comparing(cell -> cell.getRecipes().get(0).getResultItem().getItem().getRegistryName().toString())); require(shown.size() == 13, "The thirteen reported circuit/conveyor cells must be separate"); int width = 1000, height = 180; - Framebuffer target = new Framebuffer(width, height, true, Minecraft.ON_OSX); + RenderTarget target = new com.mojang.blaze3d.pipeline.TextureTarget(width, height, true, Minecraft.ON_OSX); target.bindWrite(true); - RenderSystem.matrixMode(GL_PROJECTION); - RenderSystem.pushMatrix(); RenderSystem.loadIdentity(); - RenderSystem.ortho(0, width, height, 0, -1000, 1000); - RenderSystem.matrixMode(GL_MODELVIEW); - RenderSystem.pushMatrix(); RenderSystem.loadIdentity(); + NativeGuiProjection projection = new NativeGuiProjection(width, height, -1000, 1000, 0); RenderSystem.disableDepthTest(); try { RenderSystem.clearColor(0.12F, 0.12F, 0.12F, 1F); RenderSystem.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, Minecraft.ON_OSX); - net.minecraft.inventory.container.WorkbenchContainer menu = new net.minecraft.inventory.container.WorkbenchContainer(0, new net.minecraft.entity.player.PlayerInventory(null)); - require(menu.getRecipeBookType() == net.minecraft.item.crafting.RecipeBookCategory.CRAFTING, "Wrong native crafting menu type"); + net.minecraft.world.inventory.CraftingMenu menu = new net.minecraft.world.inventory.CraftingMenu(0, new net.minecraft.world.entity.player.Inventory(null)); + require(menu.getRecipeBookType() == net.minecraft.world.inventory.RecipeBookType.CRAFTING, "Wrong native crafting menu type"); for (int i = 0; i < shown.size(); i++) { int x = 10 + (i % 5) * 198, y = 12 + (i / 5) * 55; - RecipeWidget button = new RecipeWidget(); + RecipeButton button = new RecipeButton(); // Supply the native menu type without opening a world or connecting a player. - for (Field field : RecipeWidget.class.getDeclaredFields()) { - if (field.getType() == RecipeList.class || field.getType() == RecipeBook.class) { + for (Field field : RecipeButton.class.getDeclaredFields()) { + if (field.getType() == RecipeCollection.class || field.getType() == RecipeBook.class) { field.setAccessible(true); - field.set(button, field.getType() == RecipeList.class ? shown.get(i) : book); - } else if (field.getType() == net.minecraft.inventory.container.RecipeBookContainer.class) { + field.set(button, field.getType() == RecipeCollection.class ? shown.get(i) : book); + } else if (field.getType() == net.minecraft.world.inventory.RecipeBookMenu.class) { field.setAccessible(true); field.set(button, menu); } } button.setPosition(x, y); - button.render(new com.mojang.blaze3d.matrix.MatrixStack(), -1, -1, 1F); - RenderSystem.disableLighting(); - minecraft.font.draw(new com.mojang.blaze3d.matrix.MatrixStack(), shown.get(i).getRecipes().get(0).getResultItem().getDisplayName().getString(), x + 30, y + 8, 0xFFFFFF); + button.render(new com.mojang.blaze3d.vertex.PoseStack(), -1, -1, 1F); + minecraft.font.draw(new com.mojang.blaze3d.vertex.PoseStack(), shown.get(i).getRecipes().get(0).getResultItem().getDisplayName().getString(), x + 30, y + 8, 0xFFFFFF); } ByteBuffer pixels = ByteBuffer.allocateDirect(width * height * 4); RenderSystem.readPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels); @@ -184,8 +179,7 @@ private static void render(Minecraft minecraft, ClientRecipeBook book) throws Ex ImageIO.write(image, "png", new File("recipe-book-cells.png")); require(colored > 1000, "Native recipe buttons rendered blank"); } finally { - RenderSystem.popMatrix(); RenderSystem.matrixMode(GL_PROJECTION); - RenderSystem.popMatrix(); RenderSystem.matrixMode(GL_MODELVIEW); + projection.close(); target.destroyBuffers(); minecraft.getMainRenderTarget().bindWrite(true); RenderSystem.enableDepthTest(); } diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java index a4ed162f..2a5d5b77 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java @@ -2,27 +2,27 @@ import com.google.gson.JsonObject; import cyano.poweradvantage.api.simple.SimpleMachineGUI; -import net.minecraft.block.Block; -import net.minecraft.block.BlockState; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.client.Minecraft; import com.mojang.blaze3d.platform.GlStateManager; import com.mojang.blaze3d.systems.RenderSystem; -import net.minecraft.client.shader.Framebuffer; -import net.minecraft.entity.player.PlayerInventory; -import net.minecraft.fluid.FluidState; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.IBlockReader; -import zone.moddev.mc.poweradvantage.gui.FilteredFluidTankGUI; +import com.mojang.blaze3d.pipeline.RenderTarget; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import zone.moddev.mc.basepower.gui.FilteredFluidTankGUI; import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fluids.capability.CapabilityFluidHandler; import net.minecraftforge.fluids.capability.IFluidHandler; import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction; -import zone.moddev.mc.poweradvantage.registry.MachineGUIRegistry; -import zone.moddev.mc.poweradvantage.init.Blocks; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.MetalTankTileEntity; +import zone.moddev.mc.basepower.registry.MachineGUIRegistry; +import zone.moddev.mc.basepower.init.Blocks; +import zone.moddev.mc.basepower.machines.fluidmachines.MetalTankTileEntity; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; @@ -42,14 +42,16 @@ static void verify(Minecraft minecraft, JsonObject result) { result.addProperty("metal_tank_floor_visible", metalFloor); result.addProperty("portable_tank_floor_visible", portableFloor); - MetalTankTileEntity serverTank = new MetalTankTileEntity(); + MetalTankTileEntity serverTank = new MetalTankTileEntity(BlockPos.ZERO, + zone.moddev.mc.basepower.init.Blocks.metal_storage_tank.defaultBlockState()); IFluidHandler handler = serverTank.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, Direction.UP) .orElseThrow(() -> new IllegalStateException("Metal tank has no native Forge fluid capability")); - require(handler.fill(new FluidStack(net.minecraft.fluid.Fluids.WATER, 10000), FluidAction.SIMULATE) == 10000 + require(handler.fill(new FluidStack(net.minecraft.world.level.material.Fluids.WATER, 10000), FluidAction.SIMULATE) == 10000 && serverTank.getTank().getFluidAmount() == 0, "Native simulated fill changed the tank"); - require(handler.fill(new FluidStack(net.minecraft.fluid.Fluids.WATER, 10000), FluidAction.EXECUTE) == 10000, + require(handler.fill(new FluidStack(net.minecraft.world.level.material.Fluids.WATER, 10000), FluidAction.EXECUTE) == 10000, "Native capability did not fill the tank"); - MetalTankTileEntity clientTank = new MetalTankTileEntity(); + MetalTankTileEntity clientTank = new MetalTankTileEntity(BlockPos.ZERO, + zone.moddev.mc.basepower.init.Blocks.metal_storage_tank.defaultBlockState()); clientTank.onDataPacket(null, serverTank.getUpdatePacket()); result.addProperty("synced_tank_amount", clientTank.getTank().getFluidAmount()); require(clientTank.getTank().getFluidAmount() == 10000, "Filled tank packet lost water"); @@ -67,42 +69,37 @@ static void verify(Minecraft minecraft, JsonObject result) { } private static boolean floorVisible(final BlockState tank) { - IBlockReader world = new IBlockReader() { - @Override public TileEntity getBlockEntity(BlockPos pos) { return null; } + BlockGetter world = new BlockGetter() { + @Override public int getHeight() { return 256; } + @Override public int getMinBuildHeight() { return 0; } + @Override public BlockEntity getBlockEntity(BlockPos pos) { return null; } @Override public BlockState getBlockState(BlockPos pos) { - return pos.equals(BlockPos.ZERO.above()) ? tank : net.minecraft.block.Blocks.GRASS_BLOCK.defaultBlockState(); + return pos.equals(BlockPos.ZERO.above()) ? tank : net.minecraft.world.level.block.Blocks.GRASS_BLOCK.defaultBlockState(); } @Override public FluidState getFluidState(BlockPos pos) { return getBlockState(pos).getFluidState(); } }; - return Block.shouldRenderFace(net.minecraft.block.Blocks.GRASS_BLOCK.defaultBlockState(), world, BlockPos.ZERO, Direction.UP); + return Block.shouldRenderFace(net.minecraft.world.level.block.Blocks.GRASS_BLOCK.defaultBlockState(), world, BlockPos.ZERO, Direction.UP, BlockPos.ZERO.above()); } private static int renderWater(Minecraft minecraft, MetalTankTileEntity tile) { - FilteredFluidTankGUI definition = new FilteredFluidTankGUI(new ResourceLocation("poweradvantage", "textures/gui/container/fluid_metal_tank_gui.png")); + FilteredFluidTankGUI definition = new FilteredFluidTankGUI(new ResourceLocation("basepower", "textures/gui/container/fluid_metal_tank_gui.png")); final int width = 128, height = 96; - PlayerInventory inventory = new PlayerInventory(null); + Inventory inventory = new Inventory(null); SimpleMachineGUI.Container container = definition.createMenu(1, inventory, tile); require(container.getType() == MachineGUIRegistry.TYPE && MachineGUIRegistry.TYPE != null, "Tank GUI is not backed by the registered native container type"); - SimpleMachineGUI.GUIContainer screen = container.createScreen(inventory, new net.minecraft.util.text.StringTextComponent("Tank probe")); + SimpleMachineGUI.GUIContainer screen = container.createScreen(inventory, new net.minecraft.network.chat.TextComponent("Tank probe")); screen.init(minecraft, width, height); require(screen.getMinecraft() == minecraft, "Native tank screen was not initialized"); - tile.getTank().setFluid(new cyano.poweradvantage.api.fluid.FluidStack(net.minecraft.fluid.Fluids.WATER, 10000)); - Framebuffer framebuffer = new Framebuffer(width, height, true, Minecraft.ON_OSX); + tile.getTank().setFluid(new cyano.poweradvantage.api.fluid.FluidStack(net.minecraft.world.level.material.Fluids.WATER, 10000)); + RenderTarget framebuffer = new com.mojang.blaze3d.pipeline.TextureTarget(width, height, true, Minecraft.ON_OSX); framebuffer.bindWrite(true); - RenderSystem.matrixMode(GL_PROJECTION); - RenderSystem.pushMatrix(); - RenderSystem.loadIdentity(); - RenderSystem.ortho(0.0D, width, height, 0.0D, -1.0D, 1.0D); - RenderSystem.matrixMode(GL_MODELVIEW); - RenderSystem.pushMatrix(); - RenderSystem.loadIdentity(); - RenderSystem.disableLighting(); + NativeGuiProjection projection = new NativeGuiProjection(width, height, -1, 1, 0); RenderSystem.disableDepthTest(); RenderSystem.enableBlend(); RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); RenderSystem.enableTexture(); - RenderSystem.color4f(1F, 1F, 1F, 1F); + RenderSystem.setShaderColor(1F, 1F, 1F, 1F); try { RenderSystem.clearColor(0F, 0F, 0F, 1F); RenderSystem.clear(GL_COLOR_BUFFER_BIT, Minecraft.ON_OSX); @@ -122,10 +119,7 @@ private static int renderWater(Minecraft minecraft, MetalTankTileEntity tile) { } catch (IOException failure) { throw new IllegalStateException("Could not retain the native tank gauge image", failure); } finally { - RenderSystem.popMatrix(); - RenderSystem.matrixMode(GL_PROJECTION); - RenderSystem.popMatrix(); - RenderSystem.matrixMode(GL_MODELVIEW); + projection.close(); framebuffer.destroyBuffers(); minecraft.getMainRenderTarget().bindWrite(true); RenderSystem.enableDepthTest(); diff --git a/test-rig/client-resources/resources/META-INF/mods.toml b/test-rig/client-resources/resources/META-INF/mods.toml index 00aead16..faf1ad2a 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="[36,37)" +loaderVersion="[37,38)" license="GPL-3.0-only" [[mods]] @@ -9,22 +9,22 @@ displayName="Advantage Client Resource Probe" description='''Build-only model, texture and translation checks. Never distribute with the mod.''' [[dependencies.advantageclientresources]] -modId="poweradvantage" +modId="basepower" mandatory=true -versionRange="[2.4.7.116051]" +versionRange="[2.4.9.117011]" ordering="AFTER" side="CLIENT" [[dependencies.advantageclientresources]] modId="forge" mandatory=true -versionRange="[36.2.34]" +versionRange="[37.1.1]" ordering="NONE" side="CLIENT" [[dependencies.advantageclientresources]] modId="minecraft" mandatory=true -versionRange="[1.16.5]" +versionRange="[1.17.1]" ordering="NONE" side="CLIENT" diff --git a/test-rig/client-resources/resources/gui-label-expectations.json b/test-rig/client-resources/resources/gui-label-expectations.json index 2f82b82b..bdbb23d8 100644 --- a/test-rig/client-resources/resources/gui-label-expectations.json +++ b/test-rig/client-resources/resources/gui-label-expectations.json @@ -3,44 +3,44 @@ "inventory": "Inventory", "layout": { "height": 240, "machineTop": 12, "machineHeight": 128, "inventoryTop": 150, "titleY": 1, "inventoryLabelY": 140, "inventoryRows": [158, 176, 194, 216] }, "machines": { - "poweradvantage:fluid_drain": "Drain", - "poweradvantage:fluid_discharge": "Discharge Grate", - "poweradvantage:fluid_storage_tank": "Portable Fluid Tank", - "poweradvantage:fluid_metal_tank": "Metal Fluid Tank", - "poweradvantage:still": "Distillation Furnace", - "poweradvantage:item_conveyor": "Conveyor", - "poweradvantage:item_filter_block": "Block Filter Conveyor", - "poweradvantage:item_filter_food": "Food Filter Conveyor", - "poweradvantage:item_filter_fuel": "Fuel Filter Conveyor", - "poweradvantage:item_filter_inventory": "Filter Conveyor", - "poweradvantage:item_filter_ore": "Ore Filter Conveyor", - "poweradvantage:item_filter_plant": "Plantable Filter Conveyor", - "poweradvantage:item_filter_smelt": "Furnace Filter Conveyor", - "poweradvantage:item_filter_overflow": "Overflow Filter Conveyor", - "steamadvantage:steam_boiler_coal": "Coal-Fired Steam Boiler", - "steamadvantage:steam_boiler_electric": "Electric Steam Boiler", - "steamadvantage:steam_boiler_geothermal": "Geothermal Steam Boiler", - "steamadvantage:steam_boiler_oil": "Oil-Burning Steam Boiler", - "steamadvantage:steam_tank": "Steam Boiler Tank", - "steamadvantage:steam_furnace": "Steam-Powered Furnace", - "steamadvantage:steam_crusher": "Steam-Powered Rock Crusher", - "steamadvantage:steam_drill": "Steam-Powered Drill", - "steamadvantage:steam_still": "Steam-Powered Still", - "steamadvantage:steam_pump": "Steam-Powered Pump", - "electricadvantage:electric_battery_array": "Battery Array", - "electricadvantage:electric_crusher": "Electric Rock Crusher", - "electricadvantage:electric_drill": "Electric Mining Drill", - "electricadvantage:electric_fabricator": "Automated Assembler", - "electricadvantage:electric_furnace": "Electric Arc Furnace", - "electricadvantage:electric_oven": "Electric Oven", - "electricadvantage:electric_pump": "Electric Pump", - "electricadvantage:electric_still": "Electric Distiller", - "electricadvantage:growth_chamber": "Growth Chamber", - "electricadvantage:growth_chamber_controller": "Growth Chamber Control Unit", - "electricadvantage:hydroelectric_generator": "Hydroelectric Generator", - "electricadvantage:laser_turret": "Laser Turret", - "electricadvantage:photovoltaic_generator": "Photovoltaic Generator", - "electricadvantage:plastic_refinery": "Petro-Plastic Refinery", - "electricadvantage:steam_powered_generator": "Steam-Powered Electric Generator" + "basepower:fluid_drain": "Drain", + "basepower:fluid_discharge": "Discharge Grate", + "basepower:fluid_storage_tank": "Portable Fluid Tank", + "basepower:fluid_metal_tank": "Metal Fluid Tank", + "basepower:still": "Distillation Furnace", + "basepower:item_conveyor": "Conveyor", + "basepower:item_filter_block": "Block Filter Conveyor", + "basepower:item_filter_food": "Food Filter Conveyor", + "basepower:item_filter_fuel": "Fuel Filter Conveyor", + "basepower:item_filter_inventory": "Filter Conveyor", + "basepower:item_filter_ore": "Ore Filter Conveyor", + "basepower:item_filter_plant": "Plantable Filter Conveyor", + "basepower:item_filter_smelt": "Furnace Filter Conveyor", + "basepower:item_filter_overflow": "Overflow Filter Conveyor", + "basesteam:steam_boiler_coal": "Coal-Fired Steam Boiler", + "basesteam:steam_boiler_electric": "Electric Steam Boiler", + "basesteam:steam_boiler_geothermal": "Geothermal Steam Boiler", + "basesteam:steam_boiler_oil": "Oil-Burning Steam Boiler", + "basesteam:steam_tank": "Steam Boiler Tank", + "basesteam:steam_furnace": "Steam-Powered Furnace", + "basesteam:steam_crusher": "Steam-Powered Rock Crusher", + "basesteam:steam_drill": "Steam-Powered Drill", + "basesteam:steam_still": "Steam-Powered Still", + "basesteam:steam_pump": "Steam-Powered Pump", + "baseelectric:electric_battery_array": "Battery Array", + "baseelectric:electric_crusher": "Electric Rock Crusher", + "baseelectric:electric_drill": "Electric Mining Drill", + "baseelectric:electric_fabricator": "Automated Assembler", + "baseelectric:electric_furnace": "Electric Arc Furnace", + "baseelectric:electric_oven": "Electric Oven", + "baseelectric:electric_pump": "Electric Pump", + "baseelectric:electric_still": "Electric Distiller", + "baseelectric:growth_chamber": "Growth Chamber", + "baseelectric:growth_chamber_controller": "Growth Chamber Control Unit", + "baseelectric:hydroelectric_generator": "Hydroelectric Generator", + "baseelectric:laser_turret": "Laser Turret", + "baseelectric:photovoltaic_generator": "Photovoltaic Generator", + "baseelectric:plastic_refinery": "Petro-Plastic Refinery", + "baseelectric:steam_powered_generator": "Steam-Powered Electric Generator" } } diff --git a/test-rig/client-resources/resources/pack.mcmeta b/test-rig/client-resources/resources/pack.mcmeta index b51c372a..c7bc948a 100644 --- a/test-rig/client-resources/resources/pack.mcmeta +++ b/test-rig/client-resources/resources/pack.mcmeta @@ -1,6 +1,6 @@ { "pack": { "description": "Build-only Advantage client resource checks", - "pack_format": 6 + "pack_format": 7 } } diff --git a/test-rig/client-resources/run.ps1 b/test-rig/client-resources/run.ps1 index 1dae6cc7..72ec23f2 100644 --- a/test-rig/client-resources/run.ps1 +++ b/test-rig/client-resources/run.ps1 @@ -30,39 +30,44 @@ 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]*\.116051"[ \t]*(?:#[^\r\n]*)?\r?$' -or - $Metadata -notmatch '(?m)^loaderVersion\s*=\s*"\[36,') { throw "Not a native Forge 36 candidate: $Id" } - $expected = @{ poweradvantage = '2.4.7.116051'; steamadvantage = '2.2.8.116051'; electricadvantage = '2.2.8.116051' } + $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' } 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.16.5' -or $Environment.FORGE_VERSION -ne '36.2.34' -or - $Environment.MCP_MAPPINGS -ne 'official_1.16.5' -or $Environment.MCP_VERSION -ne '20210115.111550' -or - $Environment.target -ne 'fmluserdevclient') { throw 'Regenerate the native 1.16.5 / Forge 36.2.34 official client launch.' } - if ($Attributes['org.eclipse.jdt.launching.PROGRAM_ARGUMENTS'] -notmatch '1\.16\.5-36\.2\.34' -or - $Attributes['org.eclipse.jdt.launching.PROGRAM_ARGUMENTS'] -match '1\.(?:13\.2|14\.4|15\.2)|--(?:server|port|gameDir|world)\b' -or + 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.' } + $program = $Attributes['org.eclipse.jdt.launching.PROGRAM_ARGUMENTS'] + if ($program -notmatch '1\.17\.1-37\.1\.1' -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 + @([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') { throw 'Unexpected native launch arguments; no server connection or existing game directory is allowed.' } } function Assert-NativeArchive([string]$Path) { - if ($Path -match '(?:forge|client-extra)-1\.(?:13\.2|14\.4|15\.2)|[\\/]PA11[345][\\/]') { throw "Legacy runtime archive: $Path" } + if ($Path -match '(?:forge|client-extra)-1\.(?:13\.2|14\.4|15\.2|16\.5)|[\\/]PA11[3456][\\/]') { 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.16.5'; forge = '36.2.34'; mappings = 'official_1.16.5'; mcp = '20210115.111550'; qualification = 'UNQUALIFIED'; output_source = $OutputSource; launch_profile = $LaunchProfile; require_extensions = [bool]$RequireExtensions; child_renderer_geometry = [bool]$ChildRendererGeometry } +$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.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 8 runtime not found: $java" } -if ((Get-Content -LiteralPath (Join-Path $JavaHome 'release') -Raw) -notmatch '(?m)^JAVA_VERSION="1\.8\.') { throw 'Supply Java 8 for the native client.' } +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 $probe)) { throw 'Run gradlew.bat clientResourceProbeJar verifyEclipseProductionClasspath first.' } [xml]$launch = Get-Content -LiteralPath (Join-Path $project $LaunchProfile) -Raw $attributes = @{} @@ -71,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="\[36,37\)"' -or $probeMetadata -notmatch 'versionRange="\[1\.16\.5\]"' -or - $probeMetadata -notmatch 'versionRange="\[36\.2\.34\]"' -or $probeMetadata -notmatch 'versionRange="\[2\.4\.7\.116051\]"') { throw 'Rebuild the native 1.16 probe; do not substitute a legacy 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.' } Copy-Item -LiteralPath (Join-Path $project $LaunchProfile) -Destination (Join-Path $runtime 'launch.xml') $environment = @{} foreach ($map in $launch.launchConfiguration.mapAttribute) { @@ -81,7 +86,7 @@ foreach ($map in $launch.launchConfiguration.mapAttribute) { } } Assert-NativeLaunch $environment $attributes -$modOutput = Join-Path $runtime 'poweradvantage' +$modOutput = Join-Path $runtime 'basepower' New-Item -ItemType Directory -Path $modOutput,(Join-Path $runtime 'mods') -Force | Out-Null if ($OutputSource -eq 'Eclipse') { Copy-Item -Path (Join-Path $project 'bin/main/*') -Destination $modOutput -Recurse @@ -98,14 +103,14 @@ if (-not (Test-Path -LiteralPath $metadataPath) -or (Get-Content -LiteralPath $m Set-Content -LiteralPath (Join-Path $runtime 'client-resources.json') -Encoding utf8 throw $failure } -Assert-NativeMetadata (Get-Content -LiteralPath $metadataPath -Raw) 'poweradvantage' +Assert-NativeMetadata (Get-Content -LiteralPath $metadataPath -Raw) 'basepower' $outputFiles = @(Get-ChildItem -LiteralPath $modOutput -Recurse -File | Sort-Object FullName | ForEach-Object { Get-Record $_.FullName }) 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/PowerAdvantage-2.4.7.116051-deobf.jar' } +if (-not $PowerCandidate) { $PowerCandidate = Join-Path $project 'build/libs-dev/basePower-2.4.9.117011-deobf.jar' } $PowerCandidate = (Resolve-Path -LiteralPath $PowerCandidate).Path -Assert-NativeMetadata (Read-JarMetadata $PowerCandidate) 'poweradvantage' +Assert-NativeMetadata (Read-JarMetadata $PowerCandidate) 'basepower' $inputReport.power_candidate = Get-Record $PowerCandidate $inputReport.candidate_note = 'Jar provenance only; runtime uses the separately hashed selected production classes/resources.' Copy-Item -LiteralPath $probe -Destination (Join-Path $runtime 'mods/advantageclientresources.jar') @@ -145,7 +150,7 @@ foreach ($archive in $archives) { $record = Get-Record $archive $metadata = if ([IO.Path]::GetExtension($archive) -eq '.jar') { Read-JarMetadata $archive } $modHeader = if ($metadata) { [regex]::Match($metadata, '(?ms)^\[\[mods\]\]\s*(.*?)(?=^\[|\z)').Groups[1].Value } - if ($modHeader -match '(?m)^modId\s*=\s*"(?:steamadvantage|electricadvantage|basemetals|orespawn)"') { + if ($modHeader -match '(?m)^modId\s*=\s*"(?:basesteam|baseelectric|basemetals|orespawn)"') { # Never read mutable sibling build outputs while the client is running. $snapshotDirectory = Join-Path $runtime 'runtime-archives' New-Item -ItemType Directory -Path $snapshotDirectory -Force | Out-Null @@ -156,10 +161,10 @@ foreach ($archive in $archives) { $snapshotRecord.origin_path = $record.path $record = $snapshotRecord } - foreach ($id in 'poweradvantage','steamadvantage','electricadvantage','basemetals','orespawn') { + foreach ($id in 'basepower','basesteam','baseelectric','basemetals','orespawn') { if ($modHeader -match ('(?m)^modId\s*=\s*"' + $id + '"[ \t]*(?:#[^\r\n]*)?\r?$')) { Assert-NativeMetadata $metadata $id - if ($id -eq 'poweradvantage' -or $runtimeCandidates.ContainsKey($id)) { throw "Duplicate runtime candidate: $id" } + if ($id -eq 'basepower' -or $runtimeCandidates.ContainsKey($id)) { throw "Duplicate runtime candidate: $id" } $runtimeCandidates[$id] = $record } } @@ -169,14 +174,14 @@ foreach ($archive in $archives) { $archives = $launchArchives $inputReport.archives = $archiveRecords $inputReport.runtime_candidates = $runtimeCandidates -if (-not ($archives -match 'forge-1\.16\.5-36\.2\.34\.jar$')) { throw 'Missing actual native mapped Forge runtime.' } -if ($RequireExtensions -and (-not $runtimeCandidates.ContainsKey('steamadvantage') -or -not $runtimeCandidates.ContainsKey('electricadvantage'))) { throw 'Combined launch must explicitly contain both native expansion candidates.' } +if (-not ($archives -match 'forge-1\.17\.1-37\.1\.1\.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('"') }) if ($RequireExtensions) { $vmArguments += '-Dadvantageworks.requireExtensions=true' } if ($ChildRendererGeometry) { $vmArguments += '-Dadvantageworks.childRendererGeometry=true' } if ($MachineGuiLayouts) { $vmArguments += '-Dadvantageworks.machineGuiLayouts=true' } -$environment.MOD_CLASSES = "poweradvantage%%$modOutput;poweradvantage%%$modOutput" +$environment.MOD_CLASSES = "basepower%%$modOutput;basepower%%$modOutput" $inputReport.environment = $environment $inputReport.program_arguments = $arguments $inputReport.vm_arguments = $vmArguments @@ -209,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.16.5' -or $result.forge -ne '36.2.34') { throw 'Result is not native 1.16.5 / Forge 36.2.34 evidence.' } +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 ($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 @@ -224,7 +229,7 @@ foreach ($record in $archiveRecords) { if ((Get-FileHash -LiteralPath $record.path).Hash -ne $record.sha256) { throw "Dependency changed during the run: $($record.path)" } } $log = Get-Content -LiteralPath (Join-Path $runtime 'logs/latest.log') -Raw -$namespaces = if ($RequireExtensions) { '(?:poweradvantage|steamadvantage|electricadvantage)' } else { 'poweradvantage' } +$namespaces = if ($RequireExtensions) { '(?:basepower|basesteam|baseelectric)' } else { 'basepower' } if ($log -match "Unable to load model:.*${namespaces}:|Exception loading blockstate definition:.*${namespaces}:|\[FML\.TEXTURE_ERRORS/\]") { throw "Client logged a model or texture error; inspect $runtime/logs/latest.log" } diff --git a/test-rig/client-resources/test-geometry-contract.ps1 b/test-rig/client-resources/test-geometry-contract.ps1 index 47cc0a53..258f53c3 100644 --- a/test-rig/client-resources/test-geometry-contract.ps1 +++ b/test-rig/client-resources/test-geometry-contract.ps1 @@ -1,16 +1,23 @@ +param( + [string]$RepositoryRoot = (Join-Path $PSScriptRoot '../..'), + [string]$TargetRoot = $PSScriptRoot, + [string]$NativeSourcesJar +) $ErrorActionPreference = 'Stop' +$RepositoryRoot = (Resolve-Path -LiteralPath $RepositoryRoot).Path +$TargetRoot = (Resolve-Path -LiteralPath $TargetRoot).Path $checks = 0 function Assert-GeometryContract([bool]$Condition, [string]$Message) { if (-not $Condition) { throw $Message } $script:checks++ } $relative = 'java/zone/moddev/mc/advantageworks/client' -$geometry = Get-Content -LiteralPath (Join-Path $PSScriptRoot "$relative/ChildRendererGeometryProbe.java") -Raw -$main = Get-Content -LiteralPath (Join-Path $PSScriptRoot "$relative/ClientResourceProbe.java") -Raw -$runner = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'run.ps1') -Raw -Assert-GeometryContract ($geometry -notmatch '(?m)^import zone\.moddev\.mc\.(?:steamadvantage|electricadvantage)\.') 'Child renderer probe must not add compile dependencies on the expansions.' -Assert-GeometryContract ($geometry -notmatch 'new (?:DrillBitRenderer|LaserDrillRenderer|LaserTurretRenderer|HydroturbineRenderer)|renderGeometry\(|Unsafe|allocateInstance|new ClientWorld|loadWorld\(') 'Use dispatcher-owned production renderer instances without bypassing construction or loading a world.' -foreach ($fragment in 'TileEntityRendererDispatcher.instance.getRenderer(tile)', 'minecraft.getEntityRenderDispatcher().renderers.get(type)', +$geometry = Get-Content -LiteralPath (Join-Path $TargetRoot "$relative/ChildRendererGeometryProbe.java") -Raw +$main = Get-Content -LiteralPath (Join-Path $TargetRoot "$relative/ClientResourceProbe.java") -Raw +$runner = Get-Content -LiteralPath (Join-Path $TargetRoot 'run.ps1') -Raw +Assert-GeometryContract ($geometry -notmatch '(?m)^import zone\.moddev\.mc\.(?:basesteam|baseelectric)\.') 'Child renderer probe must not add compile dependencies on the expansions.' +Assert-GeometryContract ($geometry -notmatch 'new (?:DrillBitRenderer|LaserDrillRenderer|LaserTurretRenderer|HydroturbineRenderer)|renderGeometry\(|Unsafe|allocateInstance|new Client(?:World|Level)|loadWorld\(') 'Use dispatcher-owned production renderer instances without bypassing construction or loading a world.' +foreach ($fragment in 'minecraft.getBlockEntityRenderDispatcher().getRenderer(tile)', 'minecraft.getEntityRenderDispatcher().renderers.get(type)', 'renderer.render(tile, partial, matrices, buffers', 'buffer.begin(type.mode(), type.format())', 'buffer.popNextBuffer()', 'private static final float EPSILON = 0.0001F;', 'mesh(vertices, 24)', 'mesh(vertices, 16)', 'mesh(vertices, 40)', @@ -31,20 +38,64 @@ Assert-GeometryContract ($geometryCall -gt $main.IndexOf('RecipeBookGroupingProb Assert-GeometryContract ($main.IndexOf('result.addProperty("primary_resource_status", "PASS");') -lt $geometryCall) 'Keep primary resource results separate from the extra renderer scope.' Assert-GeometryContract ($runner.Contains('[switch]$ChildRendererGeometry') -and $runner.Contains('-Dadvantageworks.childRendererGeometry=true')) 'Additional renderer geometry must be explicitly selected.' Assert-GeometryContract ($runner.Contains('if ($ChildRendererGeometry -and -not $RequireExtensions)')) 'Geometry must require the combined scope.' -# Native method names change, but the fixed accepted source hashes and complete -# recipe/tank capture comparisons remain enforced by the primary contract. -& (Join-Path $PSScriptRoot 'test-probe-contract.ps1') +# The primary contract compares all six complete bodies, including this capture, +# against SHA-bound accepted 1.16 Git blobs with exact native API adapters only. +& (Join-Path $PSScriptRoot 'test-probe-contract.ps1') -RepositoryRoot $RepositoryRoot -TargetRoot $TargetRoot $checks++ -$reference = Join-Path $PSScriptRoot "../../../../PA115/PowerAdvantage/test-rig/client-resources/$relative/ChildRendererGeometryProbe.java" -Assert-GeometryContract ((Get-FileHash -LiteralPath $reference).Hash -eq 'A125FEC9F7322B019887FB240316411DBDF25173501CFA66F8F6816D536E4E03') 'The accepted renderer reference changed.' -$source = Get-Content -LiteralPath $reference -Raw -$source = $source.Replace('minecraft.getRenderManager()', 'minecraft.getEntityRenderDispatcher()'). - Replace('type.getDrawMode()', 'type.mode()').Replace('type.getVertexFormat()', 'type.format()'). - Replace('DefaultVertexFormats.ENTITY', 'DefaultVertexFormats.NEW_ENTITY'). - Replace('minecraft.world == null', 'minecraft.level == null'). - Replace('minecraft.isIntegratedServerRunning()', 'minecraft.hasSingleplayerServer()'). - Replace('tile.getWorld()', 'tile.getLevel()') -foreach ($assertion in [regex]::Matches($source, '(?s)\brequire\(.*?\);')) { - Assert-GeometryContract ($geometry.Contains($assertion.Value)) "Renderer oracle changed: $($assertion.Value)" +foreach ($fragment in 'buffer.end();', 'int count = built.getFirst().vertexCount();', + 'ByteBuffer data = built.getSecond();', 'int base = i * format.getVertexSize();', + 'data.getFloat(base + p)', 'data.getFloat(base + uv)', 'data.get(base + n) / 127F', + '(data.get(base + c + 3) & 255) / 255F', 'vertices.add(vertex);', + 'valueOfType(entry.getKey(), RenderType.CompositeState.class)', + 'valueOfType(state, RenderStateShard.EmptyTextureStateShard.class)', + 'expectedTexture.equals(selectedTexture)', 'type::isValid', + 'type.create(net.minecraft.core.BlockPos.ZERO, state)') { + Assert-GeometryContract ($geometry.Contains($fragment)) "Missing actual native primitive/factory capture: $fragment" +} +if ($NativeSourcesJar) { + # This optional local source audit does not start Java or Gradle. Its hash is + # bound to the official 1.17.1 mappings used by the native client candidate. + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [IO.Compression.ZipFile]::OpenRead((Resolve-Path -LiteralPath $NativeSourcesJar).Path) + try { + $entry = $archive.GetEntry('com/mojang/math/Matrix4f.java') + Assert-GeometryContract ($null -ne $entry) 'Native source archive lacks Matrix4f.java.' + $reader = [IO.StreamReader]::new($entry.Open()) + try { $source = $reader.ReadToEnd() } finally { $reader.Dispose() } + } finally { $archive.Dispose() } + $sha = [Security.Cryptography.SHA256]::Create() + try { $hash = [BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($source))).Replace('-', '') } + finally { $sha.Dispose() } + Assert-GeometryContract ($hash -ceq '16B1F18F1B31925EACAE023AA952B5ED4B0314F90F3894816FFD27FC8FD46BB1') 'Native Matrix4f source hash changed; re-establish API equivalence, do not rebaseline.' + foreach ($formula in 'float f = p_162205_ - p_162204_;', 'float f1 = p_162206_ - p_162207_;', + 'float f2 = p_162209_ - p_162208_;', 'matrix4f.m00 = 2.0F / f;', + 'matrix4f.m11 = 2.0F / f1;', 'matrix4f.m22 = -2.0F / f2;', + 'matrix4f.m03 = -(p_162205_ + p_162204_) / f;', + 'matrix4f.m13 = -(p_162206_ + p_162207_) / f1;', + 'matrix4f.m23 = -(p_162209_ + p_162208_) / f2;') { + Assert-GeometryContract ($source.Contains($formula)) "Native projection formula changed: $formula" + } + # Old GL ortho(left,right,bottom,top,near,far) versus native + # orthographic(left,right,top,bottom,near,far), using every probe viewport. + foreach ($view in @(@(128,96,-1,1,0), @(1000,180,-1000,1000,0), + @(320,240,1000,3000,-2000), @(426,240,1000,3000,-2000), @(640,360,1000,3000,-2000))) { + $width,$height,$near,$far,$depth = $view + $oldArgs = @(0,$width,$height,0,$near,$far) + $newArgs = @(0,$width,0,$height,$near,$far) + $gl = @((2.0/($oldArgs[1]-$oldArgs[0])), (2.0/($oldArgs[3]-$oldArgs[2])), (-2.0/($oldArgs[5]-$oldArgs[4])), + (-($oldArgs[1]+$oldArgs[0])/($oldArgs[1]-$oldArgs[0])), (-($oldArgs[3]+$oldArgs[2])/($oldArgs[3]-$oldArgs[2])), + (-($oldArgs[5]+$oldArgs[4])/($oldArgs[5]-$oldArgs[4]))) + $native = @((2.0/($newArgs[1]-$newArgs[0])), (2.0/($newArgs[2]-$newArgs[3])), (-2.0/($newArgs[5]-$newArgs[4])), + (-($newArgs[1]+$newArgs[0])/($newArgs[1]-$newArgs[0])), (-($newArgs[2]+$newArgs[3])/($newArgs[2]-$newArgs[3])), + (-($newArgs[5]+$newArgs[4])/($newArgs[5]-$newArgs[4]))) + Assert-GeometryContract (($gl -join ',') -ceq ($native -join ',')) 'Native projection differs from accepted GL coefficients.' + Assert-GeometryContract ([Math]::Abs($native[1]*0+$native[4]-1) -lt 1e-9 -and + [Math]::Abs($native[1]*$height+$native[4]+1) -lt 1e-9 -and + [Math]::Abs($native[0]*$width+$native[3]-1) -lt 1e-9) 'Probe corners must retain their downward Y axis and full viewport.' + Assert-GeometryContract ([Math]::Abs($native[2]*$depth+$native[5]-($gl[2]*$depth+$gl[5])) -lt 1e-9) 'Native model-view translation changed the capture depth.' + } + Write-Output "PASS: SHA-bound native Matrix4f top/bottom adapter proof, five projection/depth configurations. Source: $NativeSourcesJar" +} else { + Write-Output 'NOT RUN: engine-source projection audit; supply -NativeSourcesJar with the prepared official-mappings 1.17.1 source archive.' } Write-Output "PASS: $checks static additional-geometry wiring and primary-oracle preservation checks. No renderer, Java or client was run." diff --git a/test-rig/client-resources/test-launcher.ps1 b/test-rig/client-resources/test-launcher.ps1 index 6fdcf813..360dc644 100644 --- a/test-rig/client-resources/test-launcher.ps1 +++ b/test-rig/client-resources/test-launcher.ps1 @@ -76,32 +76,33 @@ if ($CrossVersionEvidenceRoots) { $native = @' modLoader="javafml" -loaderVersion="[36,)" # native Forge +loaderVersion="[37,)" # native Forge [[mods]] # mandatory header -modId="poweradvantage" # inline comment -version="2.4.7.116051" # native candidate -[[dependencies.poweradvantage]] +modId="basepower" # inline comment +version="2.4.9.117011" # native candidate +[[dependencies.basepower]] modId="minecraft" mandatory=true -versionRange="[1.16.5]" +versionRange="[1.17.1]" '@ -Assert-NativeMetadata $native 'poweradvantage' +Assert-NativeMetadata $native 'basepower' $checks++ -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.7.116051','2.4.4.113021')) 'poweradvantage' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.7.116051','2.4.5.114041')) 'poweradvantage' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.7.116051','2.4.5.115021')) 'poweradvantage' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('[36,)','[31,)')) 'poweradvantage' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.7.116051','${version}')) 'poweradvantage' } -Assert-Throws { Assert-NativeMetadata $native 'electricadvantage' } -Assert-Throws { Assert-NativeMetadata '' 'poweradvantage' } -foreach ($id in 'steamadvantage','electricadvantage') { - $expansion = $native.Replace('poweradvantage', $id).Replace('2.4.7.116051','2.2.8.116051') +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 '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') Assert-NativeMetadata $expansion $id $checks++ - Assert-Throws { Assert-NativeMetadata ($expansion.Replace('2.2.8.116051','2.2.6.115021')) $id } + Assert-Throws { Assert-NativeMetadata ($expansion.Replace('2.2.10.117011','2.2.6.115021')) $id } } -$environment = @{ MC_VERSION = '1.16.5'; FORGE_VERSION = '36.2.34'; MCP_MAPPINGS = 'official_1.16.5'; MCP_VERSION = '20210115.111550'; target = 'fmluserdevclient' } -$attributes = @{ 'org.eclipse.jdt.launching.PROGRAM_ARGUMENTS' = '--main net.minecraftforge.userdev.LaunchTesting --cache D:\native\1.16.5-36.2.34 --'; 'org.eclipse.jdt.launching.MAIN_TYPE' = 'net.minecraftforge.launcher.Main' } +$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' } Assert-NativeLaunch $environment $attributes $checks++ foreach ($key in 'MC_VERSION','FORGE_VERSION','MCP_MAPPINGS','MCP_VERSION','target') { @@ -117,9 +118,9 @@ 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.16.5-36.2.34.jar' +Assert-NativeArchive 'D:\native\forge-1.17.1-37.1.1.jar' $checks++ -foreach ($path in 'D:\PA114\client.jar','D:\PA113\client.jar','D:\native\forge-1.14.4-28.2.26.jar','D:\native\client-extra-1.13.2.jar') { +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 } } if ((Quote-Argument 'D:\native path\') -ne '"D:\native path\\"') { throw 'Trailing-backslash command-line quoting changed.' } @@ -134,8 +135,8 @@ if ($StaticOnly) { return } $project = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path -$power = Join-Path $project 'build/libs-dev/PowerAdvantage-2.4.7.116051-deobf.jar' -Assert-NativeMetadata (Read-JarMetadata $power) 'poweradvantage' +$power = Join-Path $project 'build/libs-dev/basePower-2.4.9.117011-deobf.jar' +Assert-NativeMetadata (Read-JarMetadata $power) 'basepower' $checks++ [xml]$launch = Get-Content -LiteralPath (Join-Path $project 'runClientAllAdvantage.launch') -Raw $found = @{} @@ -145,7 +146,7 @@ foreach ($entry in ($launch.launchConfiguration.listAttribute | Where-Object { $ if (-not $path -or [IO.Path]::GetExtension($path) -ne '.jar') { continue } $metadata = Read-JarMetadata $path $header = if ($metadata) { [regex]::Match($metadata, '(?ms)^\[\[mods\]\]\s*(.*?)(?=^\[|\z)').Groups[1].Value } - foreach ($id in 'basemetals','orespawn','steamadvantage','electricadvantage') { + foreach ($id in 'basemetals','orespawn','basesteam','baseelectric') { if ($header -match ('(?m)^modId\s*=\s*"' + $id + '"[ \t]*(?:#[^\r\n]*)?\r?$')) { Assert-NativeMetadata $metadata $id $found[$id] = $true diff --git a/test-rig/client-resources/test-probe-contract-negative-controls.ps1 b/test-rig/client-resources/test-probe-contract-negative-controls.ps1 index 38d9c807..6ad3d5a8 100644 --- a/test-rig/client-resources/test-probe-contract-negative-controls.ps1 +++ b/test-rig/client-resources/test-probe-contract-negative-controls.ps1 @@ -1,29 +1,76 @@ +param([string]$EvidenceRoot) $ErrorActionPreference = 'Stop' $project = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path -$reference = (Resolve-Path (Join-Path $PSScriptRoot '../../../../PA115/PowerAdvantage/test-rig/client-resources')).Path -$output = Join-Path $project ('build/client-probe-negative-controls/' + [guid]::NewGuid().ToString('N')) +$outputRoot = if ($EvidenceRoot) { [IO.Path]::GetFullPath($EvidenceRoot) } + else { Join-Path $project 'run-client-probe-contract-controls' } +$output = Join-Path $outputRoot ((Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $output | Out-Null $relative = 'java/zone/moddev/mc/advantageworks/client' $controls = @( @{ name='recipe-pixel-threshold'; file='RecipeBookGroupingProbe'; before='colored > 1000'; after='colored > 100' }, + @{ name='recipe-cell-count'; file='RecipeBookGroupingProbe'; before='shown.size() == 13'; after='shown.size() >= 1' }, + @{ name='recipe-packet-tail'; file='RecipeBookGroupingProbe'; before='require(!buffer.isReadable(),'; after='require(true,' }, @{ name='tank-packet-quantity'; file='TankVisualProbe'; before='clientTank.getTank().getFluidAmount() == 10000'; after='clientTank.getTank().getFluidAmount() == 1000' }, + @{ name='tank-simulation-mutation'; file='TankVisualProbe'; before='serverTank.getTank().getFluidAmount() == 0'; after='serverTank.getTank().getFluidAmount() >= 0' }, + @{ name='tank-pixel-coordinate'; file='TankVisualProbe'; before='image.getRGB(88, 39)'; after='image.getRGB(0, 0)' }, + @{ name='tank-blue-threshold'; file='TankVisualProbe'; before='blue > red + 20 && blue > green + 10'; after='blue >= 0' }, @{ name='bucket-volume'; file='OilRenderProbe'; before='contents.getAmount() != 1000'; after='contents.getAmount() != 100' }, - @{ name='missing-model'; file='ClientResourceProbe'; before='require(model != missing,'; after='require(model == missing,' } + @{ name='bucket-fluid-identity'; file='OilRenderProbe'; before='contents.getFluid() != expected'; after='false' }, + @{ name='fluid-primitive-capture'; file='OilRenderProbe'; before='int vertices = built.getFirst().vertexCount();'; after='int vertices = 4;' }, + @{ name='fluid-tags-restore'; file='OilRenderProbe'; before='fluidRegistry.reset(restore);'; after='fluidRegistry.reset(temporary);' }, + @{ name='missing-model'; file='ClientResourceProbe'; before='require(model != missing,'; after='require(model == missing,' }, + @{ name='missing-model-quads'; file='ClientResourceProbe'; before='!requireQuads || !quads.isEmpty()'; after='true' }, + @{ name='missing-caption'; file='MachineGuiLayoutProbe'; before='require(labelError == null, labelError);'; after='require(true, labelError);' }, + @{ name='caption-negative-control'; file='MachineGuiLayoutProbe'; before='require(labelError(background, expected, background, screen, viewport[2]) != null,'; after='require(true,' }, + @{ name='caption-viewport'; file='MachineGuiLayoutProbe'; before='{{320, 240, 1}, {426, 240, 2}, {640, 360, 1}}'; after='{{320, 240, 1}}' }, + @{ name='geometry-count'; file='ChildRendererGeometryProbe'; before='require(vertices.size() == count,'; after='require(vertices.size() >= 0,' }, + @{ name='geometry-epsilon'; file='ChildRendererGeometryProbe'; before='EPSILON = 0.0001F'; after='EPSILON = 0.1F' }, + @{ name='geometry-degenerate-threshold'; file='ChildRendererGeometryProbe'; before='x * x + y * y + z * z > 0.00000001F'; after='x * x + y * y + z * z >= 0F' }, + @{ name='geometry-primitive-capture'; file='ChildRendererGeometryProbe'; before='data.getFloat(base + p)'; after='0F' }, + @{ name='geometry-native-buffer-import'; file='ChildRendererGeometryProbe'; before='import com.mojang.blaze3d.vertex.BufferBuilder;'; after='import fake.BufferBuilder;'; error='ChildRendererGeometryProbe import bindings changed' }, + @{ name='geometry-actual-texture'; file='ChildRendererGeometryProbe'; before='expectedTexture.equals(selectedTexture)'; after='true' }, + @{ name='geometry-matrix-balance'; file='ChildRendererGeometryProbe'; before='position.equals(original.pose()) && normal.equals(original.normal())'; after='true' }, + @{ name='geometry-manual-gap'; file='ChildRendererGeometryProbe'; before='passed == 25 && gaps == 1'; after='passed >= 0' }, + @{ name='projection-y-order'; file='NativeGuiProjection'; before='orthographic(0, width, 0, height, near, far)'; after='orthographic(0, width, height, 0, near, far)'; error='Native projection adapter changed' }, + @{ name='projection-restoration'; file='NativeGuiProjection'; before='RenderSystem.setProjectionMatrix(projection);'; after=''; error='Native projection adapter changed' }, + @{ name='runner-caption-count'; path='run.ps1'; before='{ 351 } else { 126 }'; after='{ 350 } else { 126 }'; error='Runner must require all 351' }, + @{ name='runner-negative-count'; path='run.ps1'; before='($expectedGuiTextChecks + 4)'; after='($expectedGuiTextChecks + 3)'; error='Runner must require all 351' } ) $results = @() -foreach ($control in $controls) { - $fixture = Join-Path $output $control.name +$inputs = @{} +foreach ($file in Get-ChildItem -LiteralPath $PSScriptRoot -File -Recurse) { + $inputs[$file.FullName] = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash +} +try { + # A failing prerequisite must never masquerade as a successful negative control. + & (Join-Path $PSScriptRoot 'test-probe-contract.ps1') -RepositoryRoot $project + $fixture = Join-Path $output 'valid-fixture' Copy-Item -LiteralPath $PSScriptRoot -Destination $fixture -Recurse - $path = Join-Path $fixture "$relative/$($control.file).java" - $source = [IO.File]::ReadAllText($path) - if (-not $source.Contains($control.before)) { throw "Negative control has no exact mutation point: $($control.name)" } - [IO.File]::WriteAllText($path, $source.Replace($control.before, $control.after), [Text.UTF8Encoding]::new($false)) - $rejected = $false - $failure = $null - try { & (Join-Path $fixture 'test-probe-contract.ps1') -ReferenceRoot $reference | Out-Null } - catch { $rejected = $true; $failure = $_.Exception.Message } - $results += [ordered]@{ control=$control.name; expected='REJECT'; rejected=$rejected; error=$failure } - if (-not $rejected) { throw "A weakened oracle was accepted: $($control.name). Evidence: $output" } + & (Join-Path $PSScriptRoot 'test-probe-contract.ps1') -RepositoryRoot $project -TargetRoot $fixture + foreach ($control in $controls) { + $fixture = Join-Path $output $control.name + Copy-Item -LiteralPath $PSScriptRoot -Destination $fixture -Recurse + $path = if ($control.path) { Join-Path $fixture $control.path } + else { Join-Path $fixture "$relative/$($control.file).java" } + $source = [IO.File]::ReadAllText($path) + $mutationCount = [regex]::Matches($source, [regex]::Escape($control.before)).Count + if ($mutationCount -ne 1) { throw "Negative control needs exactly one mutation point: $($control.name), got $mutationCount" } + # Generated mutants only; actual probes, reference blobs and worlds are untouched. + [IO.File]::WriteAllText($path, $source.Replace($control.before, $control.after), [Text.UTF8Encoding]::new($false)) + $expected = if ($control.error) { $control.error } else { "$($control.file) changed beyond explicit native adapters" } + $rejected = $false + $failure = $null + try { & (Join-Path $PSScriptRoot 'test-probe-contract.ps1') -RepositoryRoot $project -TargetRoot $fixture | Out-Null } + catch { $failure = $_.Exception.Message; $rejected = $failure.StartsWith($expected, [StringComparison]::Ordinal) } + $results += [ordered]@{ control=$control.name; expected='REJECT_FOR_INTENDED_REASON'; rejected=$rejected; error=$failure } + if (-not $rejected) { throw "Weakened oracle was accepted or rejected for an unrelated error: $($control.name). Actual: $failure. Evidence: $output" } + } + foreach ($path in $inputs.Keys) { + if ((Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash -cne $inputs[$path]) { throw "Actual probe source changed during controls: $path" } + } +} finally { + [ordered]@{ scope='STATIC_CONTRACT_MUTATIONS_NOT_RENDERED_CLIENT'; baseline='c310d0ca63ad9a8408dd4bba75172afafa5c71b7'; + planned=$controls.Count; completed=$results.Count; inputs=$inputs; results=$results } | + ConvertTo-Json -Depth 7 | Set-Content -LiteralPath (Join-Path $output 'results.json') -Encoding utf8 } -$results | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'results.json') -Encoding utf8 -Write-Output "PASS: $($controls.Count) weakened-oracle controls rejected. Original source and worlds were not changed. Evidence: $output" +Write-Output "PASS: $($controls.Count) weakened-oracle controls rejected for their intended reasons; unmodified fixture passed. No Java/client was run; source and worlds unchanged. Evidence: $output" diff --git a/test-rig/client-resources/test-probe-contract.ps1 b/test-rig/client-resources/test-probe-contract.ps1 index a3c4d2e5..cbdbf362 100644 --- a/test-rig/client-resources/test-probe-contract.ps1 +++ b/test-rig/client-resources/test-probe-contract.ps1 @@ -1,116 +1,299 @@ -param([string]$ReferenceRoot = (Join-Path $PSScriptRoot '../../../../PA115/PowerAdvantage/test-rig/client-resources')) +param( + [string]$RepositoryRoot = (Join-Path $PSScriptRoot '../..'), + [string]$TargetRoot = $PSScriptRoot +) $ErrorActionPreference = 'Stop' -$ReferenceRoot = (Resolve-Path -LiteralPath $ReferenceRoot).Path -$referenceHashes = @{ - ClientResourceProbe = 'CA78566EB69CF10C420856D9D748477361CF8DD1BD9E49A915AE1122C572C73E' - TankVisualProbe = '756DE3330EDF1A9E649A2CE06BEFD7AC4A1C5E39E4443EE69A0AFE1D33A1807F' - OilRenderProbe = '3560449BA7DBB2AD60CDEF4FEB5A7DCB53EF66A0AA9A82EC538F4D1993CD8ECE' - RecipeBookGroupingProbe = 'C9E02B30693C7CCFA024290DE4B286E707BE5DD9BC07633704437FEA4E660376' +$RepositoryRoot = (Resolve-Path -LiteralPath $RepositoryRoot).Path +$TargetRoot = (Resolve-Path -LiteralPath $TargetRoot).Path +$baseline = 'c310d0ca63ad9a8408dd4bba75172afafa5c71b7' +$relative = 'java/zone/moddev/mc/advantageworks/client' +$referenceHashes = [ordered]@{ + ClientResourceProbe = '014E12FFFD8B1E52FF32FA55ABCD6A1708094AF00D85AFB2F1424D448E349453' + TankVisualProbe = '666F6D8FC5F1B925665E019E4F38DAB90766A04CB64C9C256AD3BAC9BE6CF327' + OilRenderProbe = 'E99F2D29D58F5388406FB29F419D68FF02C3C9EB51A44018C9A3440E83F37777' + RecipeBookGroupingProbe = 'E30466B80CB0496E637B3C6AAC9D490EF55C8E0FE0EBC5ACAEF7EF90D8E9C1B1' + MachineGuiLayoutProbe = '8D89215AEFFDDC96A7175AE84F315DFF009D6E0F0FC1942C986E626B5806AFFF' + ChildRendererGeometryProbe = 'B56A5F683FCC1312D3B749BEC11C57099ECB73E5C87C953EF55050EAE3320650' } $checks = 0 function Assert-Contract([bool]$Condition, [string]$Message) { if (-not $Condition) { throw $Message } $script:checks++ } -function Normalize-Probe([string]$Source) { - # The accepted 1.15 source is immutable. Only these platform API spellings - # are equivalent; conditions, quantities, bounds and thresholds are retained. - $pairs = [ordered]@{ - '1.15.2'='1.16.5'; '31.2.57'='36.2.34'; - '2.4.6.115021'='2.4.7.116051'; '2.2.7.115021'='2.2.8.116051'; - 'IFluidState'='FluidState'; 'getTileEntity('='getBlockEntity('; - '.getDefaultState()'='.defaultBlockState()'; '.getFilledBucket()'='.getBucket()'; - '.getFlowingFluidState('='.getFlowing('; 'state.getFluid()'='state.getType()'; - '.getDescriptionId('=' .getDescriptionId('; '.getTranslationKey('=' .getDescriptionId('; - 'Block.shouldSideBeRendered('='Block.shouldRenderFace('; 'BlockPos.ZERO.up()'='BlockPos.ZERO.above()'; - '.getLoadingGui()'='.getOverlay()'; '.isIntegratedServerRunning()'='.hasSingleplayerServer()'; - 'I18n.format('='I18n.get('; '.getItemModelWithOverrides('='.getModel('; - '.getStateContainer()'='.getStateDefinition()'; '.getValidStates()'='.getPossibleStates()'; - '.getBlockModelShapes().getModel('='.getBlockModelShaper().getBlockModel('; - 'tab_powerAdvantage.fill('='tab_powerAdvantage.fillItemList('; 'tab_powerAdvantage.getIcon()'='tab_powerAdvantage.getIconItem()'; - 'minecraft.currentScreen'='minecraft.screen'; 'minecraft.shutdown()'='minecraft.stop()'; - '.getParticleTexture()'='.getParticleTexture(net.minecraftforge.client.model.data.EmptyModelData.INSTANCE)'; - 'quad.func_187508_a()'='quad.getSprite()'; - '.getAtlasSpriteGetter('='.getTextureAtlas('; 'AtlasTexture.LOCATION_BLOCKS_TEXTURE'='AtlasTexture.LOCATION_BLOCKS'; - '.getMinU()'='.getU0()'; '.getMaxU()'='.getU1()'; '.getMinV()'='.getV0()'; '.getMaxV()'='.getV1()'; - 'state.getBlockState()'='state.createLegacyBlock()'; - '.finishDrawing()'='.end()'; '.getNextBuffer()'='.popNextBuffer()'; '.getVertexCount()'='.vertexCount()'; - 'saved.keySet()'='saved.getAllKeys()'; '.readPacketData('='.read('; '.getRecipeOutput()'='.getResultItem()'; - 'manager.func_223389_a('='manager.replaceRecipes('; 'new ClientRecipeBook(manager)'='new ClientRecipeBook()'; - 'book::unlock'='book::add'; 'book.rebuildTable()'='book.setupCollections(manager.getRecipes())'; - 'book.getRecipes()'='book.getCollections()'; 'Minecraft.IS_RUNNING_ON_MAC'='Minecraft.ON_OSX'; - '.bindFramebuffer('='.bindWrite('; '.deleteFramebuffer()'='.destroyBuffers()'; 'minecraft.getFramebuffer()'='minecraft.getMainRenderTarget()'; - 'button.render(-1, -1, 1F)'='button.render(new com.mojang.blaze3d.matrix.MatrixStack(), -1, -1, 1F)'; - 'minecraft.fontRenderer.drawString('='minecraft.font.draw(new com.mojang.blaze3d.matrix.MatrixStack(), '; - '.getFormattedText()'='.getString()'; 'createContainer('='createMenu(' - } - $Source = $Source.Replace("`r`n", "`n") - foreach ($pair in $pairs.GetEnumerator()) { $Source = $Source.Replace($pair.Key, $pair.Value) } - $Source = [regex]::Replace($Source, '(? previousFluids = net.minecraft.tags.FluidTags.getAllTags(); +net.minecraft.tags.ITagCollectionSupplier restore = net.minecraft.tags.ITagCollectionSupplier.of(previousTags.getBlocks(), previousTags.getItems(), previousFluids, previousTags.getEntityTypes()); +net.minecraft.tags.TagRegistry fluidRegistry = net.minecraft.tags.TagRegistryManager.get(new ResourceLocation("fluid")); +'@ @' +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")); +'@ 'native fluid tag snapshot' + $source = $source.Replace('net.minecraft.tags.ITag<', 'net.minecraft.tags.Tag<').Replace('net.minecraft.tags.ITag.fromSet(', 'net.minecraft.tags.Tag.fromSet(') + $source = Adapt-Exact $source 'net.minecraft.tags.ITagCollectionSupplier temporary = net.minecraft.tags.ITagCollectionSupplier.of(previousTags.getBlocks(), previousTags.getItems(), net.minecraft.tags.ITagCollection.of(tags), previousTags.getEntityTypes());' @' +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); +'@ 'native isolated fluid tags' + $source = $source.Replace('net.minecraft.tags.TagCollectionManager.bind(', 'net.minecraft.tags.SerializationTags.bind(') + $source = Adapt-Exact $source 'new BlockAndTintGetter() {' 'new BlockAndTintGetter() { @Override public int getHeight() { return 256; } @Override public int getMinBuildHeight() { return 0; }' 'native fluid height' + $source = Adapt-Exact $source 'buffer.begin(7, DefaultVertexFormat.BLOCK);' 'buffer.begin(com.mojang.blaze3d.vertex.VertexFormat.Mode.QUADS, DefaultVertexFormat.BLOCK);' 'native fluid quad enum' + } + MachineGuiLayoutProbe { + $source = Adapt-Exact $source 'block.createTileEntity(block.defaultBlockState(), null)' '((net.minecraft.world.level.block.EntityBlock)block).newBlockEntity(BlockPos.ZERO, block.defaultBlockState())' 'native GUI tile factory' + $source = Adapt-Exact $source 'tile.setLevelAndPosition(world, BlockPos.ZERO);' 'tile.setLevel(world);' 'native GUI world assignment' + $source = Adapt-Exact $source 'RenderSystem.matrixMode(5889); RenderSystem.pushMatrix(); RenderSystem.loadIdentity(); RenderSystem.ortho(0, viewport[0], viewport[1], 0, 1000, 3000); RenderSystem.matrixMode(5888); RenderSystem.pushMatrix(); RenderSystem.loadIdentity(); RenderSystem.translatef(0, 0, -2000); RenderSystem.enableAlphaTest();' 'NativeGuiProjection projection = new NativeGuiProjection(viewport[0], viewport[1], 1000, 3000, -2000);' 'GUI shader projection' + $source = Adapt-Exact $source 'RenderSystem.popMatrix(); RenderSystem.matrixMode(5889); RenderSystem.popMatrix(); RenderSystem.matrixMode(5888);' 'projection.close();' 'GUI projection restore' + } + ChildRendererGeometryProbe { + $source = Adapt-Exact $source 'BlockEntity tile = type.create();' @' +net.minecraft.world.level.block.state.BlockState state = ForgeRegistries.BLOCKS.getValues().stream().map(net.minecraft.world.level.block.Block::defaultBlockState).filter(type::isValid).findFirst().orElseThrow(() -> new IllegalStateException("No valid native block state for " + id)); +BlockEntity tile = type.create(net.minecraft.core.BlockPos.ZERO, state); +'@ 'native registered renderer tile factory' + $source = Adapt-Exact $source 'type.mode() == 7' 'type.mode() == com.mojang.blaze3d.vertex.VertexFormat.Mode.QUADS' 'native renderer quad enum' + } + } + $source +} +function With-BaseIdentity([string]$Source) { + [regex]::Replace($Source, '(? respawnDimension = Level.OVERWORLD;" + }, + { + "firstLine": 294, + "lastLine": 301, + "text": " if (p_9131_.contains(\"SpawnX\", 99) && p_9131_.contains(\"SpawnY\", 99) && p_9131_.contains(\"SpawnZ\", 99)) {\n this.respawnPosition = new BlockPos(p_9131_.getInt(\"SpawnX\"), p_9131_.getInt(\"SpawnY\"), p_9131_.getInt(\"SpawnZ\"));\n this.respawnForced = p_9131_.getBoolean(\"SpawnForced\");\n this.respawnAngle = p_9131_.getFloat(\"SpawnAngle\");\n if (p_9131_.contains(\"SpawnDimension\")) {\n this.respawnDimension = Level.RESOURCE_KEY_CODEC.parse(NbtOps.INSTANCE, p_9131_.get(\"SpawnDimension\")).resultOrPartial(LOGGER::error).orElse(Level.OVERWORLD);\n }\n }" + }, + { + "firstLine": 329, + "lastLine": 339, + "text": " p_9197_.putString(\"Dimension\", this.level.dimension().location().toString());\n if (this.respawnPosition != null) {\n p_9197_.putInt(\"SpawnX\", this.respawnPosition.getX());\n p_9197_.putInt(\"SpawnY\", this.respawnPosition.getY());\n p_9197_.putInt(\"SpawnZ\", this.respawnPosition.getZ());\n p_9197_.putBoolean(\"SpawnForced\", this.respawnForced);\n p_9197_.putFloat(\"SpawnAngle\", this.respawnAngle);\n ResourceLocation.CODEC.encodeStart(NbtOps.INSTANCE, this.respawnDimension.location()).resultOrPartial(LOGGER::error).ifPresent((p_9134_) -> {\n p_9197_.put(\"SpawnDimension\", p_9134_);\n });\n }" + } + ] + }, + { + "entry": "net/minecraft/world/level/Level.java", + "entrySha256": "927DD75F1CFEEA6CD63FC679992258327DCF7A1BDD2543FEFEB5E3166547A6D7", + "excerpts": [ + { + "firstLine": 70, + "lastLine": 73, + "text": " public static final Codec> RESOURCE_KEY_CODEC = ResourceLocation.CODEC.xmap(ResourceKey.elementKey(Registry.DIMENSION_REGISTRY), ResourceKey::location);\n public static final ResourceKey OVERWORLD = ResourceKey.create(Registry.DIMENSION_REGISTRY, new ResourceLocation(\"overworld\"));\n public static final ResourceKey NETHER = ResourceKey.create(Registry.DIMENSION_REGISTRY, new ResourceLocation(\"the_nether\"));\n public static final ResourceKey END = ResourceKey.create(Registry.DIMENSION_REGISTRY, new ResourceLocation(\"the_end\"));" + } + ] + }, + { + "entry": "net/minecraft/resources/ResourceLocation.java", + "entrySha256": "F392A113C24DF2B087AF8289D987C9F1F20C2ECD2966D522199CF6C6E486CD98", + "excerpts": [ + { + "firstLine": 23, + "lastLine": 26, + "text": " public static final Codec CODEC = Codec.STRING.comapFlatMap(ResourceLocation::read, ResourceLocation::toString).stable();\n private static final SimpleCommandExceptionType ERROR_INVALID = new SimpleCommandExceptionType(new TranslatableComponent(\"argument.id.invalid\"));\n public static final char NAMESPACE_SEPARATOR = ':';\n public static final String DEFAULT_NAMESPACE = \"minecraft\";" + }, + { + "firstLine": 31, + "lastLine": 33, + "text": " protected ResourceLocation(String[] p_135814_) {\n this.namespace = StringUtils.isEmpty(p_135814_[0]) ? \"minecraft\" : p_135814_[0];\n this.path = p_135814_[1];" + }, + { + "firstLine": 41, + "lastLine": 43, + "text": " public ResourceLocation(String p_135809_) {\n this(decompose(p_135809_, ':'));\n }" + }, + { + "firstLine": 62, + "lastLine": 71, + "text": " protected static String[] decompose(String p_135833_, char p_135834_) {\n String[] astring = new String[]{\"minecraft\", p_135833_};\n int i = p_135833_.indexOf(p_135834_);\n if (i >= 0) {\n astring[1] = p_135833_.substring(i + 1, p_135833_.length());\n if (i >= 1) {\n astring[0] = p_135833_.substring(0, i);\n }\n }\n" + }, + { + "firstLine": 91, + "lastLine": 93, + "text": " public String toString() {\n return this.namespace + \":\" + this.path;\n }" + } + ] + } + ] +} + ], + "inheritedFixture": { + "fixture": "native116_player_spawn_default.json", + "sha256": "BE92D8F3544634BF2E8BAF12BDD4EA4DCDED8467D5BE748893640963E71EC4E3", + "payloadScope": "CASES_PRESERVED_EXACTLY_INCLUDING_116_RUNTIME_PROVENANCE_NOT_REQUALIFIED_AS_117" +}, + "nativeTargetSemantics": { + "nativeEncodedOverworld": "minecraft:overworld", + "historicalSemanticOverworld": 0, + "loadDefault": "Constructor Level.OVERWORLD retained when SpawnDimension is absent", + "saveGuard": "respawnPosition != null", + "qualificationScope": "PATCHED_117_PRIMARY_SOURCE_PROOF_NOT_NEW_PLAYER_RUNTIME_QUALIFICATION" +}, + "schemaVersion": 1, + "scope": "AUTHENTIC_PLAYER_ROOT_SCHEMA_FIXTURES_NOT_NEW_RUNTIME_QUALIFICATION" +} diff --git a/test-rig/migration/historical_test_inputs.py b/test-rig/migration/historical_test_inputs.py new file mode 100644 index 00000000..c809bb05 --- /dev/null +++ b/test-rig/migration/historical_test_inputs.py @@ -0,0 +1,56 @@ +"""Read historical oracles from their pinned trees, not a newer port's checkout.""" + +from functools import lru_cache +from pathlib import Path +import subprocess + + +RIG = Path(__file__).resolve().parents[1] +POWER114 = "ef08e5cd134dc75bd73518bf4c90a6711e67280f" +POWER115 = "5b8e73a78391960643420a09798a1ef8eb06f518" +POWER116 = "c310d0ca63ad9a8408dd4bba75172afafa5c71b7" +ADVANTAGE117 = "bfd51b06942ed4f7f8b59576e7ef233e79a91e02" +BASE117_ADAPTERS = "7aef5b899a45aa23f33397081e762c46d770e39c" +STATIONS = "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json" +JAVA = "src/main/java/zone/moddev/mc/advantageworks/" +JAVA_TESTS = "src/test/java/zone/moddev/mc/advantageworks/" + +# These named files now serve the active port. All other ledger entries still +# check the worktree directly, so an unrelated edit cannot hide in the archive. +ACTIVE_ADAPTERS = frozenset({ + STATIONS, "client-resources/resources/gui-label-expectations.json", + *("migration/" + name + ".py" for name in ( + "endurance", "extended_lifecycle", "lifecycle", "oil_transfer", + "progression", "semantic_machine_audit", + )), + *(JAVA_TESTS + name + "Test.java" for name in ( + "BayDrillAssertions", "ConnectedSession", "LifecycleProbe", + "Native114Definition", "Native115Definition", "NativeTicketFixture", + "ProfileManifest", + "WorksDefinition", "WorldgenProtocolContract", "WorldgenSamplingJob", + "runner/Native114Profile", "runner/Native115Profile", + "runner/WorldgenCompletion", "runner/WorldgenFailureCleanup", + )), +}) +RELOCATED_TESTS = frozenset(JAVA_TESTS + name + "Test.java" for name in ( + "WorldgenCompletion", "WorldgenFailureCleanup", +)) +SPLIT_TESTS = frozenset("migration/test_" + name + ".py" for name in ( + "broad_result_audit", "steam_control_audit", "native114_broad", + "native115_broad", "native116_broad", "native115_geometry", + "native116_geometry", "native115_harvest", "native116_harvest", + "native116_redstone_reload", "recipe_unlocks", +)) + + +@lru_cache(maxsize=None) +def git_blob(commit, relative): + return subprocess.check_output( + ["git", "show", commit + ":test-rig/" + relative], cwd=RIG.parent + ) + + +def oracle_bytes(commit, relative): + if relative in ACTIVE_ADAPTERS | RELOCATED_TESTS | SPLIT_TESTS or relative == "migration/steam_control_audit.py": + return git_blob(commit, relative) + return (RIG / relative).read_bytes() diff --git a/test-rig/migration/lifecycle.py b/test-rig/migration/lifecycle.py index b611956d..dedcaac3 100644 --- a/test-rig/migration/lifecycle.py +++ b/test-rig/migration/lifecycle.py @@ -86,8 +86,9 @@ def check(ok, detail): check("facing=" + fixture["facing"] in after["state"], "facing changed") check(contents(before) == contents(after), "inventory or custom item NBT changed") check(counts([after]) == {"minecraft:dirt": 7, "minecraft:apple": 13}, "seeded inventory lost or duplicated") - check(movement["oldPosition"]["block"] == ("steamadvantage:steam_pipe" if fixture["energyType"] == "steam" - else "electricadvantage:electric_conduit"), "old position is not a conduit") + namespace = fixture["block"].split(":", 1)[0] + conduit = "steam_pipe" if fixture["energyType"] == "steam" else "electric_conduit" + check(movement["oldPosition"]["block"] == f"{namespace}:{conduit}", "old position is not a conduit") old_energy = before["energy"][fixture["energyType"]] new_energy = after["energy"][fixture["energyType"]] expected = 10 if fixture["energyType"] == "steam" else 250 @@ -129,10 +130,9 @@ def build(self): for f in self.definition: self.command("fill " + " ".join(map(str, f["min"] + f["max"])) + " minecraft:air replace") if "from" in f: - mod = f["block"].split(":")[0] self.block(f["control"], "minecraft:redstone_block") self.block(f["from"], f["block"] + "[facing=" + f["facing"] + "]") - self.block(f["to"], mod + (":steam_track" if mod == "steamadvantage" else ":electric_track")) + self.block(f["to"], f["block"].replace("_drill", "_track")) self.block(offset(f["from"], f["direction"]), "minecraft:bedrock") self.block(offset(f["to"], f["direction"]), "minecraft:bedrock") self.inventory(f["from"], 0, 'minecraft:dirt{AdvWorksWitness:"' + f["id"] + '"}', 7) diff --git a/test-rig/migration/native117-historical-inputs.json b/test-rig/migration/native117-historical-inputs.json new file mode 100644 index 00000000..af53cc3e --- /dev/null +++ b/test-rig/migration/native117-historical-inputs.json @@ -0,0 +1,864 @@ +{ + "format": 1, + "sourceCommit": "c310d0ca63ad9a8408dd4bba75172afafa5c71b7", + "sourceTree": "5d3893fc5316a5af20d2456d51cc020a7741f5b0", + "scope": "Exact preserved test-rig baseline at the accepted 1.16.5 merge. Historical Python, profiles, station resources, GUI label specifications, old ledgers and the excluded NativeHarvestIsolationTest remain byte-identical. Only individually enumerated native Java, target metadata and eight parent-approved client metadata/runner/documentation/preservation adaptations are outside historical byte identity. New native117 sidecars are not historical inputs.", + "hashDefinition": "SHA-256 of raw git-blob bytes at sourceCommit, with no newline normalization or worktree re-fingerprinting.", + "baselineFileCount": 392, + "baselineInventorySha256": "C606235169C963A2CEF5445E87A645A73228AE05A35D2EFB89D22ABD6E88B559", + "excludedNativePathsSha256": "B0B86A9CFD2CF5065C155A44381AFE240CD5625C003A063E916CDE87918279BC", + "approvedCorrections": "The source baseline already includes EXTENDED-ABSENCE-001. Its accepted bytes are pinned directly; native116-historical-inputs.json remains unchanged and still records its older source baseline.", + "hashes": { + "NATIVE114.md": "2C7641EC617E2E9947EAC9A73400106CFAADFFA92ECE87D86CBB6058AAE98F11", + "NATIVE115.md": "597DD5E77662927D8053CFD9DD92148B3499406623A430ADCC18D34BCE18C37C", + "NATIVE116.md": "9042A0DC167CEBE02C6A794DB1C85330CEEFAE7EF7D4DA16F4D27B3999EDF66D", + "README.md": "F6547A29A14556F1166C83A8B615D444112208E09223D8093DC0ED800881F068", + "client-resources/resources/gui-label-expectations.json": "EC0B66B926053E90C216D856B5F33C19D5B7E92DBC4C9E2E25812B401AEBAC0E", + "eclipse/EclipseModelProbe.java": "D52141BDB5C1E3256C527B1D8CC368CF8A5D56381E8FE143C1463E4E10383402", + "migration-probe/build.gradle": "229FBDB5DF9B08E79AFDAE5C66EBB479347F584DF2F0F5015D7B3F5C4C09C4DC", + "migration-probe/settings.gradle": "015D8E7819316CF54E3E729D5A6FC0A10DD7FCDA4F1A3F55195BFDA69F7909C4", + "migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyActors.java": "6EFB3B60E6B31A027C59CA38670F8772E074E313D391E955F20FA5584DB17EBC", + "migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyConnected.java": "88A8F511AE10EF06589535A37DBEE79270C84396108EEB2F1A07B97ACB8E1911", + "migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyDimensionEntities.java": "966E10050894791368861B79779F87EB32FEC19AA05EE261FE1B0275368ED8EC", + "migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyProbe.java": "5F049CC2E57DDEC6CC14210EC822E8659153A0A58B173056DD62C86EA42D5C01", + "migration/BROAD-RESULT-AUDIT.md": "23D80023CE182FB106E35646B9AB596119D813DA9F0AB43357B19EDC286ABEB5", + "migration/EXTENDED-ABSENCE-CORRECTION.md": "A9CC6DB2D900A1EEE9A6408C3CA2373072167717C2B3D162670AE7815B3C991E", + "migration/README.md": "848A78AD4B215BBBD2B48AEB8BE16E387E421E6DA2C718509AEDB04762E72272", + "migration/actors.py": "529A775CCBEF6D7E2687262006A3781AEE4475EC5CE3ECD49F32084EBB874A3F", + "migration/actors_native.py": "14658DFEA1B966581EB1BD110139B59F576AF3BCB43B7CDA3D598160685F34DB", + "migration/audit_actors.py": "5D3AE0C4676AACE1F43953D47072D04B9A49932F14078220D559E7AD2906F2F6", + "migration/audit_actors_native.py": "3CAC4F1A2CF10183F828E64B8AE6A8D657EE74F689CEF5FACD4957805AD4875D", + "migration/audit_connected.py": "A8AE93E261658E637860A7A32D4A0074A2C7A31ED39A34F1C3747FDC369B8100", + "migration/audit_connected_progress.py": "2ADD1B2BCF43B88A14CEF5E870FC558175B422EF083DE9ED5A55CB91341A012E", + "migration/audit_dimensions.py": "CEA6968F976C20DD37BECA05D590788AC92A526D3A431B880BD94A5E65348C61", + "migration/audit_drill_restart.py": "43BF094EF333E4D8DE210A927ED73AE506ABA5464CBD07AEFF517EE1A967DF53", + "migration/audit_endurance.py": "2DCDE84D8435053661BF7D1D7E999200EFC59CD86CCD080CC89316F20FE6F306", + "migration/audit_extended_absence.py": "C6D16228F72B3AF22CB4F57A22555FE3E1E69F42743E3D8CD113FDA3065CAF13", + "migration/audit_extended_lifecycle.py": "C4388C06026BCFF33BA2F66D326AFA5B3041B00B6ED0AA1B7C627EF9BF12D111", + "migration/audit_lifecycle.py": "D868E311789E62D29632D9A4FC0B75B0A495A2DAAD39E309507AB552AA418D4F", + "migration/audit_native114_endurance.py": "4D1A7E5BCCFDF680135680DB2272FD23BA17E5C3BB45D60A0DE4C987CDA02C8A", + "migration/audit_native114_partial.py": "8FCE2D8F0B5305C2950E154396DC86F9C4EA1C9138BB22DF6DBCF568B420F004", + "migration/audit_native115_endurance.py": "702C252419CEB978A55D0582D818029065130F8CDAD6E902314B38E954A81F6A", + "migration/audit_native115_partial.py": "B2A58EF7C9D991CC7C57673561418F9956DDC0104329BF2154FFC834F829D3FE", + "migration/audit_native116_endurance.py": "B7CD482096EAF4E3CDFD53B42A41E312B365762179D9266BD81561B43EC705DF", + "migration/audit_native116_partial.py": "4C4C7F99A4D462A1234AA8397821428F6243BC9BB3A49C640E83D8252A2CE14C", + "migration/audit_pipe_only.py": "191C0453EFB0A8E866E0E9B814C325ECC212A4A9895AEA9E4965A2C7817424F6", + "migration/audit_player_layout.py": "95C672B76D5A0A34B36D22ECEBC2DFCF7425E66B9505E15FE9F9749436565008", + "migration/audit_respawn.py": "6A2EBDB6851BBE63CBAF55BDE523EEB62E28DE1913CBF6B1E9210DF6453427A6", + "migration/audit_results.py": "1EE3101218A551B9908BB480937DDAE0C2CF1ECC2EB97840725054B572B42E2E", + "migration/audit_transformer_order.py": "C8FADD3443ED62E5E8164604949E021BF6D11D10E72BE3FF030021332632C125", + "migration/audited-semantic-corrections.json": "D38719DCACA429CEF67905D4093049E8A7F06DA3F566A0A421F91C41EEE52067", + "migration/broad_campaign_status.py": "92E54DCB4B7F641F89CD6629B038684EF785F5926CE517D43D290122955B02D1", + "migration/broad_result_audit.py": "109F5FD33112DBC2FC62F8A1BD29F13771C7ACAD8892E01B874684D6E5AE7A16", + "migration/connected.py": "9E34BAE4E9FDF43AC58C9897FA2F813C53C97145FBDE472102476DCAB4E1051E", + "migration/dimensions.py": "F23E39FD5C887B489F1B3FDEBC7B8E0835564171C30544A2F7638E89B2B169BA", + "migration/drill_restart.py": "6D4BB8AE22B3BA225A8E0455254C4BAEEF4E2973B06121F843A977B586AADFEF", + "migration/endurance.py": "F4E806E2D0C5280F7CEFF544EABB14D995E718142CCC71899F26FC00E8CE0B4E", + "migration/extended_lifecycle.py": "EF746EEC9B21E5D9757684A5B1AD6387706EBCD7E2CB15D020234E9B4D57DC82", + "migration/fixtures/native116_player_spawn_default.json": "BE92D8F3544634BF2E8BAF12BDD4EA4DCDED8467D5BE748893640963E71EC4E3", + "migration/lifecycle.py": "D8A8E5457FC6F1ED70E983482049CE79B831D2EFCE7AF0872B9B5EDE66411007", + "migration/matrix.py": "F0B1C19645464583E776849A82BE3F8EAAC5129A2B17614F222E7AAFA1E2B28E", + "migration/native114.py": "E5D5242E0E1E5F686AEB83E90726CB862CE288887185C15732E0084146DD8811", + "migration/native114_actors.py": "EA0726C8781BE9F9A5775C55DD5BF58138AF74645E8DEE1373E79190487F72E8", + "migration/native114_audit_actors.py": "3C14F1941779003A4AFA92CA35831F9D48B6046C74BA163621A294198657C43A", + "migration/native114_audit_order.py": "24CF9143345AC4033BF21C9C7BE7C93D150C3542CC8DC6D88BC80B4C4D42599F", + "migration/native114_audit_respawn.py": "45207C265C3F4C78FF9B70B13D2714EF72CFAEB86D1EED5E134CB1219DC38522", + "migration/native114_broad.py": "C5AD9B69465DD59312F2686059688727D0836BEFA9F31788DC44C3DD08A6CF23", + "migration/native114_connected.py": "5B350E7E58D41BA9428B13A1B4BBF730A990528AEF8E6D2FB2AB918D8B3D1FF3", + "migration/native114_endurance.py": "EA98C26B73FDC71F1E270B6E8753674940332C7D2029D144E60F65AECF02C929", + "migration/native114_extended.py": "717582DF6A7B6DD40D8BCB8953E6667FCA4B747AFDC8DF33B03876DD4944A6F4", + "migration/native114_extended_reads.py": "70F8A516DBEC561B1D7BC78740D429DE8C4CFFE21E40FEC0625350B43D7E4947", + "migration/native114_holder_networks.py": "F731C68EC40F3F52173701D38842FCC99FC13E77E0D0201EE323EDAE2A1C7830", + "migration/native114_interop.py": "636EA0DD2CA24BEEBB3E5F1EC2BD7227F8452A4F6ABD5734F9A1D2C862ED1F02", + "migration/native114_lifecycle.py": "D77A7B5FCAE05C1A02F513979AC91EA6BC161DB500F5467E6452A64F514E4812", + "migration/native114_oil.py": "100773BE25B4F504E603A9D9BAEB82FA17D2652C82B3AF3B2A09DD714EDAA942", + "migration/native114_partial.py": "8CC74C4ECDF7DD05CB0AFCFD5D55FAF7DAAC8A5C15F11FCFF441A7774DB0A148", + "migration/native114_partial_diagnostic.py": "1B0E478BD42BA166A164A1708CFC60A9CC8471F8EFE2D589CB8163622C98389A", + "migration/native114_player_layout.py": "3B025EA3DC574D952B05D5F6595E02BE8F8A15CF925AD1E35612F1234B92CB2A", + "migration/native114_recipes.py": "AF6583B249EBC3CAC6CC0645D287CA4D0D81B9AE8ACA402AF1799AE8BE30F750", + "migration/native114_refresh.py": "EF4950B50EB5B16651D6CC31171005B42E7DFD04D5D680578C73B17F9E7755A3", + "migration/native114_respawn.py": "94EAEA6CCD212947AB4BC31941C98D7F676D72C4D35321320656877553DA6895", + "migration/native114_ticket_networks.py": "40958F44AEDC20773994EEEA08095930BF0EE3AFAAA3ADAF8A97A68827660715", + "migration/native114_transformer_order.py": "4B9F5E23C178EB05AAF216CEE1B89A73080D9EB7018035B64422AF59CD8BB6E3", + "migration/native115-historical-inputs.json": "12B5D3DDF214CF0D36513CD140762985618917C47A69D3CC793EFEFB1D9C708A", + "migration/native115.py": "3CE29ED96746354B706848A8BEA4062B5DA21D6A4B2652F75AB59231E3A22C3E", + "migration/native115_actors.py": "92D2E4DC4D07949E3D68C955CB7D901215617382F63A5BAA2AC16B8F808DDFDC", + "migration/native115_audit_actors.py": "258FD6B2B7DE0661EAB4D31477418E75D098725A18693638C189F64BC929B52B", + "migration/native115_audit_order.py": "D3F05A9CFC5366DFB1F8779F62F33D6C47ECDD911F087EE844937A2879E7829E", + "migration/native115_audit_respawn.py": "12C17CCC9BF396C0EA435DAE035FC50721F92F92438983A32A7DF8751539E7BA", + "migration/native115_broad.py": "1FE6DBC3BEBF629C8055D19AD4EA32A77CF1655A9B633D652EE3D22135129CAF", + "migration/native115_campaign.py": "70C68BA9D2545135BC751E7D5465690F2DF90A9CF3F8AF320EFB4F5E4B6FE097", + "migration/native115_connected.py": "BF6F7C51333B915F4685469592C41839D34D1C3D37C08C23C642300BE5640EC2", + "migration/native115_endurance.py": "0421695DC2B0524A7B1834C563A742071F9AC26B6AACB1FA93F27B1331116E58", + "migration/native115_extended.py": "B769680DCC11C69BA46CE5606F6D1992DC2E90FB52FF0577C80116C20F32C256", + "migration/native115_extended_reads.py": "E1CC47A9FF67CED4D82360365E6890BF55F90212C7826D2E31A294C2FBAC44AE", + "migration/native115_geometry.py": "97075BF21F4EDF789FE1ED765CBEA8F4906451F52AEBB17931F4F81A0E94BB3E", + "migration/native115_harvest.py": "2B7A723F79A7FC026FDABAF4C6017BAA7F321DAC443D8121AC0AC9CFBA3651E8", + "migration/native115_holder_networks.py": "093FEC36849C1AFE29B50E9D531C594B1A6877B7DA64173F527C13EC74884374", + "migration/native115_interop.py": "BCFD673C67DAFD114048D3CF9399CD617FAB6129017D038C8A25152F4E871084", + "migration/native115_lifecycle.py": "AADE8954E98E4AC6EC688A1C666D92A60020035414763796BA4AEFBA66FD346F", + "migration/native115_oil.py": "A99610EF34C19F1A054749B5E921E59FE170DEEE23FC1B8B9EB54BC23997D9AE", + "migration/native115_partial.py": "7E4007D9322AA916E1ADFD2E83ABB5D2C1A4F4751D7DDB30B4AD4F71258EE46D", + "migration/native115_partial_diagnostic.py": "2C1C044F79277D506FD07749903D6B7634C06D885EF1FCDD9F85A52188C34F29", + "migration/native115_player_layout.py": "11CD07F77AD9E6898BC208A5C7F2D61015741CF18745BA6C3732618F9997CFA6", + "migration/native115_recipes.py": "88F1604EBF5AAD39665FA6798560A62DB754E2F457A5B015F62A5CDF1DF376C2", + "migration/native115_refresh.py": "AFC38738EB04A3911C2AF31E50BCAEE2CE81EA13CE09F6A4D48F88BA4E495DE1", + "migration/native115_respawn.py": "8A327477651A9AF6201182DC0262B16601A7727690DC526296AECBABA7C56133", + "migration/native115_ticket_networks.py": "E8C85B049EFC06CE818123EB844F5EAA89CCAA1C94A70B436F3EA2F4792935EC", + "migration/native115_transformer_order.py": "EDA40143CF8540D6C789D13568B3AEAAEFCB9DCAA1444595174D1ACE5D54B915", + "migration/native116-historical-inputs.json": "7A30C147C44C14C277EB55DF23006601286A34B082C8978D797EA8E822CAC5C3", + "migration/native116.py": "5C4B2E615B7CBC7B679F91E782E9F7A314EC4B1DD59E7BE5A5A025520B8C4D7D", + "migration/native116_actor_io.py": "7C6664209B0EC788416357EBE9F8439E1E692EBCF8C931EF6434F6276F3345DB", + "migration/native116_actors.py": "CD3FFB22CC09A1ED22B95A204E8AB208F03235B079228357A292D416338D05C3", + "migration/native116_audit_actors.py": "E90974A650BA497A98AED13318DF120C85CC592104575535EA83E87655BD8878", + "migration/native116_audit_order.py": "404E8C5ECF42806CAC7745A21E65B970DBEACEC048A06FD90A884527FD2F9C39", + "migration/native116_audit_respawn.py": "789AF980B93BB6C6B89518780ACD0A9112867FA05325E172661C76E74C041374", + "migration/native116_broad.py": "08564A17FAF47C24B6056C812011AA1A82C29EA1986338751DD83AA990ACB5E7", + "migration/native116_campaign.py": "2709021665F2CC95EFFDD3122769C66D1E17BCF63E585815914809F9FCF09ACE", + "migration/native116_code.py": "F45FDF2AA10F10C556D17F0BE7C001D3388A4774FCEE6509B56BE9D2D48812CB", + "migration/native116_connected.py": "34BD1E66EE427980473FC9ACC8031E3BDC195D04C8C05A4CA3E50F30398AAE12", + "migration/native116_dimension_assessment.py": "307B6471B72ABB7B1BA2C8A504FB9D72984019BC82833B0E5287E54C42EA031A", + "migration/native116_dimension_io.py": "9084DFF7AD5DDF60B20742C78BEF0FC180459C2E6CECBD0F66149F8684C7CFF9", + "migration/native116_endurance.py": "FA11043C5DBF3C99CBA7A39579D86F775F739BDC2F3B9FC0133961395D760DB7", + "migration/native116_extended.py": "F7DC21756D1321824F393731A3E96F4533612AF60CCD1AECFD3EE9899AD5A052", + "migration/native116_extended_reads.py": "9859C0666367BDF4B30A19281EBDDC466FDCA9C45B136AFD7144B8A6DC22C679", + "migration/native116_format.py": "AB248756B00B907D010FF5271D2597AD1C546E0AEEB113536FF9CFEBC8E1465C", + "migration/native116_geometry.py": "3F8B2F0D6A89D1206D0DB3B0685D410D281ADE5C566F4C453448B045BB7C21F0", + "migration/native116_harvest.py": "3613038EF4AC203235CCDDDEE09F1CE5AB56CA1F8A4624921B8E26A49296EFFE", + "migration/native116_holder_networks.py": "897117BBC2A3CD55EA71DFFEA3700687EE185A56813373DBF8551EC1563D169C", + "migration/native116_interop.py": "4A9770A46989DACC67B834C1E912BE7F90EA527766A23740FA65880C4ACA8A38", + "migration/native116_legacy_snbt.py": "CD9C7ED2AA6BD25FC7F469640644337398A1FE5B121C685E01C9C401B9F31407", + "migration/native116_lifecycle.py": "3D3AB43BB12CC4B9B21DEF283D277EF639979763C1B6D20F595CD958508B8C27", + "migration/native116_machine_items.py": "A0141A8EE114CFD66FB236B7EC26907D0EDE8F38E87A13C80EFAC6CF38020F69", + "migration/native116_machine_operations.py": "15A2D1648DBA9A298062C2359B4245D6CC668075B883DE7EE9E78D1C3223B5BC", + "migration/native116_mod_inventory.py": "F9BF4A05FB4621D2989525BC3AC5B53DAB03396D8D3927FB55200F5DBFE8FB2F", + "migration/native116_observation_plan.py": "6C9F9D5DDE064234CFC65CCC2680422A20B5F2C92D4DAF0E99836379CFA48DAC", + "migration/native116_oil.py": "2220F25CDDD8743ABD8E9AD75BAD7407D4CA38A3FF3638B6A7D7132DB8A25D45", + "migration/native116_partial.py": "1A9D9F18763EA17C2CCC743608A4C7B4EF86F82045989769172C400AAF45A1A6", + "migration/native116_partial_diagnostic.py": "0A260CB108D470676F45421C530066B7E3AA7663D417111DC520A883F4FCB81B", + "migration/native116_player_layout.py": "88B31D6160BE70883943781A80E517309C36E8A978B7A2F0ED9EEB9F230BF707", + "migration/native116_recipes.py": "B54B1E03C5CED9DF8FAC05FB9175C64D6F49CFB1588C8B8A23085CD9A2145C43", + "migration/native116_redstone_reload.py": "46D86113AFF3FD69FFA747AC79CAD78A233FD930F40FDD6E63D204B4B558AD9E", + "migration/native116_refresh.py": "B12680543A8085092A5092FF52B2B02C7F9E595D562C9D98B519CE1AE3507E0E", + "migration/native116_resource_audit.py": "C780A10EAED96FB818BC4C5443F929D942E6E18F6675AFC2AF05EA4C9F705366", + "migration/native116_respawn.py": "A30A135193518E0B12818FE278C1F5C7A5EA69503E8F32B7D59DFF6F5EE0D100", + "migration/native116_save_io.py": "DEC554A49BC8D1083BE77B799DEFE1465D972317CF10274CA5EC7241442E807C", + "migration/native116_sources.py": "3888FBF9796CD59C7DBDC7D8A55C89836629A7457FD1E59E9B713E9FD812F38B", + "migration/native116_ticket_networks.py": "0289DB650C17E2BA73A74D5A92F585A9B69B84DD1FD2CE9372A8A9FDB5D62C69", + "migration/native116_ticket_precondition.py": "5A22612D77EA28D7147F6E17D420FF1CC09E3D2DCBB803DB717CF595B3AEC7E3", + "migration/native116_transformer_order.py": "173363E12BA783DD819D8294D6EA26A6CCA0EA6A0FEA09EE80CFC98D0C893E05", + "migration/oil_transfer.py": "85F60CFABFAF7107FAD461C6747319D027B9FF7DF85E303B7592F8FF78E9705F", + "migration/partial_network_trace.py": "34ADE03A034296E7CFB753777AE68126A134BC28C4870F9A73FCCA15D0B3F9F6", + "migration/pin_dimension_profiles.py": "4E79D6602CF217E3BB14C8E451C04EE1AABAE3EE65B902F250C3C20FE65D1D87", + "migration/pin_drill_profiles.py": "B58CBED8028BC4E01BC8655A65621C04F0452B6D6C193FE8DC59AB5BBF371829", + "migration/pipe_only_lifecycle.py": "F0F9C9B87C567315030B190C20A745B5319657ADE785583093E05013E14AB41F", + "migration/player_layout.py": "C2C669E64320A2196E4AA102D2B8A56FB193A46BEC72C55ADC6F2C98C6142B44", + "migration/progression.py": "951287B42ED8782C2C11003A8343959DCB2BCBA929FC6267FE1211AD72DC28F2", + "migration/recipe_unlocks.py": "9872F4BE7860798C8D14DB9CE1AD99F6E3D2D688A45EEF1932749DBA1BCC0552", + "migration/requirements.txt": "07DD999D80B34A2669A02FABDD03DFBB239109C3B6E3D979C300CFAF53764037", + "migration/respawn.py": "E6E2E14A648AAE5E976BB7B1CAC15448CF8AB59820333CEC5F9D77D21FA77A28", + "migration/semantic_machine_audit.py": "B38978791BE14B0F53526E006AABF20EE3FD1FB95DE41D6E8F879D760E95E415", + "migration/steam_control_audit.py": "390460F58C0A907F41EEA878F6DAB64DC2F901DE4385712E2A4CC5F42105AA12", + "migration/test_actors.py": "6F9625D1A658BFCD90EE34A819AEB72F5594CAB5B643FEB609B856E4B595A9AF", + "migration/test_audit_connected.py": "D0632277E35C8F24F9473C1B6E119C596D88E9737F0C630F7808F208100B4809", + "migration/test_audit_native115_partial.py": "42C159866BDE122B7D2804DA753169D1990613BCAA29E05802A99FECD2682046", + "migration/test_audit_native116_partial.py": "20CC7D1157A96E9AF4E36CB735CC7DF566636C9FF2AF66B812F3DA9DA914E9D8", + "migration/test_bridge_profiles.py": "B2A34883F3413FC5F7A2D2130293BF15A0871182D381AF8A8753F2F799817F26", + "migration/test_broad_campaign_status.py": "F9CA79445F5EEAD93FEB086E9949B27AAC25F0E20332FA4217BE876F978BADA1", + "migration/test_broad_result_audit.py": "830C1B6CEB3C5765E557BFF8F7C052F32E55EE66AE5C5C8256C12E460C46B1B4", + "migration/test_connected.py": "1BE01FD1AAF31F3C9E5636AC0180E31E0E85F5ECD345559C562CD74CDA5888FD", + "migration/test_connected_progress.py": "E6FBC9A57C65A28367B7CC7C6CC95A9EE1B9B7D818892FC0B8BADD060B867B75", + "migration/test_dimensions.py": "3B8BEE2CE218AD7F96F9FA8159AB27C6816F897898D3B1C3B503D55983B491BB", + "migration/test_drill_restart.py": "DC3C434D1B7D21E277F7CC36B038AE41F29E5CBD6EB8666F93560A3EC1DC52DE", + "migration/test_endurance.py": "A466E75F91FDB5B955D6AA9210B3DA972759D671076C7E2F671379FBE582BF55", + "migration/test_extended_absence_audit.py": "77D913B88DB06478A2FC32303C0CA6F97AFABD6240444CA86953A8FD49645F45", + "migration/test_extended_absence_continuity.py": "C2B531660DC5D1550BA5133160BABDC54966A70623BAD513E8F8012A7F79A6EC", + "migration/test_extended_lifecycle.py": "7165AE933F6FF0FBA72A712521AC86F7BEB38EDDC07451CB4988BB60D7F0DCD2", + "migration/test_lifecycle.py": "0E7441A0A5C8B7D1C3B938939C95B4EC51905EEFA1073DF8F93ED6EC37D45903", + "migration/test_matrix.py": "7E83D7925F7213FB08B741AB3BCCE21911F58BBC9C219F5AD28D61E0D1BE1A72", + "migration/test_native114.py": "B0128C5052F9452539D5F5430550A48CA0AD3F93B3E6AEF58BCE12ADC31A04EC", + "migration/test_native114_actors.py": "415F78AA14AEE890EEFAD6296BD860580E8334E8DB78C1C52864FAFB12E76CB6", + "migration/test_native114_broad.py": "D69B593E494AEE7F69DB205234A900F74FFEBE0879E9E11188C6BF82C5EFDDB6", + "migration/test_native114_connected.py": "1E5AFD8A0266EE25FED1228E90CF29F69DA56FBEB71C9E4FDDB0155DB54025A8", + "migration/test_native114_endurance.py": "233D74BD1AE28FAD417A3B0E5986D76D5A1EC7BA2A1B884B866F81D65A526649", + "migration/test_native114_holder_networks.py": "F0A7DF1ECC95F3C83F5513612CF428589DC4C2D20AB083882B7579DEBE50B64D", + "migration/test_native114_interop.py": "7A443D703D0BA1BB5EF440377B78EA8151E9D2DD802F06798DB1EA4BE5CD65E9", + "migration/test_native114_lifecycle.py": "4882058C4DD26469F6F9EE7072EE923C7998F35D334DBAA2999811A16C38FABE", + "migration/test_native114_oil.py": "67664A8EB33B626C4CCA8CBFFBCB742D6688ADED39B7A02A7D67BD9C13C34ECA", + "migration/test_native114_partial.py": "397A80F3BFFA40615CB3189ACF8878D83A9F59E2A391A5BEB45B4955D24B410E", + "migration/test_native114_player_layout.py": "B0FF8FEAB51EB40693D648A8ADC3F2F29CAB9E9EB073BF6E83F382CA9DD2B49F", + "migration/test_native114_recipes.py": "320EAD45D85E3769542688A03587705B7236A3B9E95E14998F2496243D6C64DC", + "migration/test_native114_refresh.py": "E8F8BF30247F85C6A3BB92774580E8A6070E24E90C1AA88C5EA171C2261316C0", + "migration/test_native114_respawn.py": "3255DBF4ECBEE2334BADF2775E05E8405E7EED0032CEA2282073A2BC55D23EEF", + "migration/test_native114_ticket_networks.py": "88C8D393289902D3C495E1C834C960074E550C30A0734C8B31B89713F91A1763", + "migration/test_native114_transformer_order.py": "217571DE71A61BB3F9C701B7A12F1D768538A68D8A7C744FF93CB5697329ACA0", + "migration/test_native115.py": "A4B9E9BE2A8ED94E8F5848D9D7CFAF50DB6AE9588D3712C9E9014198463BEFB1", + "migration/test_native115_actors.py": "4366F6C04E2437CE909B92B5254B77EB7CB7FB1CAB86F4DAA1B7DF1A04B8A855", + "migration/test_native115_broad.py": "F78F9893D5D796C52D9FDB2249E17F00B7818D73D1CC947F47D97E246BDC1824", + "migration/test_native115_campaign.py": "5341BE1AF7C82A0C817D150FB0C12040DFB14B6DB433980B8D53B8A18F434595", + "migration/test_native115_connected.py": "752F2AF4E727AB9A7ED9D220DF674F68015000A86C60BF349ED4BAED46A955E8", + "migration/test_native115_endurance.py": "DBE7708249E8B348E7993993BEC8B01336E18083B926C7389A7DBB0DD7D782B7", + "migration/test_native115_geometry.py": "A611DB71AD5465CC82DC62456EA8BA47EF68695A670DD7E64F112EE0C4EC76BD", + "migration/test_native115_harvest.py": "D3E0DE631AABAA6827F994F032492D8FD48A4FA2CD6FF1ADC82C2BF8EE65133C", + "migration/test_native115_holder_networks.py": "B5E8038A51D53656F4B1078C747DF167FECE443C661D1337D7CE2991B5171E6D", + "migration/test_native115_interop.py": "431709B7BB658FF56F62D2F4EA3DCA99376E1E31844C430D637AF703F12E29E7", + "migration/test_native115_lifecycle.py": "CFDFD873043EE2B2997BE5163E5D046A189B2BDB383D6CD248F4478CCEB3DCD6", + "migration/test_native115_oil.py": "20FCDDFF2F86C52DB5ADEA7816A0C5092A4DEDE6CDBC41B4E87806A3E0777870", + "migration/test_native115_partial.py": "852B7AF4DB57A44303268B695F5465B00192E9915C6F278FDF5BCD0327BE7969", + "migration/test_native115_player_layout.py": "33CEBAC950A1A3BFAD7336413FB8ECAF2D71776C206A2F3DDA198E58585FD7CC", + "migration/test_native115_recipes.py": "98F532B88066BFB7CC483CC79F44AD9CB5D7209556524747B1FBD6910F60F88A", + "migration/test_native115_refresh.py": "1BF7AC240729F3F22F603C27E41FEBC1AAABCF7CDB40488D2BC5FAC0E62BC1AC", + "migration/test_native115_respawn.py": "3AEB94B689383B780872727A4EBB5685D2AFBCFA993F1766FA6194CCED078270", + "migration/test_native115_ticket_networks.py": "A44660A7366CE4D135D3460D4AB51FDDC580D9EDF0919F961A35BE9A807D0B2A", + "migration/test_native115_transformer_order.py": "4AAC5B5EDF9C7F3B54CD414BA0D40FAB088D9EA9052D5DDEF3ACB1996CE81584", + "migration/test_native116.py": "150DB2DD05C1F8134E6081A28226A9641FC3A4CC0D254C26D0ABC0B990A60D2E", + "migration/test_native116_actor_io.py": "4365A7F1DFF38B39238CEFAB211FA5CF83337188DA5C6A911767F46113A565F0", + "migration/test_native116_actors.py": "C4E6E927A4EF8F8BE9FD97750A374A249A994309CDB87A4A8385ADB5D8FA37A3", + "migration/test_native116_audit_mod_inventory.py": "FA23AF570E204A9BE064A83A95B8ED6243D756EE260972BEA075A62C0313CD61", + "migration/test_native116_broad.py": "D16659F5C3F79E6F3773603907C932D103A4240572C03FC854AABB3F04DC70F5", + "migration/test_native116_campaign.py": "CF825216E693ADCDD637CC5940070E4C072B30DB878EF02BFA519D40CE6393A3", + "migration/test_native116_campaign_interval.py": "E34EB861E73044D1C035B5A0415FA30E0C9BA1D953DADC843F2184E2CB0A11C6", + "migration/test_native116_campaign_shards.py": "3265B2222F701CF1FFEAF4610F5368F9E13FC8FE41ED15E1668A7228CDF14CAB", + "migration/test_native116_code.py": "BFEF639D924FB63E0692B59E85B4C12F698B2C7D1881C59FBB360997F8524CE6", + "migration/test_native116_connected.py": "A2AC08BE161D8E57FDF7802F7134266BFC67BA8158A232713A9FB571F1864750", + "migration/test_native116_dimension_assessment.py": "0F981923A15BC8AF60DD796EB0519264F6036B494DAF65FBC5F725226A9E4C5A", + "migration/test_native116_dimension_io.py": "CF325452569E7F0BCFF776E0AF18C853C13EB7EF029043E924B71901AF9A4573", + "migration/test_native116_endurance.py": "3AC73B3B063737CD820657B9375CB3DCDD276AC9702DD02D5632E60061C69C44", + "migration/test_native116_format.py": "9A4D0496B1541F94385A6E8CCA02CCBC66F4CE9364569C78528B98C694A395A4", + "migration/test_native116_geometry.py": "FDA51DF77497884D25E8EF1C35F351D3C71E37FD909C0C9E0599E062050A48EC", + "migration/test_native116_harvest.py": "BD9E819AB6137D777E106F5D0A37111189B1A45308D8673589399AC563730A89", + "migration/test_native116_holder_networks.py": "60F3AB14E21938B52D98B7EF697F5BE3687DB2D28A04EEDD45280A2851996630", + "migration/test_native116_interop.py": "46586C033B01B74B3BA7661D1E631DE47DD5136B5DD73A12C32445624D41C3B3", + "migration/test_native116_legacy_snbt.py": "6254032AAFE16B94C5FE438046E17FF39E84CE65512522EB5A08BF27F276C4E7", + "migration/test_native116_lifecycle.py": "B2A00982B19C6A1EF23B905E65BE94D6AA969E8DD9BFAA2D9787152306EA65EF", + "migration/test_native116_machine_items.py": "E452A0BD69D05A336F022AC4E2A3BB9B5EEB3BAD6E7840F31065FE1B64507624", + "migration/test_native116_machine_operations.py": "8B7361DCBD9722E25A7D89D8F79C186E56CEF3D957EF231A977D890899A0E20C", + "migration/test_native116_mod_inventory.py": "7C733DADFA447A264147DA145D3E0FC2F02F925C55BCCA1F0448940BD28DBBBD", + "migration/test_native116_oil.py": "05E1395E3A467146B3BB31C327DAB9AA4C866D52D4491774582BF5F6A515506B", + "migration/test_native116_partial.py": "BAE35FB9120316AA5E342C8028D5B61E097532A9CADC2279D814CA287E44AEB1", + "migration/test_native116_player_layout.py": "4E2EE87FDF8A59E88D2BD8C353157D2311AED7FA404DCB1005746931038FA2C5", + "migration/test_native116_player_spawn_default.py": "344573F415FD43E133C20672BFCF992DEFD241DFD2E5FE72653A59A96781B11D", + "migration/test_native116_recipes.py": "DB9521C343F4DE030D48EC8905F0E4089ACD568BE373F5F987D0A8188A73B3AE", + "migration/test_native116_redstone_reload.py": "E62F5BF0A001C4E9E5FED59391DBF1AF85F19ED97FD19CFBBFBFDE12D47FBD38", + "migration/test_native116_refresh.py": "3DAD74AD3735F33C47FF044F3F9E04C888603887B9F7646067CA0ECDAFA9EE43", + "migration/test_native116_resource_audit.py": "50E29285485067DCD699BF97F0BCA0CC3F5404DA2DD5EED417F0CEBF204A5489", + "migration/test_native116_respawn.py": "514AF0E6AFA5E4E2093A376828882A7486FB05EC450894D095689DDC712E5726", + "migration/test_native116_save_io.py": "8ADC8174102F39905B26D827281EE9C179FCED97497A14778DEB42926BE3BE90", + "migration/test_native116_sources.py": "02086B55CF7356889C07E3AF33948C8058AB5AF757B096BF9D1004DF51A53429", + "migration/test_native116_ticket_networks.py": "066C420D1F7D2B1710427107B58237B2EC8907306F7F2FD5BB7D6A6DBF087C9D", + "migration/test_native116_ticket_precondition.py": "EE6148A92638CC04E73D4EEBB5724EA243E27A6F5B7F01DB24AC5E65E0E218B5", + "migration/test_native116_transformer_order.py": "A251F1D37D9B981D3BE25086030D2851B3469384C07A9FE43F60D209D74A8269", + "migration/test_oil_transfer.py": "817D82DFEB1F42F7910E16AD2A1B93DD0EC90D284B6691323B9A7DF018A6C379", + "migration/test_pin_dimension_profiles.py": "E57595D1F98B60E0D1492BBB2BA0FD2B96B3E4F7B115F65B33EAB02919ACEFF3", + "migration/test_pipe_only_lifecycle.py": "D579F1B13C8A29DA7A0B3ADF7FF508F532D7D635190DBA29E3D0462DCDF032AE", + "migration/test_progression.py": "2AF0AEE36BDB825BDE8F33924B953B07E4D5E5B74D43C713046D6B9ACF0120FE", + "migration/test_recipe_unlocks.py": "CB0A995D3B160770B62ABD7C523A2EFBE0426925FB522E75F4AD7AF351036E23", + "migration/test_respawn.py": "13A0E096A7CB225D679451316C9C13AF32C13BCAC23F7157D83AA3B28E1AA1B8", + "migration/test_semantic_machine_audit.py": "92621F3B18B063CD4E9C7AC80E6365AE51A2DEC3BCE81D43191B3C8A7AE7158E", + "migration/test_steam_control_audit.py": "2648210BD69B5E14DDB7D4AF54EBA83C9A99E251D1AB9B4ED6118A717CD105DA", + "migration/test_transformer_order.py": "5D7A9D156B86285DCDD0F2F098488AD58E42C0E0313C07C8C821B7B8F426B113", + "migration/trace_connected_progress.py": "865408310A0B79C77033DC54F5C20E60FE643D3584C64F0FC049C582ADF37593", + "migration/transformer-order-probe/TransformerOrderBridge.java": "C051C7829684D8A6C90B3819B2F390FBD9B2EE8980CE00DBFC932457CDCF4AF4", + "migration/transformer_order.py": "16F0D8A7C193B6989B26A61545E3EDBDF725A5010BCF19D8ED31A114B271A2ED", + "profiles/dev-core-apocalyptic.json": "039A28C78013AD323373CC92930A7F676A3DBCBC2AC455D48A9F3C3A6CE70E4C", + "profiles/dev-core-tech-progression.json": "2612CFADCDE424AB8244EDBD4B564BA99EFDCDD22FC2DCE4CD2AAFED164B137E", + "profiles/dev-core.json": "CD93A4205AC737A48B6572530962A36C7F06C316462A8A78B30F9610BF14E3E9", + "profiles/native114/dev-core-apocalyptic.json": "C202B0A0A071D4FFB2EF07A430EC29B829A52C256B6200388CD855FFD11156B3", + "profiles/native114/dev-core-tech-progression.json": "96DFD0F56ED920CCA834F223F0C93FC0F413F1FD4E900BD2E98B41398DB80807", + "profiles/native114/dev-core.json": "1400FAF7008D16AED17758DCE94364EDE33662BCDB385490AEE3788213A75A07", + "profiles/native114/packaged-current.json": "E8B0A815F034323C656178C189F942BD6923614B5AD96DEBD11763084673737A", + "profiles/native114/packaged-decoupled.json": "D4D38A64E32D0A08B48FCFABBDECBD506B599E7DD43DC49CB8CA593F82AA77C8", + "profiles/native114/packaged-mineralogy.json": "B7A9DEA1462A9FEC93E8E52234E05670EB9F0D0422146CE63F24CDBAC280353E", + "profiles/native114/packaged-optional-power.json": "66DE0D5B3A57B1068923A3DDA394F1DA076E3C19FF932183BCAB50DDB5F08721", + "profiles/native114/worldgen-advantage.json": "EB67B53966693C8DD2C67B1EC5A4E75A09ABEEF10E4FB8ABAD5AAB25B846D0F9", + "profiles/native114/worldgen-legacy-import.json": "C0E291E2D6CDE0BE600DF7C2ECC8BAF5DB2AB8791427DBCC785816E20E9B6EF9", + "profiles/native114/worldgen-mineralogy.json": "008006919568339B20B96CC6A6405F9C214061173A1BF153D565B50D9FFA3085", + "profiles/native115/dev-core-apocalyptic.json": "88ADE31B055D4CF1DC7B3875341559550175192CB5BFECA0AE71CD8422FE53EA", + "profiles/native115/dev-core-tech-progression.json": "A16D208603536BEFB58BE030057A73AD636B82DC92881AADC9D4C3483679DE05", + "profiles/native115/dev-core.json": "69060E77920FFD2F2C8375AB09E44FD288C776405A91AF576D15458BB7220A9B", + "profiles/native115/packaged-current.json": "E8508DCBAA431AA9F9C6A615D96235BC5DD13E3408CD22F8ED9D5A20D87F2BB8", + "profiles/native115/packaged-decoupled.json": "ADB29429A9DA7EAE439515F6430CFE88007EA3BF0C58986868B68045BA4DF231", + "profiles/native115/packaged-mineralogy.json": "4098294410A65D52D2AF8219EE2C148CC5E516F17D056B0618D6F6A5ECCCED1F", + "profiles/native115/packaged-optional-power.json": "0F232AF3E91225A81124D01307292F44E363975711315C40870BE9EAB08F585C", + "profiles/native115/worldgen-advantage.json": "7814CCB4C820C8A459E29B76183D36023162BC1564A001141648E90C0223F713", + "profiles/native115/worldgen-legacy-import.json": "11B6DDDB34A7106F4473A358961DC2118C6D170525F4BE76E2553E0069E4C731", + "profiles/native115/worldgen-mineralogy.json": "6B4F5CE314E3390F7B292A5AB14BF76B3969C602C4CF33791290F78592D86614", + "profiles/native116/dev-core-apocalyptic.json": "EF4ACC733AC8886A67C554AA65F122EF5836FEFC67583CA9EF932B1333B405F3", + "profiles/native116/dev-core-tech-progression.json": "84A8427A0E4163DCD5A5ACFF628CF7C39552EEA069FC470824A4BF83F2FBB907", + "profiles/native116/dev-core.json": "B3F2DB3A31AC8D43DEB696A4369016B66225A29BA14ABD6964067BA931EAEAFF", + "profiles/native116/packaged-current.json": "AEEA4C2ACFAF682DCA42B6EF2676201384BC05BBE9FFC46D55540F07C8DCE9D0", + "profiles/native116/packaged-decoupled.json": "5ABCC5AAE06AE2FB775681EE1251B8FB8517D60F589E140590796F27FC689D63", + "profiles/native116/packaged-mineralogy.json": "D1CE2689917F7D624A86A14E2595647695BB009C866596DC5B7A8508A7BEC8CA", + "profiles/native116/packaged-optional-power.json": "B58722A04891DDFC455C27D27924CF6FA2ABD57E823673DA939AC468900CAFC4", + "profiles/native116/worldgen-advantage.json": "1D32BEF9D318CB0906C74242983975DDAC2100E00F9EA57E9A7F35E34D543B2E", + "profiles/native116/worldgen-legacy-import.json": "79730561252A3030D62C01ED4FF0C24BF68A3F37E0905FBEDB5C572F23451838", + "profiles/native116/worldgen-mineralogy.json": "35AC2C6E3D47614D510D8F1FB977A328F0CFD9EB29236F64D54CA35FF525A85C", + "profiles/packaged-current.json": "B5966E8B2C43E46AA254E2C0B0E857C0DF0E5F09ED5BEA99AE105D6FC368CCCE", + "profiles/packaged-decoupled.json": "530193E838577757A6348BBFF2B074A745A48D3A35BDCA3CD3F3B491FB5D08CD", + "profiles/packaged-mineralogy.json": "105C6F19C91693F11B1736968D5595F78AD8A2C9A90CB1B46E4E101D26CAF588", + "profiles/packaged-optional-power.json": "D8805E53B79CAB1B879E0E42BE7979DD03CDE8950C35D1A0BFDF26BB092B9AC4", + "profiles/worldgen-advantage.json": "BAE49986C084B25FF262756F5A0819863EF729D12480265FAC4BCC8717DC49DA", + "profiles/worldgen-legacy-import.json": "9061D639C22048D13F72A58497CF725890D086C2D73645BE26AC82F16A40B170", + "profiles/worldgen-mineralogy.json": "981F47E6128777B1AF1DA53DE895D65AFEAD41D1EE0D5CC547B4F727E8DD0F8D", + "settings.gradle": "C0A6722A3081AC1F64725FF25D4F0F1938FE613FFD0B9909583600788CAC3CB9", + "shared-probe/zone/moddev/mc/advantageworks/connected/ConnectedSession.java": "B1365BE35D6AB951D8F9BE7A777C46CE96F0993FE395EDE57DB55AFF59CCBA1E", + "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json": "89FF9A4E94477F39CB463ED0D7C1C41588CD4CD3F183EEF8A9197F8A7557E7BC", + "src/main/resources/assets/advantageworkstest/works/works-1.14.4.json": "2BCE16CC7A5B1F673EAB904D9208A9A0E742B04A09143191455998A69377F0DA", + "src/main/resources/assets/advantageworkstest/works/works-1.15.2.json": "A40075802B1700844847A86A0347D3A24035916C64C7E0A307269756637BE4CA", + "src/main/resources/assets/advantageworkstest/works/works-1.16.5.json": "1B280313434DDF14E506287CEB7E06B5E9336E5EDC5EDA612B6F8A7C8D28931E", + "src/test/java/zone/moddev/mc/advantageworks/NativeHarvestIsolationTest.java": "94BF6761278EDA6A60F9A593652EC7A42D1F65A8186A4E104807EFDDB7559E65" + }, + "excludedNativeAdaptations": { + "build.gradle": { + "historicalSha256": "462627A7EDEBEFC51AADC51A295B1318C1C79338239C1AF06C6D9E17FBDD0E7F", + "kind": "native-target-metadata", + "reason": "Parent-owned Minecraft 1.17.1 / Forge 37 build, dependency, mod-loader or pack-format target; historical station resources remain immutable." + }, + "client-resources/README.md": { + "historicalSha256": "2938A50D9BBFE2D4AAC90825475F8B5D3F7D98401B0E1474954D8FAD31048CF4", + "kind": "native-client-adaptation", + "reason": "Native 1.17.1 / Forge 37.1.1 / MCP 20210706.113038 client-probe documentation and native recipe-export instructions; not an alteration of historical Python, station resources, GUI labels or gameplay oracles.", + "portPreservationGuards": [ + "client-resources/test-probe-contract.ps1", + "client-resources/test-launcher.ps1" + ], + "preservationEvidence": "nativeClientPreservation", + "auditedNativeSha256": "97871B6043D71531B2F51A2131F855CED2C1E41DCFABBA46A7635983852F38FB" + }, + "client-resources/java/zone/moddev/mc/advantageworks/client/ChildRendererGeometryProbe.java": { + "historicalSha256": "B56A5F683FCC1312D3B749BEC11C57099ECB73E5C87C953EF55050EAE3320650", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "client-resources/java/zone/moddev/mc/advantageworks/client/ClientResourceProbe.java": { + "historicalSha256": "014E12FFFD8B1E52FF32FA55ABCD6A1708094AF00D85AFB2F1424D448E349453", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "client-resources/java/zone/moddev/mc/advantageworks/client/MachineGuiLayoutProbe.java": { + "historicalSha256": "8D89215AEFFDDC96A7175AE84F315DFF009D6E0F0FC1942C986E626B5806AFFF", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java": { + "historicalSha256": "E99F2D29D58F5388406FB29F419D68FF02C3C9EB51A44018C9A3440E83F37777", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "client-resources/java/zone/moddev/mc/advantageworks/client/RecipeBookGroupingProbe.java": { + "historicalSha256": "E30466B80CB0496E637B3C6AAC9D490EF55C8E0FE0EBC5ACAEF7EF90D8E9C1B1", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java": { + "historicalSha256": "666F6D8FC5F1B925665E019E4F38DAB90766A04CB64C9C256AD3BAC9BE6CF327", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "client-resources/resources/META-INF/mods.toml": { + "historicalSha256": "BF7BF1A6A02D00B0ADF604C6659533C2C792E19B888430EDDAC3C3BBBC48F5A5", + "kind": "native-client-adaptation", + "reason": "Native client-probe loader range [37,38), Minecraft [1.17.1], Forge [37.1.1] and Power [2.4.8.117011] dependency metadata; no historical station or GUI specification changes.", + "portPreservationGuards": [ + "client-resources/test-probe-contract.ps1", + "client-resources/test-launcher.ps1" + ], + "preservationEvidence": "nativeClientPreservation", + "auditedNativeSha256": "B373A6866DC7ECF8626856EC4B1D25EAF13DBDDAEB38627D10A46474648BB0B8" + }, + "client-resources/resources/pack.mcmeta": { + "historicalSha256": "98A01374A98A2699C1B75BC20774DE01F123C2779D0AE13B73609E70A64C7288", + "kind": "native-client-adaptation", + "reason": "Native client-probe pack_format changes from 6 to 7 for Minecraft 1.17.1; the independent GUI label specification and all historical station JSON remain byte-protected.", + "portPreservationGuards": [ + "client-resources/test-probe-contract.ps1" + ], + "preservationEvidence": "nativeClientPreservation", + "auditedNativeSha256": "F75A0521574E13850B9A8D34C40FA0B350B84F0F5DF1A1D4D9201615E87352F8" + }, + "client-resources/run.ps1": { + "historicalSha256": "FA625E75FEE99EC876768EE982C4569EDC4717F955BD723933A621E76862760B", + "kind": "native-client-adaptation", + "reason": "Native Java 16 launcher, Forge 37 BootstrapLauncher inputs, exact native mod metadata and archive rejection; preserve 351/126 caption checks, the extra four negative controls, evidence paths and partial-failure retention.", + "portPreservationGuards": [ + "client-resources/test-launcher.ps1", + "client-resources/test-probe-contract.ps1", + "client-resources/test-probe-contract-negative-controls.ps1" + ], + "preservationEvidence": "nativeClientPreservation", + "auditedNativeSha256": "16E485F0C83A349A3CEF54D65594E99949B0362E1CE672763C1DC9EB6B53C080" + }, + "client-resources/test-geometry-contract.ps1": { + "historicalSha256": "B6C83B23E5CAABB597F796A54158E86C086A21B55896652CB673A804FC40C244", + "kind": "native-client-adaptation", + "reason": "Halley native geometry contract: 78 static checks retain real dispatcher-owned production rendering, captured native buffers, all geometry thresholds and manual gaps; invokes the baseline-bound primary oracle and SHA-bound Matrix4f projection proof.", + "portPreservationGuards": [ + "client-resources/test-probe-contract.ps1", + "client-resources/test-probe-contract-negative-controls.ps1" + ], + "preservationEvidence": "nativeClientPreservation", + "auditedNativeSha256": "6BDA2F0B7A8C7A926F06237F30CDFD8CE914BC556E4CFAB1709725DF44EF49A0" + }, + "client-resources/test-launcher.ps1": { + "historicalSha256": "AA27455C45CDA637F65DB430763353C59748A59A685B718D0A9B798F29EF2E28", + "kind": "native-client-adaptation", + "reason": "Native 1.17 metadata, launch parser and Java/runtime input tests for run.ps1, with rejection controls; helper-only static execution must not launch a Java client or world.", + "portPreservationGuards": [ + "client-resources/test-launcher.ps1", + "client-resources/test-probe-contract.ps1" + ], + "preservationEvidence": "nativeClientPreservation", + "auditedNativeSha256": "5795D4A7CA309508704E67B4A638FD722828BB1BD27B30189430EBC951F79158" + }, + "client-resources/test-probe-contract-negative-controls.ps1": { + "historicalSha256": "BD25E417455082FA6497EF3537589928E84F7B63B4962F6A9D02986C450C376C", + "kind": "native-client-adaptation", + "reason": "Halley native preservation mutation suite: all 28 weakened-oracle controls must be rejected for their intended reasons after an unmodified prerequisite fixture passes; actual client inputs and source stay unchanged.", + "portPreservationGuards": [ + "client-resources/test-probe-contract.ps1", + "client-resources/test-geometry-contract.ps1" + ], + "preservationEvidence": "nativeClientPreservation", + "auditedNativeSha256": "220A6A8C4C70469A344E55C3665A774C151DDE8DF9FEE8EEC690C77ECFFEB6B7" + }, + "client-resources/test-probe-contract.ps1": { + "historicalSha256": "052D01E9BB0C1C2FF2B7630135177BE00EF1CDF9C9A6909A51C8638E8153301D", + "kind": "native-client-adaptation", + "reason": "Halley native port of the preservation oracle: 57 checks bind six complete probe bodies and imports to c310 Git blobs, with exact native adapters, projection restoration, unchanged GUI labels and required caption/negative-control counts.", + "portPreservationGuards": [ + "client-resources/test-geometry-contract.ps1", + "client-resources/test-probe-contract-negative-controls.ps1" + ], + "preservationEvidence": "nativeClientPreservation", + "auditedNativeSha256": "CB49CF8C8F208ECA164AE47FB0256939D5222F2156E8C3DC0066738B324C815C" + }, + "gradle.properties": { + "historicalSha256": "43469A58EC676A014D57446218B8F7DE7D1906A568CD3D1DB851EC06545E87A9", + "kind": "native-target-metadata", + "reason": "Parent-owned Minecraft 1.17.1 / Forge 37 build, dependency, mod-loader or pack-format target; historical station resources remain immutable." + }, + "src/main/java/zone/moddev/mc/advantageworks/ActorProbe.java": { + "historicalSha256": "49B9B2224DE1064AA398836C428AE8301FB2842034F63F129844FA4629F4B2E1", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksCommand.java": { + "historicalSha256": "F6F08E5640DD81B2485A0DF8ADF6DA5539B0200C56BA1F5829BFE71D1B716545", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java": { + "historicalSha256": "3BCE2A78D4848EEBBC15729FE15132DFB6B89A2EC98842E9F02136046D0AD742", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/AssertionChecks.java": { + "historicalSha256": "1BD62451303A59F3EB0F72A746C6C1A828C389B23D417D42CEA7A1FD32D04812", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/BatteryOperationalCases.java": { + "historicalSha256": "B9877CCA762326BF64315781FEDF61ECFFA70999267D89241C01D4E6583E7A68", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/BayDrillAssertions.java": { + "historicalSha256": "833A7B8C07789A91A98446F4263A3E2005D013B68729CC3BE444BF00E44323CB", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/ConnectedProbe.java": { + "historicalSha256": "21FF8A0C47BF9B0780FDEAFF5DD57FD76C80B7A16B04CC91ECA8DFADB7170DAC", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/DimensionEntityProbe.java": { + "historicalSha256": "D9D1A0523D45D3C0709B178326F6096B807F96E738D2CD7784BE55B996894FC4", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/ElectricOperationalCases.java": { + "historicalSha256": "4C7B2AF0395B8B2949BBA92FF8A66B36AC62C30EAB40DACDA59DCC031585EBA8", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/ElectricOperationalRates.java": { + "historicalSha256": "6407F4D07D8C79172BAFE5C60EB046D1F823C4FB45FC9BAA1170FBD8115924C4", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/JsonNbtConverter.java": { + "historicalSha256": "5BF5A0BDC12B8CA0723D68ED25FF1D81E0075B2339EBBC10E0BE6754A7DA59E1", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/LifecycleProbe.java": { + "historicalSha256": "89B8457FABC5771DFDB24C234DB4D7CAD14CBBD895E5ED9D68273142EE56296D", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/MachineContext.java": { + "historicalSha256": "9D09AF7D50170A2F182BCB7AB9B301D93C932B9769A21157C3B12EB500C28F30", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/MachineOperationalProbe.java": { + "historicalSha256": "171900E01733030E0FE31A34B1DAE1D78599FCA5269BA8526AD86299254A2911", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/MachineScenario.java": { + "historicalSha256": "BE1F99E7ADB6B29E65280646E76CB182B5DCDCD6EF22878DB05F598C3EB264F6", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/MigrationProbe.java": { + "historicalSha256": "06A6134BB5E52A7A6A5EB213ACFEA9DF42022A02B9EA3FF0BE2D9801BCF31EC8", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/Native116EntityContext.java": { + "historicalSha256": "2D513E319B399921DB7FE363A4866DB6DE853757D68CC8C94FBACED22A9C901C", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof.", + "replacement": "src/main/java/zone/moddev/mc/advantageworks/Native117EntityContext.java" + }, + "src/main/java/zone/moddev/mc/advantageworks/Native116PlayerIo.java": { + "historicalSha256": "01A4522660F1CF9881A6F9EDD5A8FB3124A7684B7ACA537B7C22B69E74C2D5D2", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof.", + "replacement": "src/main/java/zone/moddev/mc/advantageworks/Native117PlayerIo.java" + }, + "src/main/java/zone/moddev/mc/advantageworks/Native116TestFlat.java": { + "historicalSha256": "5C9BDB2B211FD196EA68CC9BCE366C36CE232CFA36F94C00EC58FF7A4A189932", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof.", + "replacement": "src/main/java/zone/moddev/mc/advantageworks/Native117TestFlat.java" + }, + "src/main/java/zone/moddev/mc/advantageworks/Native116Worlds.java": { + "historicalSha256": "5308E115F872FAF08810C6D76176EC14F886D4BCE509CDF1645E2C2F908D6A5B", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof.", + "replacement": "src/main/java/zone/moddev/mc/advantageworks/Native117Worlds.java" + }, + "src/main/java/zone/moddev/mc/advantageworks/NativeEntityHold.java": { + "historicalSha256": "779B385C7272BF09A033D89424316297676F7237FB93368AD3D665B735D21C0C", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/NativeHarvestProbe.java": { + "historicalSha256": "376421DB921FA76C5D25D5F1F7C4C8EADC93873EC81591B25B8EAD17037E1246", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/NativeInteropProbe.java": { + "historicalSha256": "CFB3BD9254C0FF9003D35C0F9EBAD5B6ED4EC09B3BE5918113F483875FC5CDF4", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/NativeTicketGeometry.java": { + "historicalSha256": "C665BCA8FC78A31A39A40F98BFBEF6CA3A075C4EEC84C0F60E402760F95425BD", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/PowerOperationalCases.java": { + "historicalSha256": "20649C1F31C484F44E2F4B303513F63BC8D1055118E3CDCC60D1C3CBD4183367", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/ProcessProgression.java": { + "historicalSha256": "E83BE39CC54734D5D9A7955501963621032FC5424C69E4E38F39DD0221EA4FE2", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/ProgressionProbe.java": { + "historicalSha256": "6112A4A229AD072D60C7A7D049AB94771320B3769F87216C7A21063A8000DA33", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/RecipeBookProbe.java": { + "historicalSha256": "71D452AFA75100BA747ADBBEAF05D6568AE81A98D0EAE7E37BBC1919D2832D56", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/RecipeUnlockProbe.java": { + "historicalSha256": "FC9922134D7880B5C0D7295D8C5D0E27F537AFD42D8F8D4E245AB3E4381304CC", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/RedstoneReloadProbe.java": { + "historicalSha256": "FD01A5AF5F25315E5C42919027E8A5935381591FC41F170E6440CA7A38B3E86C", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/SourcePickupOperationalCases.java": { + "historicalSha256": "3CBA1A9D7BF8F040BBA4B08F96742B9E88D457B8B7A242D35D03CE5C806D7A47", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/SteamOperationalActors.java": { + "historicalSha256": "EB69FC043F4337F216BD0238CB3F72C099ED7D26ADA60B72D63B83685D2BDDA3", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/SteamOperationalCases.java": { + "historicalSha256": "1121435F395F20F3E15A54E85CF9A375F3D63351C0FD64D028EEC7103130D6C4", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/SteamOperationalSupplementalCases.java": { + "historicalSha256": "FF7000A797E9704D3D898D551B3B3C3728371FF70BFA5D9E2A5EB60FA3458C13", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/SteamReloadControlCases.java": { + "historicalSha256": "E161A4FA068871531CFB0DBFC9271D6D2EFD6821B629EC44DBB2F0DA764A9E63", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/WorksController.java": { + "historicalSha256": "97EEB966E3C7B81002F62EA530C3B9EEE6CE205355C5C1C12AE6B5FBAD85FA20", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/WorksDefinition.java": { + "historicalSha256": "2949DA66484448A48B39B5DF829882ADC0026B85A8EDE9965F8203BBA531B6C5", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/WorksDefinitionLoader.java": { + "historicalSha256": "88450142133F833B436C5EF5FF33D54B0E8FB5A9D76E86428D4D1A73C83A1F2B", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/WorksResultWriter.java": { + "historicalSha256": "8767130F080627D814A1E6E6E48DEDE8D48B70447B24C18C453E31B18D28A52B", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/WorksWorldGuard.java": { + "historicalSha256": "997D367A49E2E9FDD754CD31BB8E9F4F4D7B9E16FCCB2D45A873559B270704C7", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/java/zone/moddev/mc/advantageworks/WorldgenSamplingJob.java": { + "historicalSha256": "EC9481A88EF1822387518509EA9EB028F8F90ADCB15CDC6ABCBB2D8C6E1270B6", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/main/resources/META-INF/mods.toml": { + "historicalSha256": "07040A88432C48E3142CAB6EA9AF53E92C399EA73BAC55EC74B04BD330C7DB70", + "kind": "native-target-metadata", + "reason": "Parent-owned Minecraft 1.17.1 / Forge 37 build, dependency, mod-loader or pack-format target; historical station resources remain immutable." + }, + "src/main/resources/pack.mcmeta": { + "historicalSha256": "96B567BC1E21E2673A922DDAABAFEA6A8FD5D9909C04EF2A7C3C7680F57E794A", + "kind": "native-target-metadata", + "reason": "Parent-owned Minecraft 1.17.1 / Forge 37 build, dependency, mod-loader or pack-format target; historical station resources remain immutable." + }, + "src/runner/java/zone/moddev/mc/advantageworks/runner/WorksRunner.java": { + "historicalSha256": "D4D88E8275D49B2EF46CF19AC347611A65EAEE4C30BE3AEA491A369C7EAA976D", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/AssertionChecksTest.java": { + "historicalSha256": "BE7CC79FBF5449362F5D885FD87987CF50B2FD8C04D7E35F8141314D0B01AC5F", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/BayDrillAssertionsTest.java": { + "historicalSha256": "3A833DD8364A263D57BE80FBA6AF2A673D66B91576CA0C7DFB7649D6D16F1F3E", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/ConnectedSessionTest.java": { + "historicalSha256": "F578B082D6E9673C9D181C09E4A7B2C3328EE5047C29F0FBAC5296811D8F8321", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/ElectricOperationalCasesTest.java": { + "historicalSha256": "5C58BD1450E775572F14DF95327B5AB03B70E03F2ED01CE628957D4A7B63AF03", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/ElectricOperationalRatesTest.java": { + "historicalSha256": "F76F6C123633559D33EA1B46537E288BD9364DE55FF674B5A4CE007FB120BB98", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/LifecycleProbeTest.java": { + "historicalSha256": "7DD1EC784E809BDE75FEE4C07F928566F111A1FD8B78D2C56E8423C81DBEE385", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/MeasurementDefinitionTest.java": { + "historicalSha256": "B0548CB1CD129A769750972BFEFFE7CFFCCEC3F46D7849336A4EF700D6F74006", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/Native114DefinitionTest.java": { + "historicalSha256": "D26B17705BC2A2FC80723F384D4B5F947E331CE74D1C6D87FDF383676D2DCF91", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/Native115DefinitionTest.java": { + "historicalSha256": "A9D65DEE0F36EA8C278F6CC52222712A80244E27C283CE231FF8093B1AB82EAE", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/Native116CommandRegistrationTest.java": { + "historicalSha256": "CC360051C1DAE2BD3B8BF98ED9A99786D98BD9A2899252A6F7E9D8CE81077002", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof.", + "replacement": "src/test/java/zone/moddev/mc/advantageworks/Native117CommandRegistrationTest.java" + }, + "src/test/java/zone/moddev/mc/advantageworks/Native116DefinitionTest.java": { + "historicalSha256": "C26C98A5FEAB46180EA5539E0E89882A7A33D7E1C9595DC2EAC9C08CEC93EA90", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof.", + "replacement": "src/test/java/zone/moddev/mc/advantageworks/Native117DefinitionTest.java" + }, + "src/test/java/zone/moddev/mc/advantageworks/Native116EngineSignaturesTest.java": { + "historicalSha256": "2F9836C05BFB123D684670F00AD0F2E87E27330CB83D5AF2B89227E5F77F129E", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof.", + "replacement": "src/test/java/zone/moddev/mc/advantageworks/Native117EngineSignaturesTest.java" + }, + "src/test/java/zone/moddev/mc/advantageworks/Native116EntityContextTest.java": { + "historicalSha256": "D84155917DA539D7330FA71EFB8EBF024B6889602E052EBB0010113FF09B9F87", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof.", + "replacement": "src/test/java/zone/moddev/mc/advantageworks/Native117EntityContextTest.java" + }, + "src/test/java/zone/moddev/mc/advantageworks/Native116HarvestIsolationTest.java": { + "historicalSha256": "331FA9B75E4C2068DFF33DCAA2667618B3AC447888E629DD1C3E264DEFEA62C1", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof.", + "replacement": "src/test/java/zone/moddev/mc/advantageworks/Native117HarvestIsolationTest.java" + }, + "src/test/java/zone/moddev/mc/advantageworks/Native116PackMetadataTest.java": { + "historicalSha256": "2B4EA64B66F6A3E173E254F0258C769FF93740FCFFDB9054B023A7DDE86C0C8E", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof.", + "replacement": "src/test/java/zone/moddev/mc/advantageworks/Native117PackMetadataTest.java" + }, + "src/test/java/zone/moddev/mc/advantageworks/Native116PlayerIoTest.java": { + "historicalSha256": "2DC2175E84BBCE328CE52160F45B74F90F9980F1FE29D60A2E83A107C93529BB", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof.", + "replacement": "src/test/java/zone/moddev/mc/advantageworks/Native117PlayerIoTest.java" + }, + "src/test/java/zone/moddev/mc/advantageworks/Native116TestFlatTest.java": { + "historicalSha256": "47DF40DD9E5D6D2299802FF5DACD8A4D3F1EFE0AEC14C90DA873BEF88A009E9B", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof.", + "replacement": "src/test/java/zone/moddev/mc/advantageworks/Native117TestFlatTest.java" + }, + "src/test/java/zone/moddev/mc/advantageworks/NativeTicketFixtureTest.java": { + "historicalSha256": "D48FF95723F708A3FD9BD643A85E57DE51A722FCCC4B9290BA112A8205856F93", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/ProfileManifestTest.java": { + "historicalSha256": "EC9FCF606EC04EAE921D499949CD8AE47A83447C891EB8BB9E5205AB95CA6E38", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/SteamOperationalSourceContractTest.java": { + "historicalSha256": "80C9DAB8F04C1D18BD868F9587A661049F6C4778BE4B4473DE72E61F0CB911E9", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/SteamReloadControlHoldTest.java": { + "historicalSha256": "A7AA35D95439998A12EA4580B903787C61D4F433390E48AF857287620D5841F4", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/WorksDefinitionTest.java": { + "historicalSha256": "81305D912FE1632562B9039CB62D111FB025F4044A9A634F531838395D46C236", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/WorksNumericAssertionTest.java": { + "historicalSha256": "A5613E01A15D841C43737AC074EE9AA68509E034547056FDF4883B03A1FB33EC", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/WorksWorldGuardTest.java": { + "historicalSha256": "4DAA6387A93E12E8DEB14064D3C504FE6FCCAE4E0D94D13CCB233834DA9F7E5D", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/WorldgenProtocolContractTest.java": { + "historicalSha256": "FB4BF80586761F6E37FBCF29AFD6E5CAC1A48F4AF7C2683CDBDE0CE71E812CD9", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/WorldgenSamplingJobTest.java": { + "historicalSha256": "F8B14BB8E6C0AA4D185B9E6812C6090AB0B8D0FB751B33EDE61BE46DE16B9604", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/runner/HarnessOutputIsolationTest.java": { + "historicalSha256": "162CAE79A96588EE060260A97DB7DF711E66A9464F062613AE389FB976FA301A", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/runner/Native114ProfileTest.java": { + "historicalSha256": "E2B3E1147D85E54D4CF3AB17487496B45416CD42C0BFF131F54E044F9F3278F1", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/runner/Native115ProfileTest.java": { + "historicalSha256": "60C82F2F0E24EDB68C43627386B63F7ACB3B328096A3E36F9B57469D22F80BAC", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/runner/Native116ProfileTest.java": { + "historicalSha256": "884115720B1F1543D83F4D8BFFE2EE5F1BA81CEB8E327E77432E0227777808F6", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof.", + "replacement": "src/test/java/zone/moddev/mc/advantageworks/runner/Native117ProfileTest.java" + }, + "src/test/java/zone/moddev/mc/advantageworks/runner/PreparedServerSeedTest.java": { + "historicalSha256": "B2377D0A1D4A5255902D8415454D4C3E94CD92C9581B1AC3BFBB2D7C5F655312", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/runner/StationResultValidationTest.java": { + "historicalSha256": "942C2F78EB01189D8C7DF9052C22579EE0B5ECBE9140AC3F105B76516662F1D3", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/runner/WorldgenCompletionTest.java": { + "historicalSha256": "1FF86D9EBDE1594F5FF47BAEDA82E2317E94DD34158A30EEA1C4CA16D39FB02D", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + }, + "src/test/java/zone/moddev/mc/advantageworks/runner/WorldgenFailureCleanupTest.java": { + "historicalSha256": "4873204E289443F7400FF4573E6A8A33B8A9278AA148315392248B5214437B75", + "kind": "native-java-adaptation", + "reason": "Parent-owned Forge 37 native type descriptors, constructors, entity storage, ticks, rendering or typed test adaptation; not historical byte-identity proof." + } + }, + "documentationRevisions": { + "client-resources/README.md": { + "previousAuditedSha256": "97871B6043D71531B2F51A2131F855CED2C1E41DCFABBA46A7635983852F38FB", + "nativeSha256": "FA7900D9190957B07A915597404AA63027B2E0C950BA26C3ED0F2424FA651595", + "scope": "Human instructions only: Java 16, native117 recipe exports, current versions and truthful probe limits; no Java, PowerShell, Python, numerical oracle, GUI label or station change. The earlier audited receipt and historical hashes remain intact." + } + }, + "nativeClientAdaptationsSha256": "B7D9EC6E6146AE08A0838D187E52E353D836B327919B705026B84E9FB57098B7", + "nativeClientPreservation": { + "baselineCommit": "c310d0ca63ad9a8408dd4bba75172afafa5c71b7", + "receipt": "sidecar-summary-20261031-050041-c7861af6.json", + "receiptSha256": "AEB5F04F1B9F0FF55C4BFA8F669630DEEB1844E046DF11039F9B6CD6F8C2CF7A", + "status": "STATIC_SIDECAR_PASS", + "preservationChecks": 57, + "geometryContractChecks": 78, + "mutationControlsRejected": 28, + "negativeControlReceipt": "sidecar-negative-controls/20261031-045932-0e28b9e5ba234e728a49c4537475dd59/results.json", + "negativeControlReceiptSha256": "2D530DE914D9534B15E70878867623AECA6A69ACD1203DE1BB0388F95AF98FE4", + "scope": "Existing parent-approved static preservation evidence only, not rendered-client, world or gameplay acceptance. auditedNativeSha256 values come from that receipt's immutable input inventory, never from re-fingerprinting current files as historical baselines." + } +} diff --git a/test-rig/migration/native117-transformer-order-probe/TransformerOrderBridge.java b/test-rig/migration/native117-transformer-order-probe/TransformerOrderBridge.java new file mode 100644 index 00000000..040b10dd --- /dev/null +++ b/test-rig/migration/native117-transformer-order-probe/TransformerOrderBridge.java @@ -0,0 +1,267 @@ +package qualification.order; + +import cpw.mods.modlauncher.api.ITransformer; +import cpw.mods.modlauncher.api.ITransformerVotingContext; +import cpw.mods.modlauncher.api.TransformerVoteResult; +import com.google.gson.Gson; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +/** Disposable Forge 37 observer. Delegates are real coremods; their functions are never replaced. */ +public final class TransformerOrderBridge { + private static final String OWNER = "qualification/order/TransformerOrderBridge"; + private static final String ENGINE = "net/minecraftforge/coremod/CoreModEngine.class"; + private static final String[] PAIRED = {"world_info", "chunk_status", "worldgen_guard"}; + private static final String ENTITY = "poweradvantage_separate_entity_storage"; + private static final Gson JSON = new Gson(); + private static int sequence; + + private TransformerOrderBridge() {} + + private static String name(ITransformer transformer) { + String[] labels = transformer.labels(); + if (labels.length < 2) throw new IllegalStateException("Missing real coremod labels"); + return labels[labels.length - 1]; + } + + public static List> configure(List> input) throws IOException { + if (!Files.isRegularFile(Paths.get("advantage-migration.allow"))) { + throw new IllegalStateException("Unmarked order qualification runtime"); + } + String mode = new String(Files.readAllBytes(Paths.get("transformer-order.mode")), + StandardCharsets.UTF_8).trim(); + if (!Arrays.asList("natural", "bm-first", "pa-first").contains(mode)) { + throw new IllegalArgumentException("Unknown order: " + mode); + } + List> output = new ArrayList<>(input); + List natural = names(input); + for (String suffix : PAIRED) { + int bm = find(output, "basemetals_legacy_" + suffix); + int pa = find(output, "poweradvantage_legacy_" + suffix); + if (!mode.equals("natural") && ((bm < pa) != mode.equals("bm-first"))) { + ITransformer saved = output.get(bm); + output.set(bm, output.get(pa)); + output.set(pa, saved); + } + } + // There is no Base Metals entity-storage partner. Observe it without inventing an order pair. + find(output, ENTITY); + Map registration = new LinkedHashMap<>(); + registration.put("event", "registration"); + registration.put("bridge", "native117-v1"); + registration.put("mode", mode); + registration.put("natural", natural); + registration.put("forced", names(output)); + record(registration); + for (int i = 0; i < output.size(); i++) { + ITransformer transformer = output.get(i); + if (selected(name(transformer))) { + @SuppressWarnings("unchecked") + ITransformer delegate = (ITransformer) transformer; + output.set(i, new Observed(delegate)); + } + } + return output; + } + + private static int find(List> values, String expected) { + int found = -1; + for (int i = 0; i < values.size(); i++) { + if (!name(values.get(i)).equals(expected)) continue; + if (found != -1) throw new IllegalStateException("Duplicate transformer: " + expected); + found = i; + } + if (found == -1) throw new IllegalStateException("Missing transformer: " + expected); + return found; + } + + private static List names(List> values) { + List result = new ArrayList<>(); + for (ITransformer value : values) result.add(name(value)); + return result; + } + + private static boolean selected(String name) { + if (name.equals(ENTITY)) return true; + for (String suffix : PAIRED) { + if (name.equals("basemetals_legacy_" + suffix) + || name.equals("poweradvantage_legacy_" + suffix)) return true; + } + return false; + } + + private static synchronized void record(Map event) throws IOException { + event.put("sequence", ++sequence); + Files.write(Paths.get("transformer-order-events.jsonl"), + (JSON.toJson(event) + "\n").getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE, StandardOpenOption.APPEND); + } + + private static List> hooks(ClassNode node) { + List> result = new ArrayList<>(); + for (MethodNode method : node.methods) { + int index = 0; + for (AbstractInsnNode instruction : method.instructions) { + if (instruction.getOpcode() < 0) continue; + if (instruction instanceof MethodInsnNode) { + MethodInsnNode call = (MethodInsnNode) instruction; + if (call.owner.endsWith("/migration/LegacyWorldDataHook")) { + Map entry = new LinkedHashMap<>(); + entry.put("method", method.name + method.desc); + entry.put("index", index); + entry.put("owner", call.owner); + entry.put("name", call.name); + entry.put("descriptor", call.desc); + result.add(entry); + } + } + index++; + } + } + return result; + } + + private static byte[] bytes(ClassNode node) { + ClassWriter writer = new ClassWriter(0); + node.accept(writer); + return writer.toByteArray(); + } + + private static void dump(Map event, String phase, ClassNode node, String transformer) + throws Exception { + byte[] bytes = bytes(node); + Path path = Paths.get("transformer-order-bytecode", node.name + "-" + transformer + "-" + phase + ".class"); + Files.createDirectories(path.getParent()); + Files.write(path, bytes, StandardOpenOption.CREATE_NEW); + event.put(phase, hooks(node)); + event.put(phase + "Dump", path.toString().replace('\\', '/')); + event.put(phase + "Sha256", hex(MessageDigest.getInstance("SHA-256").digest(bytes))); + } + + private static final class Observed implements ITransformer { + private final ITransformer delegate; + + Observed(ITransformer delegate) { this.delegate = delegate; } + + @Override public ClassNode transform(ClassNode input, ITransformerVotingContext context) { + try { + Map event = new LinkedHashMap<>(); + event.put("event", "application"); + event.put("transformer", name(delegate)); + event.put("delegate", delegate.getClass().getName()); + event.put("labels", Arrays.asList(delegate.labels())); + event.put("class", input.name); + dump(event, "before", input, name(delegate)); + ClassNode result = delegate.transform(input, context); + if (!result.name.equals(input.name)) throw new IllegalStateException("Coremod changed class identity"); + dump(event, "after", result, name(delegate)); + record(event); + return result; + } catch (Exception ex) { + throw new IllegalStateException("Order qualification observation failed", ex); + } + } + + @Override public TransformerVoteResult castVote(ITransformerVotingContext context) { + return delegate.castVote(context); + } + @Override public Set targets() { return delegate.targets(); } + @Override public String[] labels() { return delegate.labels(); } + } + + private static String hex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte value : bytes) result.append(String.format("%02X", value & 255)); + return result.toString(); + } + + private static void inspect(Path file) throws IOException { + ClassNode node = new ClassNode(); + new ClassReader(Files.readAllBytes(file)).accept(node, 0); + Map value = new LinkedHashMap<>(); + value.put("class", node.name); + value.put("hooks", hooks(node)); + System.out.println(JSON.toJson(value)); + } + + /** Offline patcher: one call at the real CoreModEngine registration return. */ + public static void main(String[] args) throws Exception { + if (args.length == 2 && args[0].equals("--inspect")) { + inspect(Paths.get(args[1])); + return; + } + if (args.length != 3) throw new IllegalArgumentException("Expected original, output and classes"); + Path source = Paths.get(args[0]); + Path target = Paths.get(args[1]); + Path classes = Paths.get(args[2]); + try (ZipFile archive = new ZipFile(source.toFile()); + ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(target, StandardOpenOption.CREATE_NEW))) { + int changed = 0; + java.util.Enumeration entries = archive.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + byte[] bytes = read(archive.getInputStream(entry)); + if (entry.getName().equals(ENGINE)) { + ClassNode node = new ClassNode(); + new ClassReader(bytes).accept(node, 0); + for (MethodNode method : node.methods) { + if (!method.name.equals("initializeCoreMods") || !method.desc.equals("()Ljava/util/List;")) continue; + for (AbstractInsnNode instruction : method.instructions.toArray()) { + if (instruction.getOpcode() != Opcodes.ARETURN) continue; + method.instructions.insertBefore(instruction, new MethodInsnNode(Opcodes.INVOKESTATIC, + OWNER, "configure", "(Ljava/util/List;)Ljava/util/List;", false)); + changed++; + } + } + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); + node.accept(writer); + bytes = writer.toByteArray(); + } + ZipEntry copied = new ZipEntry(entry.getName()); + copied.setTime(entry.getTime()); + out.putNextEntry(copied); + out.write(bytes); + out.closeEntry(); + } + if (changed != 1) throw new IllegalStateException("Expected one registration return site, got " + changed); + for (String file : Arrays.asList("TransformerOrderBridge.class", "TransformerOrderBridge$Observed.class")) { + ZipEntry entry = new ZipEntry("qualification/order/" + file); + entry.setTime(0); + out.putNextEntry(entry); + out.write(Files.readAllBytes(classes.resolve(entry.getName()))); + out.closeEntry(); + } + } + System.out.println("Patched only CoreModEngine.initializeCoreMods registration return"); + } + + private static byte[] read(java.io.InputStream input) throws IOException { + try (java.io.InputStream stream = input; java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream()) { + byte[] buffer = new byte[8192]; + int count; + while ((count = stream.read(buffer)) != -1) output.write(buffer, 0, count); + return output.toByteArray(); + } + } +} diff --git a/test-rig/migration/native117.py b/test-rig/migration/native117.py new file mode 100644 index 00000000..45188aad --- /dev/null +++ b/test-rig/migration/native117.py @@ -0,0 +1,313 @@ +"""Separate native117 destination pinning and frozen-copy migration witness runner. + +Never builds or launches a source. Historical matrix/connected/actor campaigns remain +separate and their failures are not reclassified by this destination-only runner. +""" + +import argparse +import json +from pathlib import Path +import re +import tomllib +import zipfile +import shutil +import time +import os + +import nbtlib + +from matrix import (WORLD, compare, copy_fixture, differences, digest, fingerprint, + normalized, parse_nbt, plain, stage as historical_stage, write_json) +from native117_code import INVENTORY, Server, verify_code, verify_if_sealed +from native117_identity import target_id + + +RIG = Path(__file__).resolve().parents[1] +LEGACY_POWER = RIG.parents[2] / "PA113/PowerAdvantage" +LAUNCHER = "libraries/net/minecraftforge/forge/1.17.1-37.1.1/" + ("win_args.txt" if os.name == "nt" else "unix_args.txt") +TEST_FLAT = "advantageworkstest:test_flat" + + +def stage(runtime, target): + verify_if_sealed(target[0]) + launcher = historical_stage(runtime, target) + if (target[0] / INVENTORY).is_file(): + shutil.copy2(target[0] / INVENTORY, runtime / INVENTORY) + verify_code(runtime) + configure_creation(runtime) + return launcher + + +def configure_creation(runtime): + # Forge's factory supplies registry-aware objects; vanilla flat JsonOps cannot. + # Only launch properties change. An imported level.dat is never rewritten. + settings = {"level-type": TEST_FLAT, "generate-structures": "false", "generator-settings": ""} + path = runtime / "server.properties" + lines = [line for line in path.read_text().splitlines() if line.partition("=")[0] not in settings] + path.write_text("\n".join(lines) + "\n" + "".join(f"{key}={value}\n" for key, value in settings.items())) + + +def boot(runtime, target, java, port): + launcher = stage(runtime, target) + properties = runtime / "server.properties" + properties.write_text(properties.read_text().replace("server-port=25584", f"server-port={port}")) + return launcher + + +def native_normalized(value, enchantments=None): + result = normalized(value, enchantments) + # Native 1.14 LockCode.write omits an empty lock. Give only vanilla chest + # roots their canonical unlocked value, retaining strict checks on real locks. + if isinstance(result, dict) and result.get("id") == "minecraft:chest": + result.setdefault("Lock", "") + return result + + +def source_normalized(value, enchantments=None): + def remap_nbt(entry): + if isinstance(entry, list): + return [remap_nbt(row) for row in entry] + if not isinstance(entry, dict): + return entry + return {key: target_id(row) if key in ("id", "FluidName", "FluidID") else remap_nbt(row) + for key, row in entry.items()} + + return remap_nbt(native_normalized(value, enchantments)) + + +def compare_upgraded(source, target): + before = json.loads(source.read_text(encoding="utf-8")) + after = json.loads(target.read_text(encoding="utf-8")) + if len(before) != len(after): + raise AssertionError("Witness count changed") + + def enchantments(snapshot): + level = nbtlib.load(snapshot.parent / WORLD / "level.dat") + entries = level.get("FML", {}).get("Registries", {}).get("minecraft:enchantments", {}).get("ids", ()) + return {int(entry["V"]): str(entry["K"]) for entry in entries} + + old_ids, new_ids = enchantments(source), enchantments(target) + findings = [] + counts = {"blocks": len(before), "tiles": 0, "items": 0} + for old, new in zip(before, after): + pos = f"{old['name']}@{old['x']},{old['y']},{old['z']}" + if any(old[field] != new[field] for field in ("name", "x", "y", "z")): + raise AssertionError("Witness moved or reordered at " + pos) + expected_id = target_id(old["actualName"]) + if expected_id != new["actualName"]: + findings.append({"path": pos, "expected": expected_id, "actual": new["actualName"]}) + findings.extend(differences(old["properties"], new["properties"], pos + ".state")) + if "nbt" in old: + counts["tiles"] += 1 + old_nbt = source_normalized(plain(parse_nbt(old)), old_ids) + counts["items"] += sum(int(item.get("Count", 0)) for item in old_nbt.get("Items", ())) + if "nbt" not in new: + findings.append({"path": pos + ".tile", "expected": "tile", "actual": ""}) + else: + findings.extend(differences(old_nbt, native_normalized(plain(parse_nbt(new)), new_ids), pos + ".nbt")) + elif "nbt" in new: + findings.append({"path": pos + ".tile", "expected": "", "actual": "tile"}) + return {"status": "FAIL" if findings else "PASS", "witnesses": counts, "differences": findings} + + +def require_clean_log(path): + verify_if_sealed(path.parent) + text = path.read_text(encoding="utf-8", errors="replace") + failures = runtime_error_lines(text) + if failures: + raise AssertionError(f"Native runtime errors in {path}: {failures[:5]}") + + +def runtime_error_lines(text): + failures = [] + for original in text.splitlines(): + line = re.sub(r"\x1b\[[0-9;]*m", "", original) + if ("/ERROR]" in line or "/FATAL]" in line + or re.search(r"\b(?:[\w$]+\.)*[\w$]+(?:Exception|Error)\b", line) + or re.search(r"Missing data pack mod:(?:poweradvantage|steamadvantage|electricadvantage)(?:\s|$)", line) + or any(marker in line for marker in ( + "Exception in thread", "Caused by:", "Failed to reload data packs", + "Encountered an unexpected exception"))): + failures.append(original) + return failures + + +def start_snapshot(runtime, java, launcher, label, build=False): + server = Server(runtime, java, launcher, label) + try: + server.snapshot(label, build) + server.send("save-all flush") + time.sleep(2) + finally: + server.close() + + +def output_path(path): + path = path.resolve() + if not path.is_relative_to((RIG / "build").resolve()): + raise ValueError("Native117 outputs must be new paths under test-rig/build") + if path.exists(): + raise FileExistsError(path) + return path + + +def require_disjoint(output, protected): + output, protected = output.resolve(), protected.resolve() + if output.is_relative_to(protected) or protected.is_relative_to(output): + raise ValueError(f"Output overlaps a protected input: {protected}") + + +def jar_identity(path, mod_id, version): + with zipfile.ZipFile(path) as archive: + text = archive.read("META-INF/mods.toml").decode("utf-8") + for fields in tomllib.loads(text).get("mods", []): + if fields.get("modId") == mod_id and fields.get("version") == version: + return + raise ValueError(f"Wrong native mod identity/version: {path}") + + +def pin_target(profile_path, seed, output): + output = output_path(output) + seed = seed.resolve() + require_disjoint(output, seed) + profile = json.loads(profile_path.read_text(encoding="utf-8")) + if (profile.get("minecraftVersion") != "1.17.1" or profile.get("mode") != "packaged" + or profile.get("portStatus") != "PORTED_UNQUALIFIED" + or profile.get("forge", {}).get("version") != "1.17.1-37.1.1"): + raise ValueError("A ported native117 packaged destination profile is required") + if not (seed / LAUNCHER).is_file() or not (seed / "libraries").is_dir(): + raise FileNotFoundError("Supply an installed native Forge 37.1.1 server seed") + server_code = verify_code(seed) + jars = [] + for mod in profile["mods"]: + if not re.fullmatch(r"[A-Fa-f0-9]{64}", mod.get("sha256") or ""): + raise ValueError("Pending native117 artifact pin: " + mod["id"]) + path = (RIG / mod["path"]).resolve() + require_disjoint(output, path) + if not mod["version"].endswith(".117011") or path.name != mod["file"] or "deobf" in path.name: + raise ValueError(f"Not a native117 packaged input: {path}") + jar_identity(path, mod["id"], mod["version"]) + actual = digest(path) + if mod.get("sha256") and actual != mod["sha256"].upper(): + raise AssertionError(f"Destination jar hash mismatch: {path}") + entry = {"path": str(path), "sha256": actual, "id": mod["id"], "version": mod["version"]} + for field in ("originPath", "sourceRef"): + if field in mod: + entry[field] = mod[field] + jars.append(entry) + if not {"basepower", "advantageworkstest", "orespawn"}.issubset({j["id"] for j in jars}): + raise ValueError("Missing native Power, OreSpawn or harness input") + write_json(output, {"target117": { + "seed": str(seed), "launcher": LAUNCHER, "launcher_sha256": digest(seed / LAUNCHER), + "jars": jars, "qualificationStatus": "UNQUALIFIED", + "profile": str(profile_path.resolve()), "profileSha256": digest(profile_path), + "legacySourceRoot": str(LEGACY_POWER), + "serverCode": server_code, + }}) + + +def pinned_profile(manifest_path, key): + entry = json.loads(manifest_path.read_text(encoding="utf-8"))[key] + def resolve(value): + return (manifest_path.parent / value).resolve() + jars = [] + for jar in entry["jars"]: + path = resolve(jar["path"]) + if digest(path) != jar["sha256"].upper(): + raise AssertionError(f"Pinned input changed: {path}") + if key == "target117": + if not jar.get("version", "").endswith(".117011"): + raise ValueError("Destination manifest contains a non-native input") + jar_identity(path, jar["id"], jar["version"]) + jars.append(path) + seed = resolve(entry["seed"]) + if key == "target117": + if entry["launcher"] != LAUNCHER or digest(seed / LAUNCHER) != entry["launcher_sha256"].upper(): + raise AssertionError("Native117 server launcher changed") + identities = [jar["id"] for jar in entry["jars"]] + if len(set(identities)) != len(identities) or not {"basepower", "advantageworkstest", "orespawn"}.issubset(identities): + raise ValueError("Destination must contain unique native Power, OreSpawn and harness inputs") + if not (seed / "libraries").is_dir(): + raise FileNotFoundError("Destination server libraries are missing") + server_code = entry.get("serverCode") + if not isinstance(server_code, dict): + raise ValueError("Native117 destination requires a sealed server code inventory") + verify_code(seed, server_code) + if json.loads((seed / INVENTORY).read_text(encoding="utf-8")) != server_code: + raise AssertionError("Native117 seed inventory receipt differs from manifest") + return seed, entry["launcher"], jars + + +def run(args): + from native117_save_io import capture_save, require_same_settings + + source = args.source.resolve() + output = output_path(args.output) + for protected in (source, args.source_manifest, args.target_manifest): + require_disjoint(output, protected) + if not re.fullmatch(r"migration-[A-Za-z0-9_-]+\.json", args.source_snapshot): + raise ValueError("Source snapshot must name an existing snapshot inside the source runtime") + # Explicit archived source manifests resolve against their original PA113 location, + # never against matrix.py's copied PA114 convenience defaults. + pinned_profile(args.source_manifest.resolve(), args.source_key) + target = pinned_profile(args.target_manifest.resolve(), "target117") + require_disjoint(output, target[0]) + before = fingerprint(source) + expected = json.loads(args.source_hashes.read_text(encoding="utf-8")) + if fingerprint(source / WORLD) != expected: + raise AssertionError("Immutable source world does not match its approved fingerprint") + snapshot = source / args.source_snapshot + if not snapshot.is_file() or not (source / "migration-fixture.json").is_file(): + raise FileNotFoundError("Missing retained source snapshot or fixture specification") + try: + launcher = stage(output, target) + copy_fixture(source, output) + results, saved = {}, {} + for label in ("converted", "restarted"): + start_snapshot(output, args.java8, launcher, label) + require_clean_log(output / f"console-{label}.log") + results[label] = compare_upgraded(snapshot, output / f"migration-{label}.json") + saved[label] = capture_save(output, label) + require_same_settings(saved["converted"], saved["restarted"]) + results["idempotence"] = compare(output / "migration-converted.json", output / "migration-restarted.json", native_normalized) + write_json(output / "state-comparison.json", results) + write_json(output / "native117-provenance.json", { + "source": str(source), "sourceManifest": str(args.source_manifest.resolve()), + "sourceManifestSha256": digest(args.source_manifest), "sourceKey": args.source_key, + "sourceSnapshotSha256": digest(snapshot), "sourceHashes": expected, + "targetManifest": str(args.target_manifest.resolve()), "targetManifestSha256": digest(args.target_manifest), + "scope": "frozen-copy-state-and-restart-only", + "nativeSaveFormat": saved, + "platformNormalization": "Only vanilla chest roots treat omitted Lock as the canonical empty string; nonempty locks remain strict.", + }) + return int(any(result["status"] != "PASS" for result in results.values())) + finally: + if fingerprint(source) != before: + raise AssertionError("Protected source runtime changed") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + pin = commands.add_parser("pin-target", help="Pin actual destination jars and an installed server, without opening worlds") + pin.add_argument("--profile", type=Path, default=RIG / "profiles/native117/packaged-current.json") + pin.add_argument("--server-seed", type=Path, required=True) + pin.add_argument("--output", type=Path, required=True) + launch = commands.add_parser("run", help="Convert only a fingerprinted copy; never construct or operate machinery") + launch.add_argument("--source", type=Path, required=True) + launch.add_argument("--source-manifest", type=Path, required=True) + launch.add_argument("--source-key", required=True) + launch.add_argument("--source-hashes", type=Path, required=True) + launch.add_argument("--source-snapshot", default="migration-source.json") + launch.add_argument("--target-manifest", type=Path, required=True) + launch.add_argument("--java8", type=Path, required=True) + launch.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.command == "pin-target": + pin_target(args.profile, args.server_seed, args.output) + return 0 + return run(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_actor_io.py b/test-rig/migration/native117_actor_io.py new file mode 100644 index 00000000..2b59eff4 --- /dev/null +++ b/test-rig/migration/native117_actor_io.py @@ -0,0 +1,91 @@ +"""Bind exact 1.16 UUID/dimension representations to unchanged historical oracles.""" + +import copy +import types +import json + +import actors +import dimensions +from matrix import normalized, parse_nbt, plain +from native117_format import actor_nbt +from native117_dimension_io import parse_dimension_nbt, validate_record +from native117_identity import target_id + + +def semantic(value, keys, registry): + data = actor_nbt(plain(value)) + result = actors.semantic(data, keys, registry) + if "id" in result and data.get("id") == "baseelectric:hydroturbine": + result["id"] = data["id"] + return result + + +def source_identities(state): + """Rebrand typed source fields only; observed IDs and user NBT stay literal.""" + result = copy.deepcopy(state) + + def remap_id(value, key="id"): + if key in value: + value[key] = target_id(value[key]) + + def items(value): + if not isinstance(value, dict): + return + for key in ("Inventory", "EnderItems", "Items", "ArmorItems", "HandItems"): + for entry in value.get(key, ()): + stack(entry) + for key in ("Item", "SaddleItem", "ArmorItem"): + stack(value.get(key)) + + def stack(value): + if not isinstance(value, dict) or "Count" not in value or "id" not in value: + return + remap_id(value) + tag = value.get("tag", {}) + for key in ("Enchantments", "StoredEnchantments"): + for enchantment in tag.get(key, ()): + remap_id(enchantment) + items(tag.get("BlockEntityTag")) + + def entity(value): + remap_id(value) + items(value) + for passenger in value.get("Passengers", ()): + entity(passenger) + + for key in ("file", "adapter", "embedded"): + items(result.get(key)) + for value in result.get("entities", {}).values(): + entity(value) + generator = result.get("generator") + if generator: + remap_id(generator, "name") + if "tile" in generator: + entity(generator["tile"]) + return result + + +def native_normalized(value, registry=None): + return normalized(actor_nbt(plain(value)), registry) + + +def bind(function, **bindings): + return types.FunctionType(function.__code__, dict(function.__globals__, **bindings), + function.__name__, function.__defaults__) + + +capture_actors = bind(actors.capture, semantic=semantic) + + +class DimensionJson: + @staticmethod + def loads(text): + return validate_record(json.loads(text)) + + +capture_dimensions = bind(dimensions.capture, semantic=semantic, normalized=native_normalized, + parse_nbt=parse_dimension_nbt, json=DimensionJson) + + +def parsed_actor(entry): + return actor_nbt(plain(parse_nbt(entry))) diff --git a/test-rig/migration/native117_actor_sources.py b/test-rig/migration/native117_actor_sources.py new file mode 100644 index 00000000..1582cf90 --- /dev/null +++ b/test-rig/migration/native117_actor_sources.py @@ -0,0 +1,88 @@ +"""Generate new Overworld actor fixtures with the actual old source serializers.""" + +import argparse +from collections import Counter +import json +from pathlib import Path +import re +import socket + +from actors import boot, phase +from matrix import WORLD, copy_fixture, digest, fingerprint, write_json +from native117 import output_path, require_disjoint + + +LAUNCHERS = {"1.10.2": "forge-1.10.2-12.18.3.2511-universal.jar", + "1.12.2": "forge-1.12.2-14.23.5.2859.jar"} +SOURCE_ERRORS = re.compile(r"/(?:ERROR|FATAL)\]|\b(?:[\w$]+\.)+[\w$]+(?:Exception|Error)\b") + + +def diagnostics(path): + return Counter(re.sub(r"^\[\d\d:\d\d:\d\d\]\s*", "", line) + for line in path.read_text(encoding="utf-8", errors="replace").splitlines() + if SOURCE_ERRORS.search(line)) + + +def run(args): + output = output_path(args.output) + approved = json.loads(args.inventory.read_text(encoding="utf-8")) + sources = {} + for proof in approved: + source = Path(proof["source"]).resolve() + name = source.name + if name in sources or name not in {"original110", "maintained112"}: + raise AssertionError("Unexpected or duplicate actor source: " + name) + require_disjoint(output, source) + if fingerprint(source) != proof["sourceFingerprint"]: + raise AssertionError("Protected actor seed differs: " + str(source)) + version = "1.10.2" if name == "original110" else "1.12.2" + jars = sorted((source / "mods").glob("*.jar")) + launcher = LAUNCHERS[version] + if not (source / launcher).is_file() or not any(j.name.startswith("AdvantageMigrationProbe-") for j in jars): + raise AssertionError("Missing real old Forge launcher or actor probe: " + name) + sources[name] = (proof, (source, launcher, jars)) + if set(sources) != {"original110", "maintained112"}: + raise AssertionError("Supply both old serializer fixtures") + with socket.socket() as port: + port.bind(("127.0.0.1", args.port)) + output.mkdir() + report = dict(status="RUNNING", scope="NEW_OVERWORLD_ACTOR_FIXTURES_NOT_ARCHIVE_RECOVERY", + inventorySha256=digest(args.inventory), fixtures={}) + try: + for name, (proof, profile) in sources.items(): + source = profile[0] + runtime = output / name + try: + launcher = boot(runtime, profile, args.java8, args.port) + copy_fixture(source, runtime) + state = phase(runtime, args.java8, launcher, False, "source", "build") + new_messages = diagnostics(runtime / "console-source.log") - diagnostics(source / "console-source.log") + if new_messages: + raise AssertionError("Unexpected old source diagnostics: " + repr(dict(new_messages))) + if (set(state["entities"]) != {"cart", "dropped-musket", "dropped-oil", "owned-wolf", "hydroturbine"} + or len(state["file"].get("Inventory", [])) < 14 or not state["file"].get("EnderItems")): + raise AssertionError("Old source did not serialize the complete actor witness set") + hashes = fingerprint(runtime / WORLD) + write_json(runtime / "fixture-hashes.json", hashes) + report["fixtures"][name] = dict(source=str(source), sourceWorldUnchanged=True, + producedWorldFingerprint=hashes, actorStateSha256=digest(runtime / "semantic-source.json"), + oldSourceDiagnostics=dict(diagnostics(runtime / "console-source.log"))) + print(name + ": new Overworld actor fixture serialized and stopped", flush=True) + finally: + if fingerprint(source) != proof["sourceFingerprint"]: + raise AssertionError("Protected seed changed: " + str(source)) + report["status"] = "PASS" + except Exception as error: + report.update(status="FAIL", error=repr(error)) + raise + finally: + write_json(output / "new-actor-source-receipt.json", report) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--inventory", type=Path, required=True, help="Exact source fingerprints from the retained campaign") + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, required=True) + run(parser.parse_args()) diff --git a/test-rig/migration/native117_actors.py b/test-rig/migration/native117_actors.py new file mode 100644 index 00000000..e79303c1 --- /dev/null +++ b/test-rig/migration/native117_actors.py @@ -0,0 +1,304 @@ +"""Native 1.17 destinations for retained actor/dimension fixtures; no source boots.""" + +import argparse +import copy +import json +from pathlib import Path +import shutil +import socket +import time +from types import SimpleNamespace + +import actors +import dimensions +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 +from native117_stopped_entities import audit_stopped +from native117_readiness import await_ready +from native117_format import DIMENSIONS + + +HISTORICAL = ("original110", "early110", "maintained110", "legacy112", "maintained112") +ROUTES = HISTORICAL + ("original110-chained-113", "maintained110-direct-113", "maintained112-direct-113") + + +def baseline(source, kind): + prefix = "dimension-semantic-" if kind == "dimensions" else "semantic-" + label = next((label for label in ("runtime-restart", "restarted", "source") + if (source / (prefix + label + ".json")).is_file()), None) + if label is None: + raise FileNotFoundError(f"No retained {kind} serializer checkpoint in {source}") + capture = capture_dimensions if kind == "dimensions" else capture_actors + return source_identities(capture(source, label)), label + + +def expected_state(value, kind, survival=False, adapter=True): + if kind == "dimensions": + result = dimensions.survival_expected(value) if survival else dimensions.strict_expected(value, adapter) + else: + result = copy.deepcopy(value) + if survival: + for entity in result["entities"].values(): + for key in ("Pos", "Rotation", "Motion"): + entity.pop(key, None) + if not adapter: + result.pop("adapter", None) + result.pop("playerWorldDimension", None) + # Embedded host data is observed at conversion, not rewritten by the server-only player adapter. + return result + + +def retain(runtime, kind, label): + filename = "dimension-fixture.json" if kind == "dimensions" else "actor-fixture.json" + definition = json.loads((runtime / filename).read_text()) + folder = runtime / ("dimension-evidence" if kind == "dimensions" else "actor-evidence") / label + folder.mkdir(parents=True, exist_ok=False) + for source, target in ((runtime / WORLD / "playerdata" / (definition["playerUuid"] + ".dat"), "player.dat"), + (runtime / WORLD / "level.dat", "level.dat")): + shutil.copy2(source, folder / target) + capture = capture_dimensions if kind == "dimensions" else capture_actors + state = capture(runtime, label, folder) + if kind == "dimensions": + state = native_dimension_state(state) + write_json(runtime / (("dimension-semantic-" if kind == "dimensions" else "semantic-") + label + ".json"), state) + return state + + +def native_dimension_state(state): + """1.14+ MobEntity drops Leashed but retains Leash; verified in native 1.16 NBT.""" + result = copy.deepcopy(state) + def visit(value): + if isinstance(value, list): + for entry in value: + visit(entry) + elif isinstance(value, dict): + if value.get("id") in ("minecraft:horse", "minecraft:wolf") and "Leashed" not in value: + value["Leashed"] = int(isinstance(value.get("Leash"), dict)) + for entry in list(value.values()): + visit(entry) + visit(result.get("entities", {})) + return result + + +def command(server, runtime, kind, operation, label): + root, complete = ("advdimensions", "ADVDIMENSIONS") if kind == "dimensions" else ("advactors", "ADVACTORS") + server.send(f"{root} {operation} {label}") + server.wait(f"{complete} COMPLETE {label}", 120) + server.send("save-all flush") + server.wait("Saved the", 45) + return retain(runtime, kind, label) + + +def release(server, runtime, kind, label): + if kind == "dimensions": + # Native leash resolution must happen before the first serializer/save. + server.send(f"advdimensions operate {label}") + server.wait(f"ADVDIMENSIONS COMPLETE {label}", 120) + return None + if kind == "actors": + # Native getChunk() loads FULL terrain but does not grant entity-ticking + # tickets. Ticket only the unchanged historical actor fixture rectangle. + server.send("forceload add 96 -32 223 47") + server.send("say NATIVE115-ACTOR-TICKETS-REQUESTED") + server.wait("NATIVE115-ACTOR-TICKETS-REQUESTED", 30) + return command(server, runtime, kind, "operate", label) + + +def raw(runtime, kind, label): + return json.loads((runtime / (("dimensions-" if kind == "dimensions" else "actors-") + label + ".json")).read_text()) + + +def await_natural_ticks(server, runtime, kind, released): + """Wait for four seconds of game time, not four seconds of host time.""" + root, complete = ("advdimensions", "ADVDIMENSIONS") if kind == "dimensions" else ("advactors", "ADVACTORS") + first = raw(runtime, kind, released)["gameTime"] + previous = first + deadline = time.monotonic() + 120 + for index in range(120): + if time.monotonic() >= deadline: + break + time.sleep(1) + label = f"{released}-clock-{index}" + server.send(f"{root} snapshot {label}") + server.wait(f"{complete} COMPLETE {label}", max(1, deadline - time.monotonic())) + current = raw(runtime, kind, label)["gameTime"] + if type(current) is not int or current < previous: + raise AssertionError("Actor world clock moved backwards or is invalid") + if current - first >= 80: + return + previous = current + raise TimeoutError(f"Actor world did not advance 80 natural ticks after {released}") + + +def ready_entities(server, runtime, kind, label): + 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] + # The unchanged native probes cover exactly chunks 6..13, -2..2. + 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) + if receipt['dimension'] != dimension: + raise AssertionError('Entity readiness observed the wrong dimension') + return receipt + + +def run_route(source, runtime, target, args): + from native117 import boot + 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) + (runtime / 'advantage-works.allow').touch() + copy_fixture(source, runtime) + fixture_name = "dimension-fixture.json" if args.kind == "dimensions" else "actor-fixture.json" + shutil.copy2(source / fixture_name, runtime / fixture_name) + if args.kind == "dimensions": + definition = json.loads((runtime / fixture_name).read_text()) + definition["nativeOnLoad"] = True + write_json(runtime / fixture_name, definition) + properties = runtime / "server.properties" + properties.write_text(properties.read_text().replace("allow-nether=false", "allow-nether=true")) + write_json(runtime / "native117-source-provenance.json", { + "source": str(source.resolve()), "sourceSnapshot": source_label, + "sourceFingerprint": before, "sourceWorldFingerprint": fingerprint(source / WORLD), + "targetManifest": str(args.target_manifest.resolve()), "targetManifestSha256": digest(args.target_manifest), + "historicalOracle": "Unchanged actor/dimension semantic, exact item, freeze and tick assertions", + }) + verdict = dimensions.verdict if args.kind == "dimensions" else actors.compare + tick = dimensions.tick_result if args.kind == "dimensions" else actors.tick_result + checks, assessments = {}, {} + + def corroborate_stopped(label): + receipt = audit_stopped(runtime, runtime / 'stopped-entity-evidence' / label) + checks['saved-entities-' + label] = dict(status=receipt['status'], + snapshotSha256=receipt['snapshotSha256'], matchedEntities=receipt['matchedEntities'], + auditedDimensions=receipt['auditedDimensions']) + if receipt['status'] != 'PASS' or receipt['matchedEntities'] < 1: + raise AssertionError('Actor route lacks nonempty stopped entity-storage proof') + + def compare(key, before, after, survival=False, adapter=True): + strict = verdict(before, after) + if args.kind == "dimensions": + assessments[key] = assess(before, after, strict, getattr(args, "native_wolf_proof", None), + "survival" if survival else "strict", adapter) + return strict + try: + server = Server(runtime, args.java8, launcher, "conversion") + try: + checks['readiness-conversion'] = ready_entities(server, runtime, args.kind, 'conversion') + if args.kind == "dimensions": + release(server, runtime, args.kind, "conversion-release") + await_natural_ticks(server, runtime, args.kind, "conversion-release") + converted = command(server, runtime, args.kind, "snapshot", "converted") + live = args.kind == "dimensions" + checks["conversion"] = compare("conversion", expected_state(expected, args.kind, survival=live, adapter=False), converted, live, False) + checks["conversion-ticks" if live else "conversion-frozen"] = (tick(raw(runtime, args.kind, "conversion-release"), raw(runtime, args.kind, "converted")) + if live else actors.freeze_result(raw(runtime, args.kind, "converted"))) + adapter = command(server, runtime, args.kind, "read", "adapter") + checks["adapter"] = compare("adapter", expected_state(expected, args.kind, survival=live), adapter, live) + if not live: + checks["adapter-frozen"] = actors.freeze_result(raw(runtime, args.kind, "adapter")) + backups = actors.backup_state(runtime) + finally: + server.close() + corroborate_stopped('conversion') + require_clean_log(runtime / "console-conversion.log") + server = Server(runtime, args.java8, launcher, "restart") + try: + checks['readiness-restart'] = ready_entities(server, runtime, args.kind, 'restart') + if args.kind == "dimensions": + release(server, runtime, args.kind, "restart-load-release") + await_natural_ticks(server, runtime, args.kind, "restart-load-release") + restarted = command(server, runtime, args.kind, "read", "restarted") + checks["restart"] = compare("restart", expected_state(expected, args.kind, survival=live), restarted, live) + if not live: + checks["restart-frozen"] = actors.freeze_result(raw(runtime, args.kind, "restarted")) + checks["idempotence"] = compare("idempotence", expected_state(adapter, args.kind, survival=live), restarted, live) + release(server, runtime, args.kind, "released") + await_natural_ticks(server, runtime, args.kind, "released") + operated = command(server, runtime, args.kind, "read", "runtime") + checks["runtime"] = compare("runtime", expected_state(expected, args.kind, survival=True), operated, True) + checks["runtime-ticks"] = tick(raw(runtime, args.kind, "released"), raw(runtime, args.kind, "runtime")) + finally: + server.close() + corroborate_stopped('restart') + require_clean_log(runtime / "console-restart.log") + server = Server(runtime, args.java8, launcher, "runtime-restart") + try: + checks['readiness-runtime-restart'] = ready_entities(server, runtime, args.kind, 'runtime-restart') + 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") + checks["runtime-restart"] = compare("runtime-restart", expected_state(expected, args.kind, survival=True), operated_again, True) + checks["restart-ticks"] = tick(raw(runtime, args.kind, "restart-released"), raw(runtime, args.kind, "runtime-restart")) + finally: + server.close() + corroborate_stopped('runtime-restart') + require_clean_log(runtime / "console-runtime-restart.log") + checks["backup-stability"] = {"status": "PASS" if actors.backup_state(runtime) == backups else "FAIL", "backups": backups} + return checks + finally: + write_json(runtime / "native117-comparison.json", checks) + if args.kind == "dimensions": + proof = getattr(args, "native_wolf_proof", None) + write_json(runtime / "native117-platform-assessment.json", { + "assessment": summarize_checks(checks, assessments), "semanticAssessments": assessments, + "proofPath": str(Path(proof).resolve()) if proof else None, + "assessorSha256": digest(Path(native117_dimension_assessment.__file__)), + }) + unchanged = fingerprint(source) == before + write_json(runtime / "native117-source-integrity.json", {"status": "PASS" if unchanged else "FAIL", "unchanged": unchanged}) + if not unchanged: + raise AssertionError(f"Protected source changed: {source}") + + +def run(args): + if args.kind == "dimensions" and getattr(args, "native_wolf_proof", None): + load_native_proof(args.native_wolf_proof) + root = output_path(args.output) + require_disjoint(root, args.fixtures) + target = pinned_profile(args.target_manifest.resolve(), "target117") + require_disjoint(root, target[0]) + with socket.socket() as port: + port.bind(("127.0.0.1", args.port)) + root.mkdir(parents=True) + results = {} + for route in args.routes: + if route not in ROUTES: + raise ValueError(f"Unknown retained route: {route}") + source = args.fixtures / route + try: + results[route] = run_route(source, root / route, target, args) + except Exception as error: + results[route] = {"campaign-error": {"status": "FAIL", "error": repr(error)}} + write_json(root / "results.json", results) + raise + write_json(root / "results.json", results) + print(route + ": " + ", ".join(k + "=" + v["status"] for k, v in results[route].items()), flush=True) + status = "FAIL" if any(check["status"] != "PASS" for route in results.values() for check in route.values()) else "PASS" + write_json(root / "summary.json", {"status": status, "kind": args.kind, "routes": args.routes, + "realClientLogin": "NOT_TESTED", "sourceFixtures": "UNCHANGED", "results": results}) + return int(status != "PASS") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--kind", choices=("actors", "dimensions"), required=True) + parser.add_argument("--fixtures", type=Path, required=True) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, choices=(25574, *range(25584, 25588)), required=True) + parser.add_argument("--native-wolf-proof", type=Path, + help="Pinned no-Power control log; separate assessment never changes raw strict exit status") + parser.add_argument("--routes", nargs="+", default=list(ROUTES)) + raise SystemExit(run(parser.parse_args())) diff --git a/test-rig/migration/native117_audit_actors.py b/test-rig/migration/native117_audit_actors.py new file mode 100644 index 00000000..dfdb10c4 --- /dev/null +++ b/test-rig/migration/native117_audit_actors.py @@ -0,0 +1,256 @@ +"""Read-only replay of native actor/dimension evidence and exact item NBT.""" + +import argparse +import json +from pathlib import Path + +import actors +from native117_actor_io import capture_actors, capture_dimensions +import dimensions +from matrix import WORLD, digest, fingerprint, write_json +from native117 import pinned_profile, require_clean_log +from native117_actors import baseline, expected_state, native_dimension_state, raw +from native117_dimension_assessment import assess, summarize_checks +import native117_dimension_assessment +from native117_readiness import require_ready +from native117_entity_storage_replay import replay as replay_entity_storage +from native117_format import DIMENSIONS +from native117_mod_inventory import InventoryError, validate_mod_inventory + + +def inherited_sources(proofs): + """Check the old receipts' original sources; never approve an old destination.""" + receipts = [] + for proof in proofs: + value = json.loads(proof.read_text()) + if "sourceFingerprint" in value: + records = [{"source": value["source"], "fingerprint": value["sourceFingerprint"]}] + elif "source" in value and "fingerprint" in value: + records = [value] + else: + records = list(value.values()) + if not records: + raise AssertionError("Empty inherited source proof: " + str(proof)) + sources = [] + for record in records: + source = Path(record.get("source", record.get("path", ""))).resolve() + expected = record["fingerprint"] + if not expected or fingerprint(source) != expected: + raise AssertionError("Original source differs from inherited proof: " + str(source)) + sources.append({"source": str(source), "fingerprint": expected}) + receipts.append({"proof": str(proof.resolve()), "proofSha256": digest(proof), "sources": sources}) + return {"status": "PASS", "scope": "Original source custody only; no destination qualification", "receipts": receipts} + + +def native_controls(runtime, kind, recorded): + 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] + result = {} + for phase, snapshot in (('conversion', 'converted'), ('restart', 'restarted'), ('runtime-restart', 'runtime-restart')): + ready = json.loads((runtime / 'advantage-works-results' / ('native-readiness-entities-' + phase + '.json')).read_text()) + require_ready(ready, (6, -2, 13, 2), 'entities-' + phase, dimension) + if ready['dimension'] != dimension or raw(runtime, kind, snapshot)['gameTime'] < ready['completedWorldTime']: + raise AssertionError('Actor snapshot preceded readiness or used another dimension') + result['readiness-' + phase] = ready + expected = recorded['saved-entities-' + phase] + proof = replay_entity_storage(runtime / 'stopped-entity-evidence' / phase, + expected_snapshot_sha256=expected['snapshotSha256']) + if proof['status'] != 'PASS' or proof['matchedEntities'] < 1: + raise AssertionError('No nonempty stopped entity-storage proof') + result['saved-entities-' + phase] = dict(status=proof['status'], snapshotSha256=proof['snapshotSha256'], + matchedEntities=proof['matchedEntities'], auditedDimensions=proof['auditedDimensions']) + return result + + +def persistence_controls(runtime, kind): + """Audit receipts from the bounded live runner; never infer hook-list contents.""" + counts = (2, 2, 1) if kind == "dimensions" else (2, 3, 2) + records = {} + markers = ("WorldPersistenceHooks.lambda$handleWorldDataLoad", + "WorldPersistenceHooks.lambda$handleWorldDataSave", + "Exception reading", "Exception stopping the server", + "An unexpected error occurred trying to execute that command", + "NullPointerException") + for label, required in zip(("conversion", "restart", "runtime-restart"), counts): + path = runtime / ("console-" + label + ".log") + if not path.is_file(): + records[label] = {"status": "FAIL", "reason": "Server log missing"} + continue + lines = path.read_text(encoding="utf-8", errors="replace").splitlines() + errors = [{"line": index + 1, "text": line} for index, line in enumerate(lines) + if "/ERROR]" in line or any(marker in line for marker in markers)] + saved = sum("Saved the" in line for line in lines) + started = any("Done (" in line for line in lines) + stopped = any("Stopping server" in line for line in lines) + records[label] = {"status": "PASS" if started and stopped and saved == required and not errors else "FAIL", + "log": str(path), "logSha256": digest(path), "started": started, + "stopMarkerPresent": stopped, "saveReceipts": saved, + "requiredSaveReceipts": required, "errors": errors} + return {"status": "PASS" if all(value["status"] == "PASS" for value in records.values()) else "FAIL", + "boots": records, "liveRunnerBudgetsSeconds": {"startup": 300, "probe": 120, + "saveReceipt": 45, "shutdown": 90}, + "boundsAndExitCode": "Enforced by unchanged native117_actors.command and matrix.Server", + "nativeHookListInspection": "Parent-owned; not inferred from absence of log errors"} + + +def replay(run, kind, persistence=False): + recorded = json.loads((run / "results.json").read_text()) + findings, routes, native_only_findings = [], {}, set() + capture = capture_dimensions if kind == "dimensions" else capture_actors + verdict = dimensions.verdict if kind == "dimensions" else actors.compare + tick = dimensions.tick_result if kind == "dimensions" else actors.tick_result + for route, checks in recorded.items(): + runtime = run / route + provenance = json.loads((runtime / "native117-source-provenance.json").read_text()) + source = Path(provenance["source"]) + if fingerprint(source) != provenance["sourceFingerprint"]: + findings.append(route + ": protected source fingerprint changed") + try: + inventory = validate_mod_inventory(runtime, provenance.get("targetManifest"), + provenance.get("targetManifestSha256")) + except InventoryError as error: + inventory = {"status": "FAIL", "scope": "EXACT_SIX_MOD_INVENTORY_ONLY", + "atomicSnapshot": False, "error": str(error)} + findings.append(route + ": exact mod inventory fails: " + str(error)) + else: + pinned_profile(Path(provenance["targetManifest"]), "target117") + controls = persistence_controls(runtime, kind) if persistence else None + if controls and controls["status"] != "PASS": + findings.append(route + ": bounded persistence controls fail") + if "campaign-error" in checks: + findings.append(route + ": aborted route has no completed behavioral qualification") + for log in runtime.glob("console-*.log"): + try: + require_clean_log(log) + except AssertionError as error: + findings.append(route + ": " + str(error)) + routes[route] = {"status": "FAIL", "recordedCampaignError": checks["campaign-error"], + "semanticReplay": "NOT_POSSIBLE_WITH_INCOMPLETE_SAVED_EVIDENCE", + "modInventory": inventory} + if persistence: + routes[route]["persistenceControls"] = controls + continue + expected, _ = baseline(source, kind) + states = {} + live = kind == "dimensions" + prefix = "dimension-semantic-" if live else "semantic-" + evidence = "dimension-evidence" if live else "actor-evidence" + for label in ("converted", "adapter", "restarted", "runtime", "runtime-restart"): + state = capture(runtime, label, runtime / evidence / label) + if live: + state = native_dimension_state(state) + stored = json.loads((runtime / (prefix + label + ".json")).read_text()) + if state != stored: + findings.append(route + "/" + label + ": stored semantic differs from raw evidence") + states[label] = state + fresh = { + "conversion": verdict(expected_state(expected, kind, survival=live, adapter=False), states["converted"]), + "adapter": verdict(expected_state(expected, kind, survival=live), states["adapter"]), + "restart": verdict(expected_state(expected, kind, survival=live), states["restarted"]), + "idempotence": verdict(expected_state(states["adapter"], kind, survival=live), states["restarted"]), + "runtime": verdict(expected_state(expected, kind, survival=True), states["runtime"]), + "runtime-restart": verdict(expected_state(expected, kind, survival=True), states["runtime-restart"]), + "runtime-ticks": tick(raw(runtime, kind, "released"), raw(runtime, kind, "runtime")), + "restart-ticks": tick(raw(runtime, kind, "restart-released"), raw(runtime, kind, "runtime-restart")), + } + if live: + fresh["conversion-ticks"] = tick(raw(runtime, kind, "conversion-release"), raw(runtime, kind, "converted")) + else: + for key, label in (("conversion-frozen", "converted"), ("adapter-frozen", "adapter"), ("restart-frozen", "restarted")): + fresh[key] = actors.freeze_result(raw(runtime, kind, label)) + backups = actors.backup_state(runtime) + fresh["backup-stability"] = {"status": "PASS" if backups == checks["backup-stability"]["backups"] else "FAIL", + "backups": backups} + try: + fresh.update(native_controls(runtime, kind, checks)) + except (AssertionError, ValueError, KeyError, OSError) as error: + findings.append(route + ': native readiness/entity storage replay fails: ' + str(error)) + for key in native117_dimension_assessment.NATIVE_CHECKS: + fresh[key] = dict(status='FAIL', error=str(error)) + platform, semantic_assessments = None, {} + if live: + sidecar = runtime / "native117-platform-assessment.json" + if not sidecar.is_file(): + findings.append(route + ": missing separate native platform assessment") + else: + recorded_platform = json.loads(sidecar.read_text()) + proof = recorded_platform.get("proofPath") + pairs = { + "conversion": (expected_state(expected, kind, survival=True, adapter=False), states["converted"], False), + "adapter": (expected_state(expected, kind, survival=True), states["adapter"], True), + "restart": (expected_state(expected, kind, survival=True), states["restarted"], True), + "idempotence": (expected_state(states["adapter"], kind, survival=True), states["restarted"], True), + "runtime": (expected_state(expected, kind, survival=True), states["runtime"], True), + "runtime-restart": (expected_state(expected, kind, survival=True), states["runtime-restart"], True), + } + semantic_assessments = {key: assess(before, after, fresh[key], proof, "survival", adapter) + for key, (before, after, adapter) in pairs.items()} + platform = {"assessment": summarize_checks(fresh, semantic_assessments), + "semanticAssessments": semantic_assessments, "proofPath": proof, + "assessorSha256": digest(Path(native117_dimension_assessment.__file__))} + if platform != recorded_platform: + findings.append(route + ": separate native assessment differs from independent replay") + platform["assessment"]["status"] = "FAIL" + strict_items = {} + for label in ("converted", "adapter", "restarted", "runtime", "runtime-restart"): + strict = expected_state(expected, kind, survival=live or label.startswith("runtime"), adapter=label != "converted") + issues = dimensions.exact_item_differences(strict, states[label]) + strict_items[label] = {"status": "FAIL" if issues else "PASS", "differences": issues} + if issues: + findings.append(route + "/" + label + ": exact nested item NBT mismatch") + for key, value in fresh.items(): + if value != checks[key]: + findings.append(route + "/" + key + ": recomputed oracle differs from recorded result") + if value["status"] != "PASS": + if semantic_assessments.get(key, {}).get("status") == "PASS_WITH_NATIVE_DIFFERENCE": + native_only_findings.add(len(findings)) + findings.append(route + "/" + key + ": behavioral assertion fails") + for label in ("conversion", "restart", "runtime-restart"): + require_clean_log(runtime / ("console-" + label + ".log")) + routes[route] = {"oracles": fresh, "exactItems": strict_items, "modInventory": inventory, + "carriedItems": sum(item["Count"] for item in expected["file"].get("Inventory", [])), + "enderItems": sum(item["Count"] for item in expected["file"].get("EnderItems", []))} + if persistence: + routes[route]["persistenceControls"] = controls + if live and platform: + routes[route]["nativePlatformAssessment"] = platform + result = {"status": "FAIL" if findings else "PASS", "kind": kind, "routes": routes, "findings": findings} + if kind == "dimensions": + summary_file = run / "summary.json" + summary = json.loads(summary_file.read_text()) if summary_file.is_file() else {} + complete = (bool(recorded) and summary.get("kind") == kind + and len(summary.get("routes", [])) == len(recorded) + and set(summary.get("routes", [])) == set(recorded)) + integrity = [finding for index, finding in enumerate(findings) if index not in native_only_findings] + statuses = [value.get("nativePlatformAssessment", {}).get("assessment", {}).get("status") + for value in routes.values()] + status = "FAIL" + if complete and not integrity and all(value in ("PASS", "PASS_WITH_NATIVE_DIFFERENCE") for value in statuses): + status = "PASS_WITH_NATIVE_DIFFERENCE" if "PASS_WITH_NATIVE_DIFFERENCE" in statuses else "PASS" + result["nativePlatformAssessment"] = {"status": status, "rawStrictStatus": result["status"], + "completeCampaign": complete, "otherFindings": integrity, + "rawNativeDifferenceFindingIndexes": sorted(native_only_findings)} + return result + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--run", type=Path) + parser.add_argument("--kind", choices=("actors", "dimensions")) + parser.add_argument("--source-proofs", type=Path, nargs="+") + parser.add_argument("--persistence-controls", action="store_true") + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.output.exists(): + raise FileExistsError(args.output) + if args.source_proofs: + result = inherited_sources(args.source_proofs) + write_json(args.output, result) + print("PASS: " + str(len(result["receipts"])) + " inherited source proofs") + raise SystemExit(0) + if args.run is None or args.kind is None: + parser.error("Replay requires run and kind, or explicit inherited source proofs") + result = replay(args.run.resolve(), args.kind, args.persistence_controls) + write_json(args.output, result) + print(result["status"] + ": " + str(len(result["routes"])) + " routes; " + str(len(result["findings"])) + " integrity findings") + raise SystemExit(int(result["status"] != "PASS")) diff --git a/test-rig/migration/native117_audit_order.py b/test-rig/migration/native117_audit_order.py new file mode 100644 index 00000000..28601d56 --- /dev/null +++ b/test-rig/migration/native117_audit_order.py @@ -0,0 +1,113 @@ +"""Read-only native coremod order, actor, block and backup evidence replay.""" + +import argparse +import json +from pathlib import Path + +import actors +from native117_actor_io import capture_actors +import transformer_order as historical +from matrix import compare, digest, fingerprint, write_json +from native117 import LEGACY_POWER, native_normalized, require_clean_log +from native117_transformer_order import COREMODS, native_order_evidence +from native117_observation_plan import audit_runtime +from native117_readiness import require_ready +from native117_entity_storage_replay import replay as replay_stopped + + +def replay(root): + results = json.loads((root / "results.json").read_text()) + sources = json.loads((root / "source-fingerprints.json").read_text()) + build = json.loads((root / "probe-build.json").read_text()) + summary = json.loads((root / "summary.json").read_text()) + findings, evidence, modes = [], {}, {} + patched = root / "coremods-order-test.jar" + if digest(patched) != build["patched_sha256"] or digest(patched) != summary["patchedLibrarySha256"]: + findings.append("Patched observer library changed") + for name, entry in sources.items(): + if fingerprint(Path(entry["source"])) != entry["fingerprint"]: + findings.append(name + ": protected source changed") + for case, recorded in results.items(): + name, mode = case.split("/") + runtime = root / (name + "-" + mode) + historical.assert_staged(runtime) + if digest(runtime / COREMODS) != digest(patched): + findings.append(case + ": staged observer library changed") + expected = json.loads((root / (name + "-mixed-input") / "semantic-source.json").read_text()) + actual = {} + proofs = {} + for label in ("converted", "converted-adapter", "restarted"): + actual[label] = capture_actors(runtime, label, runtime / "ordering-evidence" / label) + if label != "converted": + actual[label].pop("embedded", None) + custody = dict(status="PASS", proof=audit_runtime(runtime, root / "observation-plan.json", + summary["observationPlanSha256"])) + if custody != recorded["observation-code-custody"]: + findings.append(case + ": observation code custody differs from the sealed plan") + for label in ("converted", "restarted"): + proof = native_order_evidence(runtime, mode, label) + controls = recorded[("converted" if label == "converted" else "restart") + "-controls"] + if proof != controls["order"]: + findings.append(case + "/" + label + ": order proof differs from raw events/dumps") + ready_label = "entities-order-" + label + readiness = json.loads((runtime / "advantage-works-results" / + ("native-readiness-" + ready_label + ".json")).read_text()) + require_ready(readiness, (6, -2, 13, 2), ready_label) + if readiness != controls["readiness"]: + findings.append(case + "/" + label + ": entity readiness differs from raw observation") + saved = replay_stopped(runtime / "order-stopped-entities" / label, + expected_snapshot_sha256=controls["savedEntities"]["snapshotSha256"]) + if (saved["status"] != "PASS" or saved["matchedEntities"] < 1 or + any(saved[key] != controls["savedEntities"][key] for key in + ("status", "snapshotSha256", "matchedEntities", "auditedDimensions"))): + findings.append(case + "/" + label + ": saved entity replay differs/fails") + proofs[label] = proof + for prefix in ("console", "latest", "debug"): + require_clean_log(runtime / (prefix + "-" + label + ".log")) + entries = json.loads((runtime / f"migration-{label}.json").read_text()) + stock = runtime / f"migration-stock-{label}.json" + if json.loads(stock.read_text()) != entries[:-len(historical.METAL_BLOCKS)]: + findings.append(case + "/" + label + ": stock snapshot differs from raw full snapshot") + blocks = LEGACY_POWER / "run-migration-matrix" / ( + "original-cyano-20261005-r1/original110" if name == "original110" else "full-20261005-r1/maintained112") + blocks_result = compare(blocks / "migration-source.json", stock, native_normalized) + metals_result = historical.strict_equal(list(historical.METAL_BLOCKS), + [entry["actualName"] for entry in entries[-len(historical.METAL_BLOCKS):]]) + if blocks_result != recorded["blocks-" + label] or blocks_result["status"] != "PASS": + findings.append(case + "/" + label + ": source block/tile comparison differs/fails") + if metals_result != recorded["metals-" + label] or metals_result["status"] != "PASS": + findings.append(case + "/" + label + ": mixed metal comparison differs/fails") + fresh = { + "conversion": actors.compare({k: v for k, v in expected.items() if k != "adapter"}, actual["converted"]), + "adapter": actors.compare({k: v for k, v in expected.items() if k != "embedded"}, actual["converted-adapter"]), + "restart": actors.compare({k: v for k, v in expected.items() if k != "embedded"}, actual["restarted"]), + "idempotence": historical.strict_equal(actual["converted-adapter"], actual["restarted"]), + "backup-provenance": historical.backup_provenance(root / (name + "-mixed-input"), runtime), + } + for key, value in fresh.items(): + if value != recorded[key] or value["status"] != "PASS": + findings.append(case + "/" + key + ": actor or backup replay differs/fails") + modes.setdefault(name, {})[mode] = {"actors": actual["converted-adapter"], "actorsRestart": actual["restarted"], + "blocks": historical.block_semantics(runtime, "converted"), "blocksRestart": historical.block_semantics(runtime, "restarted")} + evidence[case] = {"oracles": fresh, "proofs": proofs} + cross = {} + for name, values in modes.items(): + reference = next(iter(values.values())) + cross[name] = {mode: historical.strict_equal(reference, value) for mode, value in values.items()} + if cross[name] != summary["crossOrder"][name] or any(result["status"] != "PASS" for result in cross[name].values()): + findings.append(name + ": cross-order actor/block/tile semantics differ") + return {"status": "FAIL" if findings else "PASS", "runtimes": len(results), "findings": findings, + "crossOrder": cross, "evidence": evidence, "backupCaveat": "Individual mod backup original-byte custody depends on callback order"} + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--run", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.output.exists(): + raise FileExistsError(args.output) + result = replay(args.run.resolve()) + write_json(args.output, result) + print(result["status"] + ": " + str(result["runtimes"]) + " runtimes; " + str(len(result["findings"])) + " integrity findings") + raise SystemExit(int(result["status"] != "PASS")) diff --git a/test-rig/migration/native117_audit_respawn.py b/test-rig/migration/native117_audit_respawn.py new file mode 100644 index 00000000..19b6e116 --- /dev/null +++ b/test-rig/migration/native117_audit_respawn.py @@ -0,0 +1,160 @@ +"""Read-only replay of native respawn quantities, source items and saved players.""" + +import argparse +from pathlib import Path + +import nbtlib +from actors import enchantments +from native117_actor_io import semantic +from dimensions import PLAYER_STATE, exact_item_differences +from matrix import WORLD, differences, digest, fingerprint, parse_nbt, write_json +from native117 import pinned_profile, require_clean_log +from native117_actors import ROUTES, baseline +from native117_respawn import (native_respawn_verdict, REQUEST_PROTOCOL, REQUEST_INDEX, + CASE_LABELS, read_json, verify_request, verify_request_files) +from respawn import spawn_reference + + +def request_integrity(root, sources, results): + findings, bindings = [], {} + expected = {route + "/" + label for route in sources for label in CASE_LABELS} + if not sources or any(route not in ROUTES for route in sources): + findings.append("Respawn request routes are empty or unknown") + try: + index = read_json(root / REQUEST_INDEX) + if (not isinstance(index, dict) or set(index) != {"protocol", "cases"} + or index["protocol"] != REQUEST_PROTOCOL or not isinstance(index["cases"], dict)): + raise ValueError("Unknown respawn request index protocol") + if set(index["cases"]) != expected or set(results) != expected: + findings.append("Published request/result case coverage is incomplete or has extra cases") + for route, source in sources.items(): + if route not in ROUTES: + continue + runtime = root / route + try: + base = runtime / "dimension-fixture.json" + checksum = source.get("baseFixtureSha256") + if (not checksum or base.is_symlink() or digest(base) != checksum + or digest(Path(source["path"]) / "dimension-fixture.json") != checksum): + raise AssertionError("Immutable root fixture bytes differ from the source") + verify_request_files(runtime, CASE_LABELS) + except (OSError, ValueError, AssertionError) as error: + findings.append(route + ": " + str(error)) + for label in CASE_LABELS: + case = route + "/" + label + try: + entry = index["cases"].get(case) + if not isinstance(entry, dict) or set(entry) != {"request", "recordSha256"}: + raise AssertionError("Missing or incomplete case request binding") + path = runtime / ("dimensions-" + label + ".json") + if digest(path) != entry["recordSha256"]: + raise AssertionError("Native case record differs from its published binding") + record = read_json(path) + binding = verify_request(runtime, label, record, entry["request"]) + if binding["baseFixtureSha256"] != source.get("baseFixtureSha256"): + raise AssertionError("Request refers to another root fixture") + copy = runtime / "respawn-evidence" / label / "request.json" + if copy.is_symlink() or digest(copy) != binding["sha256"]: + raise AssertionError("Archived closed request differs from the consumed request") + bindings[case] = binding + except (OSError, ValueError, AssertionError, KeyError, TypeError) as error: + findings.append(case + ": " + str(error)) + except (OSError, ValueError, TypeError, KeyError) as error: + findings.append("Respawn request index invalid: " + str(error)) + return {"protocol": REQUEST_PROTOCOL, "findings": findings, "bindings": bindings, + "expectedCases": len(expected), "verifiedCases": len(bindings)} + + +def replay(root, target_manifest=None): + results = read_json(root / "results.json") + sources = read_json(root / "source-fingerprints.json") + summary_file = root / "summary.json" + summary = read_json(summary_file) if summary_file.is_file() else None + if summary is None and target_manifest is None: + raise ValueError("Aborted campaigns require an explicit pinned target manifest") + manifest = Path(summary["targetManifest"]) if summary else target_manifest.resolve() + if summary and digest(manifest) != summary["targetManifestSha256"]: + raise AssertionError("Respawn target manifest changed") + pinned_profile(manifest, "target117") + findings, fresh = [], {} + if summary is None: + findings.append("Campaign summary missing: aborted run is not a completed qualification") + elif summary.get("status") != "PASS": + findings.append("Campaign summary does not record a complete pass") + requests = request_integrity(root, sources, results) + findings.extend(requests["findings"]) + if summary and (summary.get("requestProtocol") != REQUEST_PROTOCOL + or not (root / REQUEST_INDEX).is_file() + or summary.get("requestIndexSha256") != digest(root / REQUEST_INDEX)): + findings.append("Campaign request protocol/index hash is missing or changed") + for route, source_entry in sources.items(): + if route not in ROUTES: + findings.append("Unknown or unsafe respawn source route") + continue + source = Path(source_entry["path"]) + if fingerprint(source) != source_entry["fingerprint"]: + findings.append(route + ": protected source changed") + runtime = root / route + expected_file = baseline(source, "dimensions")[0]["file"] + expected_items = {key: expected_file[key] for key in ("Inventory", "EnderItems")} + for jar in read_json(runtime / "input-jars.json"): + if digest(runtime / "mods" / jar["file"]) != jar["sha256"]: + findings.append(route + ": staged mod changed") + for label in ("respawn", "respawn-restart"): + log = runtime / ("console-" + label + ".log") + if not log.is_file(): + findings.append(route + "/" + label + ": server log missing") + continue + try: + require_clean_log(log) + except AssertionError as error: + findings.append(route + "/" + label + ": " + str(error)) + for case in (case for case in results if case.startswith(route + "/")): + label = case.split("/")[1] + if label not in CASE_LABELS or case != route + "/" + label: + findings.append(case + ": unsafe or unknown case label") + continue + folder = runtime / "respawn-evidence" / label + record = read_json(runtime / f"dimensions-{label}.json") + registry = enchantments(runtime) + result = native_respawn_verdict(record, registry, spawn_reference(folder / "level.dat")) + before = semantic(parse_nbt(record["respawn"]["before"]), PLAYER_STATE, registry) + source_issues = differences(expected_items, before, "source-inventory") + source_issues.extend(exact_item_differences(expected_items, before, "source-inventory")) + result["differences"].extend(source_issues) + observed = semantic(parse_nbt(record["respawn"]["after"]), PLAYER_STATE, registry) + saved = semantic(nbtlib.load(folder / "player.dat").unpack(), PLAYER_STATE, registry) + save_issues = differences(observed, saved, "saved-player") + exact_item_differences(observed, saved, "saved-player") + result["differences"].extend(save_issues) + if source_issues or save_issues: + result["status"] = "FAIL" + fresh[case] = result + if result != results[case]: + findings.append(case + ": raw oracle result differs from recorded result") + if result["status"] != "PASS": + findings.append(case + ": behavioral assertion fails") + expected_cases = {route + "/" + boot + "-" + keep + "-" + mode + for route in sources for boot in ("respawn", "respawn-restart") + for keep in ("keep", "drop") for mode in ("forced", "bed", "missing")} + missing = sorted(expected_cases - fresh.keys()) + if missing or (summary and len(fresh) != summary["cases"]) or len(fresh) != len(results): + findings.append("Respawn case coverage is incomplete") + return {"status": "FAIL" if findings else "PASS", "cases": len(fresh), "findings": findings, + "expectedCases": len(expected_cases), "missingCases": missing, + "results": fresh, "realClientDeathScreen": "NOT_TESTED", + "requestIntegrity": requests, + "targetManifest": str(manifest), "targetManifestSha256": digest(manifest)} + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--run", type=Path, required=True) + parser.add_argument("--target-manifest", type=Path) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.output.exists(): + raise FileExistsError(args.output) + result = replay(args.run.resolve(), args.target_manifest) + write_json(args.output, result) + print(result["status"] + ": " + str(result["cases"]) + " cases; " + str(len(result["findings"])) + " integrity findings") + raise SystemExit(int(result["status"] != "PASS")) diff --git a/test-rig/migration/native117_broad.py b/test-rig/migration/native117_broad.py new file mode 100644 index 00000000..29803aac --- /dev/null +++ b/test-rig/migration/native117_broad.py @@ -0,0 +1,329 @@ +"""Bounded native117 walkable-bay sweep with initial, movement and restart oracles. + +Production jars come only from a new immutable manifest. Historical station +definitions and failed broad runs remain unchanged. No source world is launched. +""" + +import argparse +from collections import Counter +import json +import math +from pathlib import Path +import shutil +import struct +import time +import traceback +from broad_campaign_status import campaign_document, campaign_status + +import nbtlib +from matrix import digest, plain, stage, write_json +from native117_code import Server +from native117 import stage +from native117 import RIG, output_path, pinned_profile, require_disjoint +from native117_lifecycle import require_native_boot, verify_staged + + +DIRECTIONS = ("down", "up", "north", "south", "west", "east") +REAR = ((0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1), (1, 0, 0), (-1, 0, 0)) +DRILL_BAYS = ("S-05", "E-06") + + +def read_json(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def drill_cases(station): + result = [] + for assertion in station["assertions"]: + if assertion.get("block") not in ("basesteam:steam_drill", "baseelectric:electric_drill"): + continue + direction = assertion.get("meta", DIRECTIONS.index(assertion.get("expectedText", "down"))) + origin = tuple(a + b for a, b in zip(station["origin"], assertion["pos"])) + rear = tuple(a + b for a, b in zip(origin, REAR[direction])) + result.append(dict(id=station["id"] + ":" + assertion["id"], origin=origin, rear=rear, + facing=DIRECTIONS[direction], block=assertion["block"])) + if len(result) != 6 or {r["facing"] for r in result} != set(DIRECTIONS): + raise AssertionError("The original six-facing bay definition changed") + return result + + +def nbt(row): + return plain(nbtlib.parse_nbt(row["nbt"])) if "nbt" in row else {} + + +def drill_state(station, document, *, initial=False): + rows = {tuple(row["pos"]): row for row in document["snapshot"]} + if len(rows) != len(document["snapshot"]): + raise AssertionError("Duplicate snapshot positions") + cases = drill_cases(station) + stock = Counter() + for row in rows.values(): + for stack in nbt(row).get("Items", []): + marker = stack.get("tag", {}).get("AdvBayStock") + if marker: + if stack.get("id") != "minecraft:diamond": + raise AssertionError("Inventory witness item identity changed") + stock[marker] += int(stack["Count"]) + expected_positions, result = set(), {} + for case in cases: + candidates = [rows[pos] for pos in (case["origin"], case["rear"]) + if pos in rows and rows[pos]["block"] == case["block"]] + if len(candidates) != 1: + raise AssertionError(case["id"] + ": missing or duplicate drill in declared cells") + row = candidates[0] + pos = tuple(row["pos"]) + if initial and pos != case["origin"]: + raise AssertionError(case["id"] + ": initial placement moved before activation") + if initial: + track = "basesteam:steam_track" if station["id"] == "S-05" else "baseelectric:electric_track" + if rows.get(case["rear"], {}).get("block") != track: + raise AssertionError(case["id"] + ": initial declared rear track is missing") + if "facing=" + case["facing"] not in row["state"]: + raise AssertionError(case["id"] + ": block facing changed") + data = nbt(row) + if data.get("ForgeData", {}).get("AdvBayWitness") != case["id"]: + raise AssertionError(case["id"] + ": persistent moved tile witness lost/replaced") + if tuple(int(data[axis]) for axis in ("x", "y", "z")) != pos: + raise AssertionError(case["id"] + ": tile coordinates do not match its block") + if stock[case["id"]] != 7: + raise AssertionError(case["id"] + ": inventory stock lost or duplicated") + energy = data.get("Energy", []) + if len(energy) != 1: + raise AssertionError(case["id"] + ": energy witness missing") + value = struct.unpack("!f", struct.pack("!I", int(energy[0]) & 0xffffffff))[0] + capacity = 50 if station["id"] == "S-05" else 500 + if not math.isfinite(value) or not 0 <= value <= capacity: + raise AssertionError(case["id"] + ": invalid energy") + expected_positions.add(pos) + result[case["id"]] = dict(pos=list(pos), facing=case["facing"], block=row["block"], + tile=data["id"], witness=case["id"], stock=7, + energyBits=[int(e) for e in energy], energy=value, + moved=pos == case["rear"]) + actual_positions = {pos for pos, row in rows.items() + if row["block"] in ("basesteam:steam_drill", "baseelectric:electric_drill")} + if actual_positions != expected_positions or set(stock) != {case["id"] for case in cases}: + raise AssertionError("Extra/off-track drill or inventory witness in bay") + return result + + +def operating_state(initial, operated): + if initial.keys() != operated.keys() or len(initial) != 12: + raise AssertionError("Twelve unique drill witnesses are required") + for key in initial: + for field in ("facing", "block", "tile", "witness", "stock"): + if initial[key][field] != operated[key][field]: + raise AssertionError(key + ": movement changed " + field) + # These two original connected lanes are the Q-RIG-001 reproduction. + for key in ("S-05:down-facing", "E-06:up-facing"): + if not operated[key]["moved"]: + raise AssertionError(key + ": the broad run did not exercise actual track movement") + return dict(status="PASS", drills=12, moved=sum(v["moved"] for v in operated.values())) + + +def restarted_state(before, after): + if before != after: + changed = [key for key in set(before) | set(after) if before.get(key) != after.get(key)] + raise AssertionError("Restart changed position, identity, facing, stock or energy: " + str(changed)) + return dict(status="PASS", drills=len(after), exactEnergy=True) + + +def assertions_state(records, stations): + 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": + raise AssertionError("Wrong bay/target report") + if document["status"] not in ("PASS", "MANUAL_REQUIRED"): + raise AssertionError(key + ": " + document["status"]) + expected = next(s for s in stations if s["id"] == key) + if (len(document["assertions"]) != len(expected["assertions"]) + or {a["id"] for a in document["assertions"]} != {a["id"] for a in expected["assertions"]}): + raise AssertionError(key + ": assertion set changed") + for assertion in document["assertions"]: + counts[assertion["status"]] += 1 + if assertion["status"] not in ("PASS", "SKIP"): + raise AssertionError(key + "/" + assertion["id"] + ": " + assertion["status"]) + if assertion["status"] == "SKIP": + spec = next(a for a in expected["assertions"] if a["id"] == assertion["id"]) + if not set(spec.get("requiredMods", [])) - set(document["mods"]): + raise AssertionError(key + "/" + assertion["id"] + ": unexplained skip") + if document["manualChecks"]: + manual.append(key) + return dict(status="PASS", assertions=dict(counts), manualRequired=manual) + + +def loaded_mods(records, expected): + for key, record in records.items(): + if record["mods"] != expected: + raise AssertionError(key + ": loaded mod identities/versions differ from the pinned profile") + + +def authorize_broad(runtime): + # matrix.stage is shared with frozen conversion campaigns. A live works + # campaign must explicitly opt out of MigrationProbe's tile-tick freeze. + for name in ("advantage-works.allow", "migration-operate.allow"): + (runtime / name).touch() + + +def configure_broad(runtime, port): + # Match the walkable WorksRunner rather than matrix.stage's migration-only + # defaults. Clearing a freshly generated village can race native POI tasks. + overrides = {"server-port": str(port), "generator-settings": "", "generate-structures": "false", + "level-seed": "advantage-works-1.17.1-v1", "difficulty": "0", "gamemode": "creative", + "allow-flight": "true", "enable-command-block": "true", "force-gamemode": "true", + "spawn-animals": "false", "spawn-monsters": "false", "spawn-npcs": "false", "pvp": "false"} + path = runtime / "server.properties" + 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"{key}={value}\n" for key, value in overrides.items())) + + +class BroadServer(Server): + def command(self, command, token): + self.send(command) + self.wait(token, 150) + + def checkpoint(self, station, label): + self.command(f"advworks checkpoint {station} {label}", "Checkpoint written for " + station) + return read_json(Path(self.log.name).parent / "advantage-works-results" / f"{station}-checkpoint-{label}.json") + + def check_all(self, stations, phase): + self.command("advworks check all", "Advantage Works check:") + directory = Path(self.log.name).parent / "advantage-works-results" + records = {s["id"]: read_json(directory / (s["id"] + "-latest.json")) for s in stations} + write_json(directory / (phase + "-all-stations.json"), records) + 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-")]) + runtime = output / name + launcher = stage(runtime, profile) + 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") + result["expectedMods"] = expected_mods + server = None + try: + verify_staged(runtime, profile) + server = BroadServer(runtime, args.java8, 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. + for station in stations: + if station["id"] not in DRILL_BAYS: + continue + for case in drill_cases(station): + pos = " ".join(map(str, case["origin"])) + marker = json.dumps(case["id"]) + tag = ('{ForgeData:{AdvBayWitness:' + marker + '},Items:[{Slot:0b,id:"minecraft:diamond",' + 'Count:7b,tag:{AdvBayStock:' + marker + '}}]}') + server.command(f"data merge block {pos} {tag}", "Modified block data of") + initial = {} + for station in stations: + if station["id"] in DRILL_BAYS: + initial.update(drill_state(station, server.checkpoint(station["id"], "initial"), initial=True)) + result["phases"]["initial"] = dict(status="PASS", drills=initial) + write_json(runtime / "broad-results.json", result) + for station in stations: + sid = station["id"] + server.command("advworks start " + sid, "Started " + sid) + time.sleep(max(60, args.run_seconds) if sid == "W-03" else args.run_seconds) + server.checkpoint(sid, "operated") + 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) + before = {} + for station in stations: + if station["id"] in DRILL_BAYS: + before.update(drill_state(station, first[station["id"]])) + result["phases"]["operation"] = operating_state(initial, before) + 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") + # 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) + after = {} + for station in stations: + if station["id"] in DRILL_BAYS: + after.update(drill_state(station, second[station["id"]])) + result["afterRestart"] = after + result["phases"]["persistence"] = restarted_state(before, after) + server.close() + server = None + require_native_boot(runtime / "console-restarted.log") + verify_staged(runtime, profile) + result["status"] = "PASS" + except Exception: + result["error"] = traceback.format_exc() + finally: + if server is not None: + try: + server.close() + except Exception: + result["shutdownError"] = traceback.format_exc() + write_json(runtime / "broad-results.json", result) + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--port", type=int, default=25588) + parser.add_argument("--run-seconds", type=int, default=8) + parser.add_argument("--profile", choices=("both", "packaged-current", "packaged-decoupled"), default="both") + 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) + profile = pinned_profile(args.target_manifest, "target117") + target_pins = read_json(args.target_manifest)["target117"]["jars"] + required = {"basepower", "basesteam", "baseelectric", "advantageworkstest", "basemetals", "orespawn"} + if {pin["id"] for pin in target_pins} != required: + raise AssertionError("Supply the complete current-profile manifest; the runner derives its decoupled subset") + for protected in (args.target_manifest, profile[0], *profile[2]): + require_disjoint(output, protected) + stations_path = RIG / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json" + stations = read_json(stations_path)["stations"] + if len(stations) != 21: + raise AssertionError("The complete 21-bay specification is required") + output.mkdir(parents=True) + shutil.copy2(Path(__file__), output / "runner-source.py") + source_hash = digest(output / "runner-source.py") + names = ("packaged-current", "packaged-decoupled") if args.profile == "both" else (args.profile,) + results = {} + def write_result(finished=False): + write_json(output / "native117-broad-results.json", campaign_document( + names, results, dict( + targetManifest=str(args.target_manifest.resolve()), targetManifestSha256=digest(args.target_manifest), + runnerSha256=source_hash, stationDefinitionSha256=digest(stations_path)), + finished=finished)) + write_result() + for index, name in enumerate(names): + try: + results[name] = run_profile(args, profile, output, name, args.port + index, stations) + except Exception: + results[name] = dict(status="FAIL", profile=name, phases={}, error=traceback.format_exc()) + write_result() + print(name, results[name]["status"], flush=True) + write_result(finished=True) + return int(campaign_status(names, results, finished=True) != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_broad_audit_fixtures.py b/test-rig/migration/native117_broad_audit_fixtures.py new file mode 100644 index 00000000..1a850ea0 --- /dev/null +++ b/test-rig/migration/native117_broad_audit_fixtures.py @@ -0,0 +1,395 @@ +"""Offline evidence-fixture controls, not additional Minecraft runs.""" + +from collections import Counter +from copy import deepcopy +import io +import json +from pathlib import Path +import tempfile +import unittest +from contextlib import redirect_stdout +import zipfile + +import native117_broad_result_audit as audit + + +LINE = Path(__file__).resolve().parents[3].name.removeprefix("PA") + + +def write(path, data): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data), encoding="utf-8") + + +def load(path): + return json.loads(path.read_text(encoding="utf-8")) + + +class BroadResultAuditTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.runtime = self.root / "broad" + self.runtime.mkdir() + source = Path(__file__).resolve().parents[1] / "src/main/resources" / audit.RESOURCE + definition = source.read_bytes() + self.stations = json.loads(definition)["stations"] + self.definitions = {s["id"]: s for s in self.stations} + jars = [] + for name in ("basepower", "basesteam", "baseelectric", "advantageworkstest", "basemetals", "orespawn"): + path = self.root / (name + ".jar") + version = "1.0.0.117011" if LINE == "117" else "fixture" + if name == "advantageworkstest" or LINE == "117": + with zipfile.ZipFile(path, "w") as archive: + if name == "advantageworkstest": + archive.writestr(audit.RESOURCE, definition.replace(b"\r\n", b"\n")) + if LINE == "117": + archive.writestr("META-INF/mods.toml", '[[mods]]\nmodId="' + name + '"\nversion="' + version + '"\n') + else: + path.write_bytes(name.encode("ascii")) + jars.append(dict(id=name, path=str(path), sha256=audit.sha(path.read_bytes()), version=version)) + self.pins = dict(jars=jars, launcher="forge.jar", launcher_sha256=audit.sha(b"engine"), + serverCode=dict(files={"forge.jar": dict(sha256=audit.sha(b"engine"), size=6)})) + if LINE == "117": + self.pins["launcher"] = sorted(audit.NATIVE117_LAUNCHERS)[0] + self.pins["serverCode"]["files"] = { + path: dict(sha256=audit.sha(b"engine"), size=6) + for path in audit.NATIVE117_REQUIRED_CODE | {self.pins["launcher"]}} + self.manifest = self.root / "inputs.json" + write(self.manifest, {"target" + LINE: self.pins}) + runner = b"# immutable historical broad source fixture\n" + (self.runtime / "runner-source.py").write_bytes(runner) + self.report = dict(status="PASS", targetManifest=str(self.manifest), + targetManifestSha256=audit.sha(self.manifest.read_bytes()), + runnerSha256=audit.sha(runner), stationDefinitionSha256=audit.sha(definition), profiles={}) + for name in audit.PROFILES: + self.make_profile(name) + self.save_top() + + def save_top(self): + write(self.runtime / ("native" + LINE + "-broad-results.json"), self.report) + + def profile(self, name="packaged-current"): + return self.runtime / name + + def save_profile(self, name="packaged-current"): + write(self.profile(name) / "broad-results.json", self.report["profiles"][name]) + self.save_top() + + def make_profile(self, name): + directory = self.profile(name) + directory.mkdir() + for relative in self.pins["serverCode"]["files"]: + path = directory / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"engine") + staged = [] + mods = {} + for pin in self.pins["jars"]: + if name == "packaged-decoupled" and pin["id"] == "basemetals": + continue + path = directory / "mods" / Path(pin["path"]).name + path.parent.mkdir(exist_ok=True) + path.write_bytes(Path(pin["path"]).read_bytes()) + staged.append(dict(file=path.name, sha256=pin["sha256"], source=pin["path"])) + mods[pin["id"]] = pin["version"] + write(directory / "input-jars.json", staged) + minecraft, forge = audit.VERSIONS[LINE] + mods.update(minecraft=minecraft, forge=forge) + raw = {} + for spec in self.stations: + assertions = [dict(id=a["id"], type=a["type"], status="SKIP" if set(a.get("requiredMods", [])) - set(mods) else "PASS", + detail="optional integration is absent" if set(a.get("requiredMods", [])) - set(mods) else "matched") + for a in spec["assertions"]] + raw[spec["id"]] = dict(station=spec["id"], minecraft=minecraft, forge=forge, mods=deepcopy(mods), + assertions=assertions, status="MANUAL_REQUIRED", manualChecks=spec["manualChecks"], + evidenceScope="declared_assertions_only; manual procedures are not automated passes", + worldTime=3, snapshot=[dict(pos=[0, 0, 0], block="minecraft:air", state="air")]) + initial, moved = {}, {} + for sid in ("S-05", "E-06"): + start, end = self.drills(self.definitions[sid], False), self.drills(self.definitions[sid], True) + write(directory / "advantage-works-results" / (sid + "-checkpoint-initial.json"), dict(snapshot=start)) + initial[sid] = dict(snapshot=start) + raw[sid]["snapshot"] = end + for phase in ("first", "restarted"): + write(directory / "advantage-works-results" / (phase + "-all-stations.json"), raw) + for sid in audit.STATIONS: + write(directory / "advantage-works-results" / (sid + "-checkpoint-operated.json"), + dict(station=sid, minecraft=minecraft, forge=forge, mods=mods, checkpoint="operated", worldTime=1, + snapshot=raw[sid]["snapshot"])) + initial = audit.drill_snapshot(initial, self.definitions, initial=True) + moved = audit.drill_snapshot(raw, self.definitions) + counts = dict(Counter(a["status"] for record in raw.values() for a in record["assertions"])) + checks = dict(status="PASS", assertions=counts, manualRequired=list(audit.STATIONS)) + result = dict(profile=name, status="PASS", expectedMods=mods, beforeRestart=moved, afterRestart=deepcopy(moved), phases={ + "initial": dict(status="PASS", drills=initial), "first": checks, "restarted": deepcopy(checks), + "operation": dict(status="PASS", drills=12, moved=12), + "persistence": dict(status="PASS", drills=12, exactEnergy=True)}) + self.report["profiles"][name] = result + write(directory / "broad-results.json", result) + log = ("Starting minecraft server version " + minecraft + "\nForge mod loading, version " + forge + ", for MC " + minecraft + + '\nDone (1.0s)! For help, type "help"\nSaved the game\nStopping server\nSaving players\nSaving worlds\n' + + "".join("Saving chunks for level 'fixture'/minecraft:" + d + "\n" for d in ("overworld", "the_nether", "the_end")) + + "".join("ThreadedAnvilChunkStorage (" + label + "): All chunks are saved\n" for label in ("fixture", "DIM-1", "DIM1"))) + if LINE == "117": + log = log.replace(", for MC " + minecraft, ", for MC " + minecraft + " with MCP 20210706.113038") + log += "ThreadedAnvilChunkStorage: All dimensions are saved\n" + for phase in ("first", "restarted"): + path = directory / ("console-" + phase + ".log") + commissioning = ("Advantage Works built 21 station(s); skipped 0\n" + "".join( + "Started " + sid + " - fixture\n" + sid + "-checkpoint-operated.json\nStopped " + sid + " - fixture\n" + for sid in audit.STATIONS)) if phase == "first" else "" + rendered = log.replace("Saved the game", commissioning + "Advantage Works check: {MANUAL_REQUIRED=21}\nSaved the game") + path.write_bytes(rendered.encode("utf-8")) + if LINE == "117": + for kind in ("latest", "debug"): + (directory / (kind + "-" + phase + ".log")).write_bytes(rendered.encode("utf-8")) + if LINE == "113": + write(path.with_suffix(".diagnostics.json"), dict(status="PASS", logSha256=audit.sha(path.read_bytes()), classifiedDiagnostics=[])) + + def drills(self, spec, moved): + result = [] + for a in spec["assertions"]: + block = a.get("block") + if block not in ("basesteam:steam_drill", "baseelectric:electric_drill"): + continue + direction = a.get("meta", audit.DIRECTIONS.index(a.get("expectedText", "down"))) + origin = [x + y for x, y in zip(spec["origin"], a["pos"])] + rear = [x + y for x, y in zip(origin, audit.REAR[direction])] + pos = rear if moved else origin + key = spec["id"] + ":" + a["id"] + tag = ('{id:"' + block + '",x:' + str(pos[0]) + ',y:' + str(pos[1]) + ',z:' + str(pos[2]) + + ',ForgeData:{AdvBayWitness:' + json.dumps(key) + '},Energy:[I;0],Items:[{id:"minecraft:diamond",Count:7b,' + + 'tag:{AdvBayStock:' + json.dumps(key) + '}}]}') + result.append(dict(pos=pos, block=block, state="facing=" + audit.DIRECTIONS[direction], nbt=tag)) + if not moved: + result.append(dict(pos=rear, block="basesteam:steam_track" if spec["id"] == "S-05" else "baseelectric:electric_track", state="track")) + return result + + def assert_rejected(self, contains=None, requested=audit.PROFILES): + result = audit.audit(self.runtime, LINE, requested) + self.assertEqual("FAIL", result["status"], result) + if contains: + self.assertIn(contains, " ".join(result["errors"])) + + def mutate_raw(self, mutate, phase="first", name="packaged-current"): + path = self.profile(name) / "advantage-works-results" / (phase + "-all-stations.json") + data = load(path) + mutate(data) + write(path, data) + + def test_complete_both_profiles_accepts_bounded_proof_not_manual_pass(self): + result = audit.audit(self.runtime, LINE) + self.assertEqual("PASS", result["status"], result["errors"]) + self.assertEqual(2, len(result["profiles"])) + self.assertEqual("NOT_AUTOMATED_BY_BROAD_SWEEP", result["profiles"][audit.PROFILES[0]]["manualProcedures"]) + self.assertIn("NOT_INDEPENDENTLY_RECORDED", result["profiles"][audit.PROFILES[0]]["shutdown"]["first"]["processExit"]) + + def test_first_profile_pass_never_implies_both(self): + self.report["profiles"].pop("packaged-decoupled") + self.save_top() + self.assert_rejected("Requested profile set incomplete") + + def test_explicit_single_profile_is_distinct_from_default_both(self): + self.report["profiles"].pop("packaged-decoupled") + self.save_top() + self.assertEqual("PASS", audit.audit(self.runtime, LINE, ("packaged-current",))["status"]) + + def test_unrequested_extra_profile_rejected(self): + self.assert_rejected("Requested profile set incomplete", ("packaged-current",)) + + def test_second_profile_failure_cannot_be_masked(self): + self.report["profiles"]["packaged-decoupled"]["status"] = "FAIL" + self.save_profile("packaged-decoupled") + self.assert_rejected("profile did not pass") + + def test_missing_local_second_report_rejected(self): + (self.profile("packaged-decoupled") / "broad-results.json").unlink() + self.assert_rejected("broad-results.json") + + def test_top_running_is_not_final_pass(self): + self.report["status"] = "RUNNING" + self.save_top() + self.assert_rejected("did not finish PASS") + + def test_empty_or_duplicate_requested_set_rejected(self): + self.assert_rejected("Invalid requested", ()) + self.assert_rejected("Invalid requested", ("packaged-current", "packaged-current")) + + def test_missing_station_and_missing_raw_assertion_rejected(self): + self.mutate_raw(lambda rows: rows.pop("W-04")) + self.assert_rejected("all 21") + + def test_omitted_assertion_rejected_despite_summary_counts(self): + self.mutate_raw(lambda rows: rows["W-01"]["assertions"].pop()) + self.assert_rejected("assertion set incomplete") + + def test_duplicate_assertion_rejected(self): + self.mutate_raw(lambda rows: rows["W-01"]["assertions"].append(deepcopy(rows["W-01"]["assertions"][0]))) + self.assert_rejected("duplicate") + + def test_partial_pass_counts_rejected(self): + self.report["profiles"]["packaged-current"]["phases"]["first"]["assertions"]["PASS"] -= 1 + self.save_profile() + self.assert_rejected("summary counts/manual list") + + def test_manual_station_cannot_be_reported_as_pass(self): + self.mutate_raw(lambda rows: rows["W-01"].update(status="PASS")) + self.assert_rejected("classification masked") + + def test_manual_procedures_cannot_disappear(self): + self.mutate_raw(lambda rows: rows["W-01"].update(manualChecks=[])) + self.assert_rejected("manual checks omitted") + + def test_unexplained_skip_rejected(self): + self.mutate_raw(lambda rows: rows["W-01"]["assertions"][0].update(status="SKIP")) + self.assert_rejected("wrong raw result") + + def test_absent_mod_integration_cannot_be_pass(self): + def change(rows): + a = next(a for a in rows["S-04"]["assertions"] if a["id"] == "base-metals-steel-plate") + a["status"] = "PASS" + self.mutate_raw(change, name="packaged-decoupled") + self.assert_rejected("wrong raw result") + + def test_failed_assertion_cannot_hide_behind_manual(self): + self.mutate_raw(lambda rows: rows["W-01"]["assertions"][0].update(status="FAIL")) + self.assert_rejected("wrong raw result") + + def test_loaded_mod_version_mismatch_rejected(self): + self.mutate_raw(lambda rows: rows["W-01"]["mods"].update(basepower="wrong")) + self.assert_rejected("loaded mod set") + + def test_error_and_shutdown_error_cannot_be_masked(self): + self.report["profiles"]["packaged-current"]["shutdownError"] = "process exit failed" + self.save_profile() + self.assert_rejected("failure masked") + + def test_top_level_error_cannot_hide_behind_pass(self): + self.report["error"] = "second profile crashed" + self.save_top() + self.assert_rejected("failure masked") + + def test_recorded_nonzero_process_exit_is_not_ignored(self): + self.report["profiles"]["packaged-current"]["processExitCode"] = 1 + self.save_profile() + self.assert_rejected("process exit failed") + + def test_pass_detail_must_not_say_did_not_match(self): + self.mutate_raw(lambda rows: rows["W-01"]["assertions"][0].update(detail="did not match")) + self.assert_rejected("detail contradicts result") + + def test_missing_restart_phase_rejected(self): + self.report["profiles"]["packaged-current"]["phases"].pop("persistence") + self.save_profile() + self.assert_rejected("required phases incomplete") + + def test_native_drift_cannot_be_masked_by_summary_pass(self): + self.mutate_raw(lambda rows: rows["S-05"]["snapshot"][0].update(nbt=rows["S-05"]["snapshot"][0]["nbt"].replace("Count:7b", "Count:6b")), phase="restarted") + self.assert_rejected("stock lost/duplicated") + + def test_native_drill_summary_is_not_the_measurement(self): + self.report["profiles"]["packaged-current"]["beforeRestart"]["S-05:down-facing"]["pos"][0] += 1 + self.save_profile() + self.assert_rejected("differs from native snapshots") + + def test_mod_bytes_and_staging_receipt_both_required(self): + (self.profile() / "mods/basepower.jar").write_bytes(b"changed") + self.assert_rejected("mod bytes differ") + + def test_extra_mod_rejected(self): + (self.profile() / "mods/unpinned.jar").write_bytes(b"extra") + self.assert_rejected("mod bytes differ") + + def test_staging_receipt_cannot_lie(self): + path = self.profile() / "input-jars.json" + data = load(path) + data.pop() + write(path, data) + self.assert_rejected("input receipt differs") + + def test_manifest_and_runner_hashes_required(self): + self.manifest.write_text(self.manifest.read_text() + "\n") + self.assert_rejected("manifest changed") + + def test_archived_runner_changed_rejected(self): + (self.runtime / "runner-source.py").write_bytes(b"changed") + self.assert_rejected("runner changed") + + def test_staged_engine_bytes_required(self): + (self.profile() / self.pins["launcher"]).write_bytes(b"wrong") + self.assert_rejected("launcher changed") + + def test_missing_restart_log_rejected(self): + (self.profile() / "console-restarted.log").unlink() + self.assert_rejected("console-restarted.log") + + def test_native_check_log_must_correspond_to_all_raw_stations(self): + path = self.profile() / "console-restarted.log" + path.write_bytes(path.read_bytes().replace(b"MANUAL_REQUIRED=21", b"MANUAL_REQUIRED=20")) + self.assert_rejected("native check summary differs") + + def test_native_first_boot_must_run_each_station(self): + path = self.profile() / "console-first.log" + path.write_bytes(path.read_bytes().replace(b"Started W-04 -", b"omitted W-04 -")) + self.assert_rejected("station activity") + + def test_restart_cannot_rebuild_to_hide_state_loss(self): + path = self.profile() / "console-restarted.log" + path.write_bytes(path.read_bytes() + b"Advantage Works built 21 station(s)\n") + self.assert_rejected("activity after cold restart") + + def test_missing_operated_station_checkpoint_rejected(self): + (self.profile() / "advantage-works-results/W-04-checkpoint-operated.json").unlink() + self.assert_rejected("W-04-checkpoint-operated.json") + + def test_stale_restart_records_are_not_accepted(self): + self.mutate_raw(lambda rows: rows["W-01"].update(worldTime=0), phase="restarted") + self.assert_rejected("not ordered") + + def test_shutdown_without_save_rejected(self): + path = self.profile() / "console-restarted.log" + path.write_bytes(path.read_bytes().replace(b"Saved the game", b"not saved").replace(b"All chunks are saved", b"not completed")) + self.assert_rejected("save and shutdown" if LINE == "113" else "completed chunk-storage shutdown") + + def test_shutdown_player_saves_are_required(self): + path = self.profile() / "console-restarted.log" + path.write_bytes(path.read_bytes().replace(b"Saving players", b"not completed")) + self.assert_rejected("incomplete shutdown saves") + + def test_later_native_stop_can_prove_save_without_explicit_flush_command(self): + if LINE == "113": + self.skipTest("Forge 25 protocol explicitly flushes before stopping") + path = self.profile() / "console-restarted.log" + path.write_bytes(path.read_bytes().replace(b"Saved the game", b"native stop autosave")) + result = audit.audit(self.runtime, LINE) + self.assertEqual("PASS", result["status"], result["errors"]) + self.assertFalse(result["profiles"]["packaged-current"]["shutdown"]["restarted"]["explicitSaveCommand"]) + + def test_missing_dimension_shutdown_rejected(self): + path = self.profile() / "console-restarted.log" + path.write_bytes(path.read_bytes().replace(b"/minecraft:the_end", b"/missing")) + self.assert_rejected("saved dimension") + + def test_error_in_complete_log_rejected(self): + path = self.profile() / "console-restarted.log" + path.write_bytes(path.read_bytes() + b"[Server thread/ERROR] serious native failure\n") + self.assert_rejected("native error") + + def test_duplicate_json_keys_rejected(self): + with self.assertRaisesRegex(AssertionError, "Duplicate JSON"): + audit.decode(b'{"status":"FAIL","status":"PASS"}') + + def test_cli_default_rejects_partial_and_returns_nonzero(self): + self.report["profiles"].pop("packaged-decoupled") + self.save_top() + output = io.StringIO() + with redirect_stdout(output): + code = audit.main(["--runtime", str(self.runtime), "--line", LINE]) + self.assertEqual(1, code) + self.assertEqual(list(audit.PROFILES), json.loads(output.getvalue())["requestedProfiles"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/native117_broad_result_audit.py b/test-rig/migration/native117_broad_result_audit.py new file mode 100644 index 00000000..c81e9b62 --- /dev/null +++ b/test-rig/migration/native117_broad_result_audit.py @@ -0,0 +1,439 @@ +"""Offline, fail-closed audit of a complete native broad campaign. + +The historical writer can say PASS after only its first requested profile. +Never use that progress status alone as a campaign result. This auditor does +not import, modify, launch, or repair a broad runner or a Minecraft world. +""" + +import argparse +from collections import Counter +import hashlib +import json +import math +from pathlib import Path +import re +import struct +import zipfile + + +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")} +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", +} +NATIVE117_REQUIRED_CODE = { + "libraries/net/minecraftforge/fmlloader/1.17.1-37.1.1/fmlloader-1.17.1-37.1.1.jar", + "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", +} +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"]) +RESOURCE = "assets/advantageworkstest/works/works-1.13.2.json" +DIRECTIONS = ("down", "up", "north", "south", "west", "east") +REAR = ((0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1), (1, 0, 0), (-1, 0, 0)) +PHASES = {"initial", "first", "operation", "restarted", "persistence"} + + +def require(condition, message): + if not condition: + raise AssertionError(message) + + +def unique_object(pairs): + result = {} + for key, value in pairs: + require(key not in result, "Duplicate JSON key: " + key) + result[key] = value + return result + + +def decode(data): + return json.loads(data.decode("utf-8-sig"), object_pairs_hook=unique_object) + + +def sha(data): + return hashlib.sha256(data).hexdigest().upper() + + +class Evidence: + def __init__(self): + self.hashes = {} + + def read(self, path): + path = Path(path).resolve() + data = path.read_bytes() + digest = sha(data) + previous = self.hashes.setdefault(str(path), digest) + require(previous == digest, "Evidence changed while auditing: " + str(path)) + return data + + def json(self, path): + return decode(self.read(path)) + + def unchanged(self): + for path, expected in self.hashes.items(): + require(sha(Path(path).read_bytes()) == expected, "Evidence changed while auditing: " + path) + + +def indexed(rows, key, label): + require(isinstance(rows, list), label + ": missing list") + result = {} + for row in rows: + require(isinstance(row, dict) and key in row, label + ": malformed row") + require(row[key] not in result, label + ": duplicate " + str(row[key])) + result[row[key]] = row + return result + + +def require_profiles(actual, requested): + require(set(actual) == set(requested), + "Requested profile set incomplete or unexpected: requested=" + str(sorted(requested)) + + "; reported=" + str(sorted(actual))) + + +def raw_checks(records, definitions, mods, minecraft, forge): + require(set(records) == set(STATIONS), "Raw station set must contain all 21 bays") + counts, manual = Counter(), [] + for sid in STATIONS: + spec, row = definitions[sid], records[sid] + require(row["station"] == sid and row["minecraft"] == minecraft and row["forge"] == forge, + sid + ": wrong native station/engine identity") + require(row["mods"] == mods, sid + ": native loaded mod set differs from pins") + require(row.get("evidenceScope") == "declared_assertions_only; manual procedures are not automated passes", + sid + ": missing bounded evidence scope") + require(row["manualChecks"] == spec.get("manualChecks", []), sid + ": manual checks omitted or changed") + expected = indexed(spec["assertions"], "id", sid + " definition") + actual = indexed(row["assertions"], "id", sid + " raw assertions") + require(set(actual) == set(expected), sid + ": raw assertion set incomplete") + require(not set(spec.get("requiredMods", [])) - set(mods), sid + ": required station mod absent") + passed = 0 + for aid, assertion in expected.items(): + result = actual[aid] + require(result["type"] == assertion["type"], sid + "/" + aid + ": assertion type changed") + missing = set(assertion.get("requiredMods", [])) - set(mods) + status = "SKIP" if missing else "PASS" + require(result["status"] == status, sid + "/" + aid + ": wrong raw result or unexplained skip") + detail = "optional integration is absent" if missing else "matched" + require(result.get("detail") == detail, sid + "/" + aid + ": raw check detail contradicts result") + counts[status] += 1 + passed += status == "PASS" + classification = ("MANUAL_REQUIRED" if spec.get("manualChecks") else "PASS") if passed else "SKIP" + require(row["status"] == classification, sid + ": manual/skip classification masked") + if spec.get("manualChecks"): + manual.append(sid) + snapshots = row.get("snapshot") + require(isinstance(snapshots, list) and snapshots, sid + ": missing raw snapshot") + positions = [tuple(item["pos"]) for item in snapshots] + require(len(set(positions)) == len(positions), sid + ": duplicate raw snapshot position") + return dict(status="PASS", assertions=dict(counts), manualRequired=manual) + + +def drill_snapshot(records, definitions, initial=False): + # Recompute the twelve witnesses from native SNBT, not summary PASS counts. + import nbtlib + result = {} + for sid, block, track, capacity in ( + ("S-05", "basesteam:steam_drill", "basesteam:steam_track", 50), + ("E-06", "baseelectric:electric_drill", "baseelectric:electric_track", 500)): + spec = definitions[sid] + snapshot = records[sid]["snapshot"] + rows = {tuple(row["pos"]): row for row in snapshot} + require(len(rows) == len(snapshot), sid + ": duplicate drill position") + stock = Counter() + for row in snapshot: + data = nbtlib.parse_nbt(row["nbt"]) if "nbt" in row else {} + for item in data.get("Items", []): + marker = item.get("tag", {}).get("AdvBayStock") + if marker: + require(str(item["id"]) == "minecraft:diamond", sid + ": witness item identity changed") + stock[str(marker)] += int(item["Count"]) + found, facings = set(), set() + for assertion in spec["assertions"]: + if assertion.get("block") != block: + continue + facing = (DIRECTIONS[assertion["meta"]] if "meta" in assertion + else assertion.get("expectedText", "down")) + direction = DIRECTIONS.index(facing) + origin = tuple(a + b for a, b in zip(spec["origin"], assertion["pos"])) + rear = tuple(a + b for a, b in zip(origin, REAR[direction])) + matches = [p for p in (origin, rear) if rows.get(p, {}).get("block") == block] + key = sid + ":" + assertion["id"] + require(len(matches) == 1, key + ": missing/duplicate native drill") + pos = matches[0] + if initial: + require(pos == origin and rows.get(rear, {}).get("block") == track, + key + ": initial origin/track changed") + row = rows[pos] + require(re.search(r"(?:\[|,|^)facing=" + facing + r"(?:,|\]|$)", row["state"]), + key + ": native facing changed") + data = nbtlib.parse_nbt(row["nbt"]) + require(str(data.get("ForgeData", {}).get("AdvBayWitness")) == key, + key + ": native tile witness missing") + require(tuple(int(data[a]) for a in ("x", "y", "z")) == pos, key + ": native coordinates changed") + require(stock[key] == 7, key + ": native stock lost/duplicated") + bits = [int(value) for value in data["Energy"]] + require(len(bits) == 1, key + ": missing native energy buffer") + energy = struct.unpack("!f", struct.pack("!I", bits[0] & 0xffffffff))[0] + require(math.isfinite(energy) and 0 <= energy <= capacity, key + ": invalid native energy") + result[key] = dict(pos=list(pos), facing=facing, block=block, tile=str(data["id"]), + witness=key, stock=7, energyBits=bits, energy=energy, moved=pos == rear) + found.add(pos) + facings.add(facing) + require(facings == set(DIRECTIONS) and len(found) == 6, sid + ": six facings missing") + require({p for p, row in rows.items() if row["block"] == block} == found, + sid + ": extra off-track drill") + require(set(stock) == {key for key in result if key.startswith(sid + ":")}, + sid + ": unexpected stock witness") + require(len(result) == 12, "Twelve native drill witnesses required") + return result + + +def log_evidence(evidence, path, line, phase, records): + text = evidence.read(path).decode("utf-8", errors="replace") + minecraft, forge = VERSIONS[line] + require("Starting minecraft server version " + minecraft in text + and "Forge mod loading, version " + forge + ", for MC " + minecraft in text, + str(path) + ": missing native boot identity") + require(re.search(r"Done \([^\n]+For help", text), str(path) + ": native boot incomplete") + summaries = re.findall(r"Advantage Works check: \{([^}]+)\}", text) + require(len(summaries) == 1, str(path) + ": missing or duplicate native all-station check") + native_counts = dict((key, int(value)) for key, value in re.findall(r"([A-Z_]+)=(\d+)", summaries[0])) + require(native_counts == dict(Counter(row["status"] for row in records.values())), + str(path) + ": native check summary differs from raw station classifications") + if phase == "first": + require("Advantage Works built 21 station(s); skipped 0" in text, str(path) + ": native bay construction incomplete") + for sid in STATIONS: + for marker in ("Started " + sid + " -", "Stopped " + sid + " -", sid + "-checkpoint-operated.json"): + require(text.count(marker) == 1, str(path) + ": missing/duplicate native station activity " + marker) + else: + require(not any(marker in text for marker in ("Advantage Works built", "Modified block data of")) + and not re.search(r"(?:Started|Reset) (?:W-|S-|E-|P-API-)", text), + str(path) + ": construction/refill/start activity after cold restart") + saved = text.rfind("Saved the game") + stopped = text.find("Stopping server", max(saved, text.find("Done ("))) + require(stopped >= 0 and (line != "113" or saved >= 0), str(path) + ": missing native save and shutdown") + require(text.find("Done (") < text.find("Advantage Works check:") < stopped, + str(path) + ": native check is outside the running-server interval") + # Forge 25 uses an explicit flush. Later broad protocols rely on the native + # stop path, which logs per-dimension chunk-storage completion instead. + shutdown = text[stopped:] + require("Saving players" in shutdown and "Saving worlds" in shutdown, str(path) + ": incomplete shutdown saves") + for dimension in ("minecraft:overworld", "minecraft:the_nether", "minecraft:the_end"): + require(any("Saving chunks for level" in row and dimension in row for row in shutdown.splitlines()), + str(path) + ": missing saved dimension " + dimension) + if line != "113": + completed = set(re.findall(r"ThreadedAnvilChunkStorage \(([^)]+)\): All chunks are saved", shutdown)) + require({"DIM-1", "DIM1"} <= completed and bool(completed - {"DIM-1", "DIM1"}), + str(path) + ": missing completed chunk-storage shutdown") + errors = ("NoSuchFieldError", "NoSuchMethodError", "Failed to reload data packs", + "Encountered an unexpected exception", "Exception in server tick loop") + for row in text.splitlines(): + require("/ERROR]" not in row and not any(marker in row for marker in errors), + str(path) + ": native error: " + row) + # Reuse only existing, narrow engine diagnostics; never trust a diagnostic + # PASS without validating its raw log and classified line identities. + if line == "113": + from native113_diagnostics import audit_lines + diagnostics = audit_lines(text.splitlines()) + recorded = evidence.json(path.with_suffix(".diagnostics.json")) + require(recorded.get("status") == "PASS" and recorded.get("logSha256") == sha(text.encode("utf-8")) + and recorded.get("classifiedDiagnostics") == diagnostics["classifiedDiagnostics"], + str(path) + ": stale or masked native diagnostics") + elif line == "114": + notice = "[ne.mi.co.ForgeConfig/CORE]: Forge config just got changed on the file system!" + require(not any("/FATAL]" in row and not row.endswith(notice) for row in text.splitlines()), + str(path) + ": unclassified fatal") + else: + require("/FATAL]" not in text, str(path) + ": fatal native runtime error") + if line == "117": + require("with MCP 20210706.113038" in text, str(path) + ": missing native117 MCP identity") + require("ThreadedAnvilChunkStorage: All dimensions are saved" in shutdown, + str(path) + ": missing completed all-dimension shutdown") + error = re.compile(r"/(?:ERROR|FATAL)\]|\[(?:ERROR|FATAL)\]|^(?:ERROR|FATAL) StatusLogger|" + r"\b(?:[\w$]+\.)+[\w$]*(?:Exception|Error)\b|" + r"\b[\w$]+(?:Exception|Error)\b|Exception in thread|Caused by:|" + r"Advantage Works command failed") + for row in text.splitlines(): + require(not error.search(row), str(path) + ": native117 runtime error: " + row) + return dict(status="PASS", saveAndAllDimensionShutdown=True, explicitSaveCommand=saved >= 0, + processExit="NOT_INDEPENDENTLY_RECORDED_BY_HISTORICAL_BROAD_WRITER") + + +def staged_pins(evidence, runtime, pins, name, line=None): + jars = [row for row in pins["jars"] if name != "packaged-decoupled" or row["id"] != "basemetals"] + expected = {Path(row["path"]).name: row["sha256"].upper() for row in jars} + require(len(expected) == len(jars), "Duplicate pinned jar filenames") + actual = {path.name: sha(evidence.read(path)) for path in (runtime / "mods").glob("*.jar")} + require(actual == expected, name + ": staged mod bytes differ from pins") + recorded = indexed(evidence.json(runtime / "input-jars.json"), "file", name + " staged input receipt") + require({key: row["sha256"].upper() for key, row in recorded.items()} == expected, + name + ": staged input receipt differs from pins") + 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") + folded = set() + for relative, entry in sealed.items(): + require(relative.startswith("libraries/") and "\\" not in relative + and all(part not in ("", ".", "..") for part in relative.split("/")), + name + ": unsafe native117 engine inventory path") + require(relative.casefold() not in folded, name + ": duplicate native117 engine path") + folded.add(relative.casefold()) + require(isinstance(entry, dict) and type(entry.get("size")) is int and entry["size"] > 0 + and re.fullmatch(r"[A-F0-9]{64}", entry.get("sha256", "")), + name + ": invalid native117 engine inventory entry") + path = runtime / relative + require(path.resolve().is_relative_to(runtime.resolve()) + and not any(p.is_symlink() for p in (path, *path.parents) if p.is_relative_to(runtime)), + name + ": linked or escaping native117 engine file") + import tomllib + for pin in jars: + require(pin["version"].endswith(".117011"), name + ": non-native117 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") + 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) + for path in paths if path.is_file()} + require(actual == sealed, name + ": sealed native engine bytes changed") + return dict(modJars=expected, sealedEngine=bool(sealed)) + + +def profile_audit(evidence, runtime, name, report, pins, definitions, line): + local = evidence.json(runtime / "broad-results.json") + require(local == report, name + ": local result differs from top-level result") + require(report["profile"] == name and report["status"] == "PASS", name + ": profile did not pass") + require(not report.get("error") and not report.get("shutdownError"), name + ": failure masked by status") + for key in ("exitCode", "processExitCode", "returnCode"): + if key in report: + require(report[key] == 0, name + ": recorded process exit failed") + require(set(report["phases"]) == PHASES, name + ": required phases incomplete") + require(all(row.get("status") == "PASS" for row in report["phases"].values()), name + ": phase failure masked") + minecraft, forge = VERSIONS[line] + mods = {row["id"]: row["version"] for row in pins["jars"] + if name != "packaged-decoupled" or row["id"] != "basemetals"} + mods.update(minecraft=minecraft, forge=forge) + require(report["expectedMods"] == mods, name + ": expected mod identities changed") + custody = staged_pins(evidence, runtime, pins, name, line) + directory = runtime / "advantage-works-results" + checks, records = {}, {} + for phase in ("first", "restarted"): + raw = evidence.json(directory / (phase + "-all-stations.json")) + records[phase] = raw + checked = raw_checks(raw, definitions, mods, minecraft, forge) + require(report["phases"][phase] == checked, name + ": summary counts/manual list differs from raw " + phase) + checks[phase] = checked + for sid in STATIONS: + operated = evidence.json(directory / (sid + "-checkpoint-operated.json")) + require(operated["station"] == sid and operated["minecraft"] == minecraft and operated["forge"] == forge + and operated["mods"] == mods and operated["checkpoint"] == "operated" and operated["snapshot"], + sid + ": operated checkpoint identity/snapshot missing") + require(operated["worldTime"] <= records["first"][sid]["worldTime"] <= records["restarted"][sid]["worldTime"], + sid + ": raw checkpoints are not ordered across operation and cold restart") + initial_records = {sid: evidence.json(directory / (sid + "-checkpoint-initial.json")) + for sid in ("S-05", "E-06")} + initial = drill_snapshot(initial_records, definitions, initial=True) + before = drill_snapshot(records["first"], definitions) + after = drill_snapshot(records["restarted"], definitions) + require(report["phases"]["initial"] == dict(status="PASS", drills=initial), name + ": initial drill summary masked") + require(report["beforeRestart"] == before and report["afterRestart"] == after, + name + ": drill summary differs from native snapshots") + for key in initial: + require(all(initial[key][field] == before[key][field] for field in ("facing", "block", "tile", "witness", "stock")), + key + ": moved identity changed") + require(before["S-05:down-facing"]["moved"] and before["E-06:up-facing"]["moved"], "Required native track movement absent") + require(report["phases"]["operation"] == dict(status="PASS", drills=12, moved=sum(row["moved"] for row in before.values())), + name + ": drill movement count masked") + require(before == after, name + ": native cold restart changed drill state") + require(report["phases"]["persistence"] == dict(status="PASS", drills=12, exactEnergy=True), + name + ": persistence summary changed") + shutdown = {phase: log_evidence(evidence, runtime / ("console-" + phase + ".log"), line, phase, records[phase]) + 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"] = { + phase: {kind: log_evidence(evidence, runtime / (kind + "-" + phase + ".log"), + line, phase, records[phase]) for kind in ("latest", "debug")} + for phase in ("first", "restarted")} + return result + + +def audit(runtime, line, requested=PROFILES): + runtime, line = Path(runtime).resolve(), str(line) + evidence = Evidence() + result = dict(status="FAIL", line=line, runtime=str(runtime), requestedProfiles=list(requested), + profiles={}, errors=[], writerWarning="Historical broad writer can emit PASS for a completed subset; require this independent audit.") + try: + require(line in VERSIONS, "Unsupported native line") + require(requested and len(set(requested)) == len(requested) and set(requested) <= set(PROFILES), "Invalid requested profile set") + report = evidence.json(runtime / ("native" + line + "-broad-results.json")) + require_profiles(report["profiles"], requested) + require(report["status"] == "PASS", "Top-level campaign did not finish PASS") + require(not report.get("error") and not report.get("shutdownError"), "Top-level failure masked by PASS") + for key in ("exitCode", "processExitCode", "returnCode"): + if key in report: + require(report[key] == 0, "Top-level recorded process exit failed") + manifest_path = Path(report["targetManifest"]) + manifest_data = evidence.read(manifest_path) + require(sha(manifest_data) == report["targetManifestSha256"], "Pinned target manifest changed") + pins = decode(manifest_data)["target" + line] + require(sha(evidence.read(runtime / "runner-source.py")) == report["runnerSha256"], "Archived broad runner changed") + jars = indexed(pins["jars"], "id", "target mod pins") + require(set(jars) == {"basepower", "basesteam", "baseelectric", "advantageworkstest", "basemetals", "orespawn"}, + "Complete pinned current-profile inputs required") + harness = Path(jars["advantageworkstest"]["path"]) + harness_bytes = evidence.read(harness) + require(sha(harness_bytes) == jars["advantageworkstest"]["sha256"], "Pinned harness bytes changed") + with zipfile.ZipFile(harness) as archive: + definition = archive.read(RESOURCE) + # Gradle normalizes line endings; only that byte normalization is allowed. + lf = definition.replace(b"\r\n", b"\n") + require(report["stationDefinitionSha256"] in (sha(definition), sha(lf), sha(lf.replace(b"\n", b"\r\n"))), + "Reported station definition differs from pinned harness resource") + definitions = indexed(decode(definition)["stations"], "id", "station definitions") + require(set(definitions) == set(STATIONS), "Pinned definition does not describe all 21 stations") + for name in requested: + try: + result["profiles"][name] = profile_audit(evidence, runtime / name, name, report["profiles"][name], pins, definitions, line) + except (AssertionError, OSError, ValueError, KeyError, TypeError, IndexError, ImportError) as error: + result["profiles"][name] = dict(status="FAIL", error=str(error)) + result["errors"].append(name + ": " + str(error)) + evidence.unchanged() + if not result["errors"]: + result["status"] = "PASS" + except (AssertionError, OSError, ValueError, KeyError, TypeError, IndexError, zipfile.BadZipFile) as error: + result["errors"].append(str(error)) + result["evidenceSha256"] = evidence.hashes + result["scope"] = "Complete requested broad profiles and declared native checks only; not manual acceptance or operating/endurance coverage." + return result + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--runtime", type=Path, required=True, help="Broad output root containing nativeNNN-broad-results.json") + parser.add_argument("--line", choices=tuple(VERSIONS), required=True) + parser.add_argument("--profiles", nargs="+", choices=("both",) + PROFILES, default=["both"], + help="Explicit requested set; defaults to both, never inferred from existing results") + args = parser.parse_args(argv) + requested = PROFILES if args.profiles == ["both"] else tuple(args.profiles) + if "both" in requested: + parser.error("both cannot be combined with a named profile") + result = audit(args.runtime, args.line, requested) + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 if result["status"] == "PASS" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_campaign.py b/test-rig/migration/native117_campaign.py new file mode 100644 index 00000000..24569585 --- /dev/null +++ b/test-rig/migration/native117_campaign.py @@ -0,0 +1,638 @@ +"""Preserve historical inputs and require explicit 114, 115 and 116 source routes. + +Preparation reads canonical evidence and fingerprints source worlds; it never +launches them. Runs always create new 117 destinations and reuse native117's +unchanged frozen/connected state and operating algorithms. +""" + +import argparse +from argparse import Namespace +from pathlib import Path +from unittest.mock import patch +import traceback +import shutil + +from matrix import WORLD, digest, fingerprint, write_json +import native117 +import native117_connected +import native114 +import native114_connected +from native117_lifecycle import read_json +from native117_refresh import snapshot_name +from native117_sources import ACCEPTED_POWER115, SOURCE115, pending_routes, require_source115_receipt +from native117_sources import (preflight_source115, run_source115, source115_result, + source115_settings, verify_source115_inputs) +from native117_sources116 import (ACCEPTED_POWER116, SOURCE116, SCOPE as SOURCE116_SCOPE, + pending_routes as pending116_routes, require_source116_receipt) + + +ACCEPTED_POWER114 = "ef08e5cd134dc75bd73518bf4c90a6711e67280f" +HISTORICAL = ("early110", "legacy112", "maintained110", "maintained112", "original110") +SOURCE113 = tuple(name + "-direct-113" for name in HISTORICAL) + tuple( + name + "-chained-113" for name in ("early110", "maintained110", "original110")) +FROZEN_IDS = tuple("legacy/" + name for name in HISTORICAL + SOURCE113) +CONNECTED_IDS = tuple("legacy/" + name for name in HISTORICAL) +REQUIRED = { + "frozen": set(FROZEN_IDS) | {"native114/full"} | SOURCE115["frozen"] | SOURCE116["frozen"], + "connected": set(CONNECTED_IDS) | {"native114/" + name for name in HISTORICAL} | SOURCE115["connected"] | SOURCE116["connected"], +} + + +def approved_file(path, checksum): + path = Path(path).resolve() + if digest(path) != checksum.upper(): + raise AssertionError("Archived evidence/input changed: " + str(path)) + return path + + +def source_row(output, kind, route_id, source, manifest, key, snapshot, fixture_key=None): + source, manifest = source.resolve(), manifest.resolve() + native117.require_disjoint(output, source) + native117.require_disjoint(output, manifest) + native117.pinned_profile(manifest, key) + before = fingerprint(source) + hashes = fingerprint(source / WORLD) + if not hashes: + raise AssertionError("Missing canonical source world: " + str(source)) + filename = "migration-" + snapshot + ".json" if kind == "connected" else snapshot + if not (source / filename).is_file() or not (source / "migration-fixture.json").is_file(): + raise FileNotFoundError("Missing retained source serializer checkpoint: " + str(source)) + hash_file = output / "source-hashes" / (kind + "-" + route_id.replace("/", "-") + ".json") + write_json(hash_file, hashes) + if fingerprint(source) != before: + raise AssertionError("Protected canonical source changed during preparation") + return dict(id=route_id, kind=kind, source=str(source), sourceManifest=str(manifest), sourceKey=key, + sourceManifestSha256=digest(manifest), sourceHashes=str(hash_file), sourceHashesSha256=digest(hash_file), + sourceSnapshot=snapshot, sourceSnapshotSha256=digest(source / filename), sourceFixtureKey=fixture_key) + + +def require_routes(document): + if document.get("format") != 1 or document.get("acceptedPower114") != ACCEPTED_POWER114: + raise AssertionError("Campaign must retain the explicitly accepted merged Power 114 identity") + rows = document["routes"] + for kind, expected in REQUIRED.items(): + selected = [row["id"] for row in rows if row.get("kind") == kind] + if len(selected) != len(expected) or set(selected) != expected: + raise AssertionError("Incomplete/duplicated " + kind + " routes: all 36 historical/native114/native115/native116 cases are required") + if len(rows) != sum(map(len, REQUIRED.values())): + raise AssertionError("Unexpected campaign route kind") + + +def prepare(args): + output = native117.output_path(args.output) + for protected in (args.frozen_evidence, args.connected_evidence): + native117.require_disjoint(output, protected) + frozen, connected = read_json(args.frozen_evidence), read_json(args.connected_evidence) + for report, count in ((frozen, 13), (connected, 5)): + if report.get("status") != "PASS" or len(report["routes"]) != count or any( + row.get("status") != "PASS" for row in report["routes"]): + raise AssertionError("Select the complete accepted 114 evidence, not an incomplete/passing subset") + output.mkdir(parents=True, exist_ok=False) + document = dict(format=1, acceptedPower114=ACCEPTED_POWER114, qualificationStatus="UNQUALIFIED", routes=[], + frozenEvidence=str(args.frozen_evidence.resolve()), frozenEvidenceSha256=digest(args.frozen_evidence), + connectedEvidence=str(args.connected_evidence.resolve()), connectedEvidenceSha256=digest(args.connected_evidence)) + for row in frozen["routes"]: + runtime = Path(row["runtime"]) + old = runtime / "native114-provenance.json" + if row.get("provenanceSha256"): + approved_file(old, row["provenanceSha256"]) + if read_json(runtime / "state-comparison.json") != row["comparison"]: + raise AssertionError("Archived frozen comparison differs from the selected final report") + provenance = read_json(old) + source = Path(provenance["source"]) + name = source.name + manifest = approved_file(provenance["sourceManifest"], provenance["sourceManifestSha256"]) + selected = snapshot_name(source, provenance["sourceSnapshotSha256"]) + if fingerprint(source / WORLD) != provenance["sourceHashes"]: + raise AssertionError("Original frozen source fingerprint changed") + legacy = source_row(output, "frozen", "legacy/" + name, source, manifest, provenance["sourceKey"], selected) + document["routes"].append(legacy) + if name == "original110-chained-113": + document["routes"].append(dict(id="native114/full", kind="frozen", native114SourceRecipe=legacy, + source114Manifest=provenance["targetManifest"], source114ManifestSha256=provenance["targetManifestSha256"], + sourceStatus="REQUIRES_DISPOSABLE_114_GENERATION")) + for row in connected["routes"]: + old = approved_file(row["result"], row["sha256"]) if "result" in row and "sha256" in row else Path(row["result"]) + # Refresh reports record result paths without a second digest. The result + # itself still pins every input and is tied to the selected archive above. + provenance = read_json(old) + if (provenance.get("status") != "PASS" or provenance.get("sourcePreservation", {}).get("status") != "PASS" + or provenance.get("sourceRuntimeBefore") != provenance.get("sourceRuntimeAfter")): + raise AssertionError("Native114 connected operation/source preservation did not pass") + source = Path(provenance["source"]) + name = provenance["sourceKey"] + manifest = approved_file(provenance["sourceManifest"], provenance["sourceManifestSha256"]) + approved_file(provenance["sourceHashes"], provenance["sourceHashesSha256"]) + if fingerprint(source / WORLD) != read_json(Path(provenance["sourceHashes"])): + raise AssertionError("Original connected source fingerprint changed") + legacy = source_row(output, "connected", "legacy/" + name, source, manifest, name, provenance["sourceCheckpoint"]) + document["routes"].append(legacy) + document["routes"].append(dict(id="native114/" + name, kind="connected", native114SourceRecipe=legacy, + source114Manifest=provenance["targetManifest"], source114ManifestSha256=provenance["targetManifestSha256"], + sourceStatus="REQUIRES_DISPOSABLE_114_GENERATION")) + document["acceptedPower115"] = ACCEPTED_POWER115 + document["routes"].extend(pending_routes()) + document["acceptedPower116"] = ACCEPTED_POWER116 + document["routes"].extend(pending116_routes()) + require_routes(document) + write_json(output / "native117-upgrade-routes.json", document) + return 0 + + +def require_source114_receipt(row): + if not row["id"].startswith("native114/"): + return + if row.get("sourceStatus") != "GENERATED_DISPOSABLE_114_VERIFIED": + raise AssertionError("A new fingerprint of an unverified old 114 destination is not approval") + receipt_path = approved_file(row["source114Receipt"], row["source114ReceiptSha256"]) + receipt = read_json(receipt_path) + source = Path(row["source"]) + if (receipt.get("status") != "PASS" or receipt.get("acceptedPower114") != ACCEPTED_POWER114 + or receipt.get("sourceRuntime") != str(source.resolve()) + or receipt.get("runtimeFingerprint") != fingerprint(source) + or receipt.get("worldFingerprint") != fingerprint(source / WORLD)): + raise AssertionError("Recorded disposable114 source receipt/world fingerprint changed") + approved_file(receipt["target114Manifest"], receipt["target114ManifestSha256"]) + native114.pinned_profile(Path(receipt["target114Manifest"]), "target114") + if row["kind"] == "frozen" and (receipt.get("blocks"), receipt.get("tiles")) != (355, 332): + raise AssertionError("Native114 source must retain the full 355-block/332-tile fixture") + if row["kind"] == "connected" and set(receipt.get("connectedCases", [])) != { + "C-CONVEYOR", "C-CRUSHER", "C-EDRILL", "C-ELECTRIC", "C-SDRILL", "C-STEAM", "battery"}: + raise AssertionError("Native114 connected source lacks a required lane/drill/battery") + + +def generate114(args): + document = read_json(args.routes) + require_routes(document) + output = native117.output_path(args.output) + native117.require_disjoint(output, args.routes) + for row in document["routes"]: + if not row["id"].startswith("native114/"): + continue + recipe = row["native114SourceRecipe"] + for protected in (recipe["source"], recipe["sourceManifest"], recipe["sourceHashes"], row["source114Manifest"]): + native117.require_disjoint(output, Path(protected)) + seed, _, jars = native114.pinned_profile(Path(row["source114Manifest"]), "target114") + for protected in (seed, *jars): + native117.require_disjoint(output, protected) + output.mkdir(parents=True, exist_ok=False) + for row in document["routes"]: + if not row["id"].startswith("native114/"): + continue + if row.get("sourceStatus") != "REQUIRES_DISPOSABLE_114_GENERATION": + raise AssertionError("Do not replay or replace an existing source-generation receipt") + recipe = row["native114SourceRecipe"] + manifest = approved_file(row["source114Manifest"], row["source114ManifestSha256"]) + native114.pinned_profile(manifest, "target114") + source_manifest = approved_file(recipe["sourceManifest"], recipe["sourceManifestSha256"]) + source_hashes = approved_file(recipe["sourceHashes"], recipe["sourceHashesSha256"]) + source = Path(recipe["source"]) + filename = "migration-" + recipe["sourceSnapshot"] + ".json" if row["kind"] == "connected" else recipe["sourceSnapshot"] + approved_file(source / filename, recipe["sourceSnapshotSha256"]) + destination = output / row["id"].replace("/", "-") + settings = Namespace(source=source, source_manifest=source_manifest, source_key=recipe["sourceKey"], + source_hashes=source_hashes, source_snapshot=recipe["sourceSnapshot"], + source_checkpoint=recipe["sourceSnapshot"], target_manifest=manifest, + java8=args.java8, output=destination / "runtime", ticks=512, port=args.port) + receipt = dict(status="RUNNING", acceptedPower114=ACCEPTED_POWER114, + method="new disposable copy converted/operated under exact accepted archived114 jars; no canonical launch", + target114Manifest=str(manifest), target114ManifestSha256=digest(manifest), + originalSource=str(source), originalSourceHashesSha256=digest(source_hashes)) + destination.mkdir(parents=True, exist_ok=False) + receipt_path = destination / "source114-receipt.json" + try: + if row["kind"] == "frozen": + definition = read_json(source / recipe["sourceSnapshot"]) + counts = (len(definition), sum("nbtBase64" in entry for entry in definition)) + if counts != (355, 332): + raise AssertionError("Approved source is not the full 355-block/332-tile fixture") + stage = native114.stage + def stage_at_port(runtime, profile): + launcher = stage(runtime, profile) + properties = runtime / "server.properties" + lines = [line for line in properties.read_text().splitlines() + if not line.startswith(("server-port=", "generator-settings="))] + properties.write_text("\n".join(lines) + f"\nserver-port={args.port}\ngenerator-settings=\n", encoding="utf-8") + return launcher + with patch.object(native114, "stage", stage_at_port): + code = native114.run(settings) + runtime, snapshot, fixture_key = settings.output, "migration-restarted.json", None + report = read_json(runtime / "state-comparison.json") + if any(report.get(gate, {}).get("status") != "PASS" for gate in ("converted", "restarted", "idempotence")): + raise AssertionError("Disposable114 source differs from the approved frozen semantics") + receipt.update(blocks=355, tiles=332, semanticComparison=report) + else: + code = native114_connected.run(settings) + runtime, snapshot = settings.output / "target114", "restarted-worked" + fixture_key = read_json(runtime / "connected-fixture.json")["sourceProfile"] + report = read_json(settings.output / "native114-connected-results.json") + if report.get("sourcePreservation", {}).get("status") != "PASS" or any( + value.get("status") != "PASS" for value in report["results"].values()): + raise AssertionError("Disposable114 source failed preserved stocks or measured operation") + receipt["connectedCases"] = sorted(report["sourceState"]["cases"]) + receipt["operatingComparison"] = report["results"] + if code: + raise AssertionError("Native114 source-generation protocol failed") + receipt.update(status="PASS", sourceRuntime=str(runtime.resolve()), + runtimeFingerprint=fingerprint(runtime), worldFingerprint=fingerprint(runtime / WORLD)) + write_json(receipt_path, receipt) + pinned = source_row(output / "source-records", row["kind"], row["id"], runtime, manifest, + "target114", snapshot, fixture_key) + row.update(pinned, sourceStatus="GENERATED_DISPOSABLE_114_VERIFIED", source114Receipt=str(receipt_path.resolve()), + source114ReceiptSha256=digest(receipt_path)) + require_source114_receipt(row) + except BaseException as error: + receipt.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + write_json(receipt_path, receipt) + raise + finally: + write_json(output / "native117-upgrade-routes.json", document) + return 0 + + +def observation_ticks(args): + ticks = getattr(args, "ticks", 512) + if not isinstance(ticks, int) or isinstance(ticks, bool) or not 1 <= ticks <= 4000: + raise ValueError("Use one fixed connected interval between 1 and 4000 ticks") + return ticks + + +def select_shard(rows, index=0, count=1): + if not isinstance(index, int) or not isinstance(count, int) or not 1 <= count <= 15 or not 0 <= index < count: + raise ValueError("Invalid campaign shard") + selected = rows[index::count] + if not selected: raise ValueError("Empty campaign shard") + return selected + + +def run(args): + ticks = observation_ticks(args) + document = read_json(args.routes) + require_routes(document) + output = native117.output_path(args.output) + native117.require_disjoint(output, args.routes) + native117.require_disjoint(output, args.target_manifest) + seed, _, jars = native117.pinned_profile(args.target_manifest.resolve(), "target117") + for protected in (seed, *jars): + native117.require_disjoint(output, protected) + shard_index, shard_count = getattr(args, "shard_index", 0), getattr(args, "shard_count", 1) + rows = select_shard([row for row in document["routes"] if row["kind"] == args.kind], shard_index, shard_count) + for row in rows: + require_source114_receipt(row) + require_source115_receipt(row) + if row["id"].startswith("native116/"): + require_source116_receipt(row) + for protected in (row["source"], row["sourceManifest"], row["sourceHashes"]): + native117.require_disjoint(output, Path(protected)) + report = dict(status="RUNNING", kind=args.kind, routesManifest=str(args.routes.resolve()), + routesManifestSha256=digest(args.routes), targetManifest=str(args.target_manifest.resolve()), + targetManifestSha256=digest(args.target_manifest), observationTicks=ticks, + shard=dict(index=shard_index, count=shard_count), selectedRouteIds=[row["id"] for row in rows], routes=[]) + output.mkdir(parents=True, exist_ok=False) + helpers = [Path(__file__), Path(native117.__file__), Path(native117_connected.__file__), + Path(__file__).with_name("native117_resource_audit.py"), Path(__file__).with_name("native117_legacy_snbt.py"), + Path(__file__).with_name("semantic_machine_audit.py"), + Path(__file__).with_name("native117_sources.py"), Path(__file__).with_name("native117_sources116.py")] + report["protocolSha256"] = {str(path): digest(path) for path in helpers} + (output / "protocol-sources").mkdir() + for path in helpers: shutil.copy2(path, output / "protocol-sources" / path.name) + for index, row in enumerate(rows): + outcome = dict(id=row["id"], status="RUNNING") + report["routes"].append(outcome) + try: + manifest = approved_file(row["sourceManifest"], row["sourceManifestSha256"]) + hashes = approved_file(row["sourceHashes"], row["sourceHashesSha256"]) + source = Path(row["source"]) + filename = "migration-" + row["sourceSnapshot"] + ".json" if args.kind == "connected" else row["sourceSnapshot"] + approved_file(source / filename, row["sourceSnapshotSha256"]) + settings = Namespace(source=source, source_manifest=manifest, source_key=row["sourceKey"], + source_hashes=hashes, source_snapshot=row["sourceSnapshot"], source_checkpoint=row["sourceSnapshot"], + source_fixture_key=row.get("sourceFixtureKey"), target_manifest=args.target_manifest, + java8=args.java8, output=output / f"route-{index:02}", ticks=ticks, port=args.port) + if args.kind == "connected": + code = native117_connected.run(settings) + else: + stage = native117.stage + def stage_at_port(runtime, profile): + launcher = stage(runtime, profile) + properties = runtime / "server.properties" + lines = [line for line in properties.read_text().splitlines() + if not line.startswith(("server-port=", "generator-settings="))] + properties.write_text("\n".join(lines) + f"\nserver-port={args.port}\ngenerator-settings=\n", encoding="utf-8") + return launcher + with patch.object(native117, "stage", stage_at_port): + code = native117.run(settings) + from native117_resource_audit import compare_snapshot_resources + budgets = {phase: compare_snapshot_resources(source / row["sourceSnapshot"], + settings.output / ("migration-" + phase + ".json"), legacy_source=True) + for phase in ("converted", "restarted")} + write_json(settings.output / "exact-resource-comparison.json", budgets) + if any(result["status"] != "PASS" for result in budgets.values()): code = 1 + outcome.update(status="PASS" if code == 0 else "FAIL", runtime=str(settings.output)) + except Exception as error: + outcome.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + write_json(output / "native117-campaign-results.json", report) + report["status"] = ("PASS_SHARD" if shard_count > 1 else "PASS") if len(report["routes"]) == len(rows) and all( + row["status"] == "PASS" for row in report["routes"]) else "FAIL" + report["protocolUnchanged"] = {str(path): digest(path) for path in helpers} == report["protocolSha256"] + if (not report["protocolUnchanged"] or digest(args.target_manifest) != report["targetManifestSha256"] + or digest(args.routes) != report["routesManifestSha256"]): report["status"] = "FAIL" + write_json(output / "native117-campaign-results.json", report) + return int(report["status"] not in ("PASS", "PASS_SHARD")) + + +def aggregate_shards(documents): + if not documents: raise ValueError("No campaign shards") + first = documents[0] + count = first["shard"]["count"] + if len(documents) != count or {row["shard"]["index"] for row in documents} != set(range(count)): + raise AssertionError("Missing or duplicate campaign shard") + for row in documents: + for key in ("kind", "routesManifestSha256", "targetManifestSha256", "observationTicks"): + if row[key] != first[key]: raise AssertionError("Mixed campaign inputs: " + key) + if row["shard"]["count"] != count or row["status"] not in ("PASS", "PASS_SHARD") or row.get("protocolUnchanged") is not True: + raise AssertionError("Failed campaign shard") + if len(row["routes"]) != len(row["selectedRouteIds"]) or {r["id"] for r in row["routes"]} != set(row["selectedRouteIds"]): + raise AssertionError("Incomplete shard routes") + routes = [route for row in documents for route in row["routes"]] + if len(routes) != len(REQUIRED[first["kind"]]) or {row["id"] for row in routes} != set(REQUIRED[first["kind"]]): + raise AssertionError("Missing or duplicate full-matrix routes") + if any(row["status"] != "PASS" for row in routes): raise AssertionError("Failed route") + return dict(status="PASS", kind=first["kind"], observationTicks=first["observationTicks"], routes=routes, + targetManifestSha256=first["targetManifestSha256"], routesManifestSha256=first["routesManifestSha256"]) + + +def aggregate(args): + output = native117.output_path(args.output) + if output.exists(): raise FileExistsError(output) + documents = [read_json(path) for path in args.reports] + result = aggregate_shards(documents) + result["shardReports"] = {str(path.resolve()): digest(path) for path in args.reports} + output.parent.mkdir(parents=True, exist_ok=True) + write_json(output, result) + return 0 + + +def attach115(args): + """Attach explicit approved receipts to a NEW route document, without launching a source.""" + document = read_json(args.routes) + require_routes(document) + output = native117.output_path(args.output) + for protected in (args.routes, args.receipts): + native117.require_disjoint(output, protected) + bundle = read_json(args.receipts) + if bundle.get("acceptedPower115") != ACCEPTED_POWER115: + raise AssertionError("Receipt bundle must select the explicitly merged Power 1.15 source") + receipts = bundle["receipts"] + expected = SOURCE115["frozen"] | SOURCE115["connected"] + if len(receipts) != len(expected) or {entry["id"] for entry in receipts} != expected: + raise AssertionError("All six native115 source receipts are required exactly once") + output.mkdir(parents=True, exist_ok=False) + for entry in receipts: + receipt_path = approved_file(entry["receipt"], entry["sha256"]) + receipt = read_json(receipt_path) + source = Path(receipt["sourceRuntime"]) + native117.require_disjoint(output, source) + original = next(row for row in document["routes"] if row["id"] == entry["id"]) + row = source_row(output / "source-records", original["kind"], original["id"], source, + Path(receipt["target115Manifest"]), "target115", receipt["sourceSnapshot"], + receipt.get("sourceFixtureKey")) + row.update(sourceStatus="GENERATED_DISPOSABLE_115_VERIFIED", source115Receipt=str(receipt_path), + source115ReceiptSha256=digest(receipt_path)) + require_source115_receipt(row) + original.clear() + original.update(row) + document["source115Receipts"] = str(args.receipts.resolve()) + document["source115ReceiptsSha256"] = digest(args.receipts) + write_json(output / "native117-upgrade-routes.json", document) + return 0 + + +def _require_historical_routes(document): + """The previous 30 rows remain authoritative, including existing receipts.""" + if document.get("format") != 1 or document.get("acceptedPower114") != ACCEPTED_POWER114: + raise AssertionError("Retain the accepted historical route document") + rows = document["routes"] + expected = {(kind, identity) for kind, identities in REQUIRED.items() for identity in identities - SOURCE116[kind]} + if len(rows) != 30 or {(row.get("kind"), row.get("id")) for row in rows} != expected: + raise AssertionError("Exactly the 30 historical routes and receipts are required") + for row in rows: + require_source114_receipt(row) + require_source115_receipt(row) + + +def upgrade116(args): + """Add six PENDING routes to a NEW document; never regenerate older sources.""" + document = read_json(args.routes) + _require_historical_routes(document) + output = native117.output_path(args.output) + native117.require_disjoint(output, args.routes) + before = digest(args.routes) + for row in document["routes"]: + for key in ("source", "sourceManifest", "sourceHashes", "source114Receipt", "source115Receipt"): + if key in row: + native117.require_disjoint(output, Path(row[key])) + document["routes"].extend(pending116_routes()) + document.update(acceptedPower116=ACCEPTED_POWER116, qualificationStatus="UNQUALIFIED", + previousRoutes=str(args.routes.resolve()), previousRoutesSha256=before) + require_routes(document) + if digest(args.routes) != before: + raise AssertionError("Historical route authority changed during upgrade") + if getattr(args, "validate_only", False): + print("PREFLIGHT_ONLY: 30 retained routes + 6 pending native116 sources; no runtime or output") + return 0 + output.mkdir(parents=True, exist_ok=False) + write_json(output / "native117-upgrade-routes.json", document) + return 0 + + +def attach116(args): + """Attach exactly six approved stopped sources; retain all 30 existing rows.""" + document = read_json(args.routes) + require_routes(document) + historical = dict(document, routes=[row for row in document["routes"] if not row["id"].startswith("native116/")]) + _require_historical_routes(historical) + selected = [row for row in document["routes"] if row["id"].startswith("native116/")] + if any(row.get("sourceStatus") != "REQUIRES_APPROVED_DISPOSABLE_116_SOURCE" for row in selected): + raise AssertionError("Do not replace existing native116 source receipts") + output = native117.output_path(args.output) + sources_path = getattr(args, "sources", None) or args.receipts.with_name("source116-routes.json") + files = {str(path.resolve()): digest(path) for path in (args.routes, args.receipts, sources_path)} + for path in files: + native117.require_disjoint(output, Path(path)) + bundle, generated = read_json(args.receipts), read_json(sources_path) + if (bundle.get("status") != "PASS" or bundle.get("scope") != SOURCE116_SCOPE + or bundle.get("acceptedPower116") != ACCEPTED_POWER116 + or bundle.get("canonicalSourcesUnchanged") is not True or bundle.get("seedCodeUnchanged") is not True): + raise AssertionError("All six stopped source116 receipts must pass with original/code preservation") + expected = SOURCE116["frozen"] | SOURCE116["connected"] + receipts, rows = bundle["receipts"], generated["routes"] + if (len(receipts) != 6 or {row["id"] for row in receipts} != expected + or len(rows) != 6 or {row["id"] for row in rows} != expected + or generated.get("acceptedPower116") != ACCEPTED_POWER116 + or generated.get("qualificationStatus") != "UNQUALIFIED"): + raise AssertionError("Exactly six source116 receipt/route pairs are required; destination PASS is not approval") + for row in rows: + entry = next(item for item in receipts if item["id"] == row["id"]) + if (Path(entry["receipt"]).resolve() != Path(row["source116Receipt"]).resolve() + or entry["sha256"].upper() != row["source116ReceiptSha256"].upper()): + raise AssertionError("Source116 bundle and source route receipt differ") + for key in ("source", "sourceManifest", "sourceHashes", "source116Receipt"): + native117.require_disjoint(output, Path(row[key])) + require_source116_receipt(row) + index = next(i for i, item in enumerate(document["routes"]) if item["id"] == row["id"]) + document["routes"][index] = row + if historical["routes"] != [row for row in document["routes"] if not row["id"].startswith("native116/")]: + raise AssertionError("Historical rows/receipts changed during attachment") + for path, checksum in files.items(): + approved_file(path, checksum) + document.update(acceptedPower116=ACCEPTED_POWER116, qualificationStatus="UNQUALIFIED", + source116Receipts=str(args.receipts.resolve()), source116ReceiptsSha256=digest(args.receipts), + source116Routes=str(sources_path.resolve()), source116RoutesSha256=digest(sources_path)) + require_routes(document) + output.mkdir(parents=True, exist_ok=False) + write_json(output / "native117-upgrade-routes.json", document) + return 0 + + +def generate115(args): + """Generate six NEW accepted115 sources without regenerating other sources.""" + plan = preflight_source115(args) + document = read_json(getattr(args, "routes", None) or args.source_routes) + document.update(acceptedPower115=ACCEPTED_POWER115, qualificationStatus="UNQUALIFIED") + if getattr(args, "routes", None) is None: + document["routes"].extend(pending_routes()) + document["routes"].extend(pending116_routes()) + document["acceptedPower116"] = ACCEPTED_POWER116 + require_routes(document) + for row in document["routes"]: + if row["id"].startswith("native115/") and row.get("sourceStatus") != "REQUIRES_APPROVED_DISPOSABLE_115_SOURCE": + raise AssertionError("Do not replace an existing native115 source receipt") + if args.validate_only: + import json + print(json.dumps(dict(status="PREFLIGHT_ONLY_NO_RUNTIME", sourceCount=len(plan["sources"]), + target115Manifest=plan["target115Manifest"], target115ManifestSha256=plan["target115ManifestSha256"], + output=plan["output"], port=args.port), indent=2)) + return 0 + output = Path(plan["output"]) + output.mkdir(parents=True, exist_ok=False) + write_json(output / "source115-input-provenance.json", plan) + bundle = dict(status="RUNNING", scope="DISPOSABLE_NATIVE115_SOURCE_ONLY", acceptedPower115=ACCEPTED_POWER115, + target115Manifest=plan["target115Manifest"], target115ManifestSha256=plan["target115ManifestSha256"], receipts=[]) + try: + for selected in plan["sources"]: + verify_source115_inputs(plan) + identity, kind = selected["id"], selected["kind"] + destination = output / identity.replace("/", "-") + destination.mkdir() + settings = source115_settings(args, selected["recipe"], destination / "runtime") + receipt_path = destination / "source115-receipt.json" + receipt = dict(status="RUNNING", scope="DISPOSABLE_NATIVE115_SOURCE_ONLY", acceptedPower115=ACCEPTED_POWER115, + id=identity, kind=kind, stoppedCleanly=False, target115Manifest=plan["target115Manifest"], + target115ManifestSha256=plan["target115ManifestSha256"], sourceRecipe=selected["recipe"], + method="New disposable native115 copy under exact archived F; canonical sources never launch", + java8=str(settings.java8), port=args.port) + write_json(receipt_path, receipt) + print("Generating " + identity + " from a protected copied fixture", flush=True) + try: + code, stops = run_source115(settings, kind) + if code: + raise AssertionError("Native115 source-generation protocol failed") + runtime, snapshot, fixture, evidence = source115_result(kind, settings, stops) + verify_source115_inputs(plan) + receipt.update(evidence, status="PASS", sourceRuntime=str(runtime.resolve()), sourceSnapshot=snapshot, + sourceFixtureKey=fixture, runtimeFingerprint=fingerprint(runtime), worldFingerprint=fingerprint(runtime / WORLD)) + write_json(receipt_path, receipt) + row = source_row(output / "source-records", kind, identity, runtime, settings.target_manifest, + "target115", snapshot, fixture) + row.update(sourceStatus="GENERATED_DISPOSABLE_115_VERIFIED", source115Receipt=str(receipt_path.resolve()), + source115ReceiptSha256=digest(receipt_path)) + require_source115_receipt(row) + original = next(row for row in document["routes"] if row["id"] == identity) + original.clear() + original.update(row) + bundle["receipts"].append(dict(id=identity, receipt=str(receipt_path.resolve()), sha256=digest(receipt_path))) + print("PASS " + identity + ": saved/stopped native115 source only; native117 remains UNQUALIFIED", flush=True) + except BaseException as error: + receipt.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + write_json(receipt_path, receipt) + raise + finally: + write_json(output / "native117-upgrade-routes.json", document) + write_json(output / "source115-receipts.json", bundle) + require_routes(document) + for row in document["routes"]: + require_source115_receipt(row) + verify_source115_inputs(plan) + bundle.update(status="PASS", canonicalSourcesUnchanged=True, seedCodeUnchanged=True) + document.update(source115Receipts=str((output / "source115-receipts.json").resolve())) + except BaseException as error: + bundle.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + raise + finally: + try: + verify_source115_inputs(plan) + except BaseException as error: + bundle.update(status="FAIL", protectionFailure=str(error)) + raise + finally: + write_json(output / "source115-receipts.json", bundle) + document["source115ReceiptsSha256"] = digest(output / "source115-receipts.json") + write_json(output / "native117-upgrade-routes.json", document) + return 0 + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + pin = commands.add_parser("prepare") + pin.add_argument("--frozen-evidence", type=Path, required=True) + pin.add_argument("--connected-evidence", type=Path, required=True) + pin.add_argument("--output", type=Path, required=True) + source114 = commands.add_parser("generate114", help="Generate and shut down new disposable accepted114 sources; never launch canonical saves") + source114.add_argument("--routes", type=Path, required=True) + source114.add_argument("--java8", type=Path, required=True) + source114.add_argument("--output", type=Path, required=True) + source114.add_argument("--port", type=int, default=25595) + source115 = commands.add_parser("attach115", help="Attach six approved stopped native115 source receipts; no source launch") + source115.add_argument("--routes", type=Path, required=True) + source115.add_argument("--receipts", type=Path, required=True) + source115.add_argument("--output", type=Path, required=True) + upgrade = commands.add_parser("upgrade116", help="Retain all 30 approved historical rows and add six pending 1.16 sources; no source launch") + upgrade.add_argument("--routes", type=Path, required=True) + upgrade.add_argument("--output", type=Path, required=True) + upgrade.add_argument("--validate-only", action="store_true") + source116 = commands.add_parser("attach116", help="Attach six explicit stopped 1.16 receipts to a NEW 36-route document; no runtime") + source116.add_argument("--routes", type=Path, required=True) + source116.add_argument("--receipts", type=Path, required=True) + source116.add_argument("--sources", type=Path, help="Generated source116-routes.json; defaults beside the receipt bundle") + source116.add_argument("--output", type=Path, required=True) + generate = commands.add_parser("generate115", help="Create six disposable stopped115 sources under exact archived F; one leased server at a time") + generate.add_argument("--source-routes", type=Path, required=True) + generate.add_argument("--routes", type=Path, help="Canonical native117 plan to retain; legacy recipes must match the sealed F source plan") + generate.add_argument("--target115-manifest", type=Path, required=True) + generate.add_argument("--java8", type=Path, required=True) + generate.add_argument("--output", type=Path, required=True) + generate.add_argument("--port", type=int, required=True) + generate.add_argument("--validate-only", action="store_true", help="Read-only preflight; never creates an output or launches a process") + launch = commands.add_parser("run") + launch.add_argument("kind", choices=("frozen", "connected")) + launch.add_argument("--routes", type=Path, required=True) + launch.add_argument("--target-manifest", type=Path, required=True) + launch.add_argument("--java8", type=Path, required=True) + launch.add_argument("--output", type=Path, required=True) + launch.add_argument("--port", type=int, default=25595) + launch.add_argument("--ticks", type=int, default=512, help="Fixed interval for every connected route; never retry until a drill passes") + launch.add_argument("--shard-index", type=int, default=0) + launch.add_argument("--shard-count", type=int, default=1) + combine = commands.add_parser("aggregate", help="Require every disjoint shard before accepting a full campaign") + combine.add_argument("--reports", nargs="+", type=Path, required=True) + combine.add_argument("--output", type=Path, required=True) + arguments = parser.parse_args(argv) + return {"prepare": prepare, "generate114": generate114, "generate115": generate115, + "attach115": attach115, "upgrade116": upgrade116, "attach116": attach116, + "run": run, "aggregate": aggregate}[arguments.command](arguments) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_code.py b/test-rig/migration/native117_code.py new file mode 100644 index 00000000..dc02c4bb --- /dev/null +++ b/test-rig/migration/native117_code.py @@ -0,0 +1,116 @@ +"""Exact, code-only Forge seed custody; worlds and configuration are not code.""" + +import json +import queue +import shutil +import subprocess +import threading +from pathlib import Path +from matrix import Server as HistoricalServer, digest +import native117_startup_diagnostics as startup_diagnostics + +INVENTORY = "native117-code-inventory.json" + + +def code_files(root): + root = Path(root) + paths = list((root / "libraries").rglob("*")) + list(root.glob("*.jar")) + return {path.relative_to(root).as_posix(): {"sha256": digest(path), "size": path.stat().st_size} + for path in sorted(paths) if path.is_file()} + + +def verify_code(root, expected=None): + root = Path(root) + receipt = root / INVENTORY + if expected is None: + expected = json.loads(receipt.read_text(encoding="utf-8")) + files = expected.get("files") + if not isinstance(files, dict) or not files: + raise ValueError("Missing sealed native117 code inventory") + if code_files(root) != files: + raise AssertionError("Native117 server code differs from its exact sealed inventory: " + str(root)) + return expected + + +def verify_if_sealed(root): + if (Path(root) / INVENTORY).is_file(): + verify_code(root) + + +class CodeCustody: + """Adds pre-boot and post-shutdown checks without changing server commands.""" + def __init__(self, runtime, *args, **kwargs): + self.code_runtime = Path(runtime) + verify_if_sealed(self.code_runtime) + super().__init__(runtime, *args, **kwargs) + + def close(self): + try: + return super().close() + finally: + verify_if_sealed(self.code_runtime) + + +class NativeLaunch(HistoricalServer): + def validate_native_log(self, path): + from native117 import require_clean_log + require_clean_log(path) + + 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" + 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() + self.log = (runtime / f"console-{label}.log").open("x", encoding="utf-8") + self.process = subprocess.Popen( + [str(java), "-Xms512M", "-Xmx2G", "--add-opens", "java.base/java.lang.invoke=ALL-UNNAMED", + "-Dlog4j.configurationFile=" + logging_configuration, + "-Dfml.queryResult=confirm", "@" + str(launcher), "nogui"], + cwd=runtime, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, encoding="utf-8", errors="replace", creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + self.thread = threading.Thread(target=self.read, daemon=True) + self.thread.start() + try: + self.wait("Done (", 300) + except BaseException as failure: + diagnostic = None + if isinstance(failure, Exception): + try: + self.log.flush() + diagnostic = startup_diagnostics.observe(runtime, java, self.process, label, failure) + except Exception as error: + failure.add_note("Startup diagnostic observation failed: " + repr(error)) + cleanup_failure = None + try: + self.close() + except BaseException as error: + cleanup_failure = error + failure.add_note("Startup cleanup also failed: " + repr(error)) + if diagnostic is not None: + try: + startup_diagnostics.retain(runtime, label, diagnostic, cleanup_failure) + except Exception as error: + failure.add_note("Startup diagnostic retention failed: " + repr(error)) + raise + + def close(self): + try: + return super().close() + finally: + for name in ("latest.log", "debug.log"): + source = self.native_runtime / "logs" / name + destination = self.native_runtime / f"{name[:-4]}-{self.native_label}.log" + if source.is_file() and not destination.exists(): shutil.copy2(source, destination) + for kind in ("console", "latest", "debug"): + archived = self.native_runtime / f"{kind}-{self.native_label}.log" + if not archived.is_file(): + raise AssertionError("Missing native runtime log: " + str(archived)) + self.validate_native_log(archived) + + +class Server(CodeCustody, NativeLaunch): + pass diff --git a/test-rig/migration/native117_commands.py b/test-rig/migration/native117_commands.py new file mode 100644 index 00000000..ed171b65 --- /dev/null +++ b/test-rig/migration/native117_commands.py @@ -0,0 +1,18 @@ +"""Translate the fixture's old inventory command to the native 1.17 command.""" + +import re +from nbtlib import parse_nbt + +REPLACE = re.compile(r'replaceitem block (-?\d+ -?\d+ -?\d+) (container\.\d+) ([a-z0-9_.-]+:[a-z0-9_./-]+)(\{.*\})? ([1-9]\d*)') + + +def native(command): + if not command.startswith('replaceitem '): + return command + match = REPLACE.fullmatch(command) + if match is None: + raise ValueError('Unrecognized legacy fixture inventory command') + payload = match[4] or '' + if payload: + parse_nbt(payload) + return 'item replace block ' + match[1] + ' ' + match[2] + ' with ' + match[3] + payload + ' ' + match[5] diff --git a/test-rig/migration/native117_connected.py b/test-rig/migration/native117_connected.py new file mode 100644 index 00000000..3ec65a40 --- /dev/null +++ b/test-rig/migration/native117_connected.py @@ -0,0 +1,275 @@ +"""Resume prebuilt, pinned connected source copies in two genuine native117 boots. + +Only staging and process adapters are injected into connected.prepare/resume; +the connected state, stock and operation oracles are reused unchanged. +""" + +import argparse +import json +from pathlib import Path +import re +import socket +import shutil +from unittest.mock import patch + +import connected +from native117_code import CodeCustody, NativeLaunch, INVENTORY, verify_if_sealed +from matrix import WORLD, compare, digest, fingerprint, write_json +from native117 import (LAUNCHER, compare_upgraded, configure_creation, native_normalized, output_path, pinned_profile, + require_clean_log, require_disjoint) +from native117_save_io import capture_save, require_same_settings + + +def read_json(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def require_command(command): + if command in ("stop", "save-all", "save-all flush"): + return + if re.fullmatch(r"advconnected snapshot [A-Za-z0-9_-]+", command): + return + match = re.fullmatch(r"advconnected step ([0-9]+) [A-Za-z0-9_-]+", command) + if match and 1 <= int(match[1]) <= 4000: + return + raise AssertionError("Forbidden native connected mutation: " + command) + + +def require_document(document, definition, *, destination): + expected = {entry["id"]: entry for entry in definition["blocks"]} + rows = document["snapshot"] + actual = {entry["id"]: entry for entry in rows} + if len(expected) != len(definition["blocks"]) or len(actual) != len(rows) or actual.keys() != expected.keys(): + raise AssertionError("Connected witness IDs/count changed") + for key, entry in actual.items(): + for field in ("station", "kind", "name", "x", "y", "z"): + if entry[field] != expected[key][field]: + raise AssertionError(f"Connected witness changed: {key}.{field}") + if not document.get("held"): + raise AssertionError("Connected snapshot is not frozen") + if destination and document.get("buildCallsThisProcess") != 0: + raise AssertionError("Destination construction is forbidden") + verdict = connected.state(document) + if verdict["status"] != "PASS": + raise AssertionError("Invalid connected stocks: " + json.dumps(verdict)) + return verdict + + +def require_native_boot(log): + require_clean_log(log) + text = log.read_text(encoding="utf-8", errors="replace") + for marker in ("Starting minecraft server version 1.17.1", + "Forge mod loading, version 37.1.1, for MC 1.17.1 with MCP 20210706.113038"): + if marker not in text: + raise AssertionError("Missing genuine native117 boot proof: " + marker) + + +def verify_source(args): + source = args.source.resolve() + definition = read_json(source / "connected-fixture.json") + if definition.get("format") != 1: + raise ValueError("Unsupported connected fixture format") + if definition.get("sourceProfile") != (getattr(args, "source_fixture_key", None) or args.source_key): + raise ValueError("Source fixture profile does not match the explicit source key") + before = fingerprint(source) + expected = read_json(args.source_hashes) + if not expected or fingerprint(source / WORLD) != expected: + raise AssertionError("Protected connected source world fingerprint mismatch") + profile = pinned_profile(args.source_manifest.resolve(), args.source_key) + pins = sorted((jar.name, digest(jar)) for jar in profile[2]) + staged = read_json(source / "input-jars.json") + if sorted((entry["file"], entry["sha256"].upper()) for entry in staged) != pins: + raise AssertionError("Source fixture inputs differ from the explicit archived profile") + if sorted((jar.name, digest(jar)) for jar in (source / "mods").glob("*.jar")) != pins: + raise AssertionError("Source runtime jars differ from the explicit archived profile") + if not re.fullmatch(r"[A-Za-z0-9_-]+", args.source_checkpoint): + raise ValueError("Unsafe source checkpoint") + snapshot = connected.read(source, args.source_checkpoint) + if snapshot["snapshot"] != read_json(source / f"migration-{args.source_checkpoint}.json"): + raise AssertionError("Retained connected and migration checkpoints disagree") + verdict = require_document(snapshot, definition, destination=False) + if snapshot.get("requestedTicks", 0) <= 0 or snapshot.get("advancedTicks") != snapshot["requestedTicks"]: + raise AssertionError("Source checkpoint lacks an exact measured native interval") + if args.source_checkpoint == "source": + source_operation = connected.operation(connected.read(source, "cold"), snapshot, snapshot["requestedTicks"]) + if source_operation["status"] != "PASS": + raise AssertionError("Retained source did not operate: " + json.dumps(source_operation)) + history = source.parent / "connected-results.json" + historical = read_json(history) if history.is_file() else None + if historical is not None and historical.get("sources", {}).get(args.source_key, {}).get("status") != "PASS": + raise AssertionError("Retained source operation did not pass; do not manufacture a replacement") + if fingerprint(source) != before: + raise AssertionError("Source changed during preflight") + return definition, snapshot, verdict, before, historical + + +def prepare_target(runtime, profile, definition, port, source, pins): + if profile[1] != LAUNCHER: + raise ValueError("Only the genuine native117 launcher is permitted") + verify_if_sealed(profile[0]) + launcher = connected.prepare(runtime, profile, definition, port, source) + if (profile[0] / INVENTORY).is_file(): + shutil.copy2(profile[0] / INVENTORY, runtime / INVENTORY) + verify_if_sealed(runtime) + configure_creation(runtime) + for marker in ("connected-build.allow", "connected-built.json"): + if (runtime / marker).exists(): + raise AssertionError("Source construction authority leaked into the destination") + expected = sorted((Path(jar["path"]).name, jar["sha256"].upper()) for jar in pins["jars"]) + actual = sorted((jar.name, digest(jar)) for jar in (runtime / "mods").glob("*.jar")) + if actual != expected: + raise AssertionError("Staged native candidate jars differ from the pinned profile") + if digest(runtime / launcher) != pins["launcher_sha256"].upper(): + raise AssertionError("Staged native launcher differs from the pinned profile") + return launcher + + +class NativeConnectedServer(CodeCustody, connected.ConnectedServer, NativeLaunch): + """Keep command tracing before the native launch and log lifecycle in the MRO.""" + def __init__(self, runtime, java, launcher, label, on_loaded): + if launcher != LAUNCHER: + raise AssertionError("Refusing a non-native117 process") + self.on_loaded = on_loaded + self.loaded_verified = False + super().__init__(runtime, java, launcher, label) + try: + require_native_boot(Path(self.log.name)) + except BaseException: + self.close() + raise + + def send(self, command): + require_command(command) + if command.startswith("advconnected step ") and not self.loaded_verified: + raise AssertionError("Preservation must pass before any destination operation") + super().send(command) + + def observe(self, operation_name, label, ticks=0): + super().observe(operation_name, label, ticks) + if operation_name == "snapshot" and ticks == 0: + self.on_loaded(label) + self.loaded_verified = True + + +def continued_output(first, last): + initial, final = connected.state(first), connected.state(last) + missing = [key for key, case in initial["cases"].items() + if key != "battery" and case["remaining"] > 0 + and final["cases"][key]["produced"] <= case["produced"]] + return dict(status="FAIL" if missing else "PASS", failures=missing) + + +def run(args): + from semantic_machine_audit import connected_interval + from native117_resource_audit import compare_snapshot_resources + if not 1 <= args.port <= 65535 or not 1 <= args.ticks <= 4000: + raise ValueError("Port/tick interval outside native command bounds") + output = output_path(args.output) + for protected in (args.source, args.source_manifest, args.source_hashes, args.target_manifest): + require_disjoint(output, protected) + definition, checkpoint, source_state, source_before, historical = verify_source(args) + target = pinned_profile(args.target_manifest.resolve(), "target117") + target_pins = read_json(args.target_manifest)["target117"] + identities = {entry["id"] for entry in target_pins["jars"]} + if not {"basesteam", "baseelectric"}.issubset(identities): + raise ValueError("Connected qualification requires native Steam and Electric candidates") + require_disjoint(output, target[0]) + for jar in target[2]: + require_disjoint(output, jar) + with socket.socket() as listener: + listener.bind(("127.0.0.1", args.port)) + output.mkdir(parents=True, exist_ok=False) + runtime = output / "target117" + report_path = output / "native117-connected-results.json" + report = dict(status="RUNNING", scope="prebuilt-connected-source-to-native117-two-boots", + protocol="No source process; destination snapshot/step/save/stop only; no rebuilding/refilling/resetting", + source=str(args.source.resolve()), sourceKey=args.source_key, + sourceFixtureKey=getattr(args, "source_fixture_key", None) or args.source_key, + sourceCheckpoint=args.source_checkpoint, sourceState=source_state, + sourceManifest=str(args.source_manifest.resolve()), sourceManifestSha256=digest(args.source_manifest), + sourceHashes=str(args.source_hashes.resolve()), sourceHashesSha256=digest(args.source_hashes), + sourceRuntimeBefore=source_before, sourceWorldExpected=read_json(args.source_hashes), + sourceCheckpointSha256=digest(args.source / f"connected-{args.source_checkpoint}.json"), + targetManifest=str(args.target_manifest.resolve()), targetManifestSha256=digest(args.target_manifest), + targetProfile=target_pins, port=args.port, ticks=args.ticks, + runnerSha256=digest(Path(__file__)), oracleSha256=digest(Path(connected.__file__)), + semanticOracleSha256=digest(Path(__file__).with_name("semantic_machine_audit.py")), + historicalCampaign=historical, results={}) + write_json(report_path, report) + try: + launcher = prepare_target(runtime, target, definition, args.port, args.source, target_pins) + report["stagedInputs"] = read_json(runtime / "input-jars.json") + for label, previous in (("converted", args.source / f"migration-{args.source_checkpoint}.json"), + ("restarted", runtime / "migration-converted-worked.json")): + def on_loaded(loaded_label): + loaded = connected.read(runtime, loaded_label) + require_document(loaded, definition, destination=True) + if loaded.get("requestedTicks") != 0 or loaded.get("advancedTicks") != 0: + raise AssertionError("Initial preservation requires a frozen zero-step snapshot") + result = compare_upgraded(previous, runtime / f"migration-{loaded_label}.json") if label == "converted" \ + else compare(previous, runtime / f"migration-{loaded_label}.json", native_normalized) + report["results"][label + "_preservation"] = result + resources = compare_snapshot_resources(previous, runtime / f"migration-{loaded_label}.json", + legacy_source=label == "converted") + report["results"][label + "_exact_resources"] = resources + write_json(report_path, report) + if result["status"] != "PASS" or resources["status"] != "PASS": + raise AssertionError(label + " native117 preservation failed") + + def boot(current, settings, selected_launcher, selected_label): + if current.resolve() != runtime.resolve(): + raise AssertionError("Only the newly staged target may be launched") + return NativeConnectedServer(current, settings.java8, selected_launcher, selected_label, on_loaded) + + # Dependency injection is confined to the process factory, never an oracle. + with patch.object(connected, "boot", boot): + verdict = connected.resume(runtime, args, launcher, label) + report["results"][label + "_operation"] = verdict + report["results"][label + "_completed_work"] = connected_interval( + connected.read(runtime, label + "-loaded"), connected.read(runtime, label + "-worked"), + definition, args.ticks) + require_document(connected.read(runtime, label + "-worked"), definition, destination=True) + require_native_boot(runtime / f"console-{label}.log") + report["results"][label + "_native_boot"] = dict(status="PASS", launcher=launcher) + report.setdefault("nativeSaveFormat", {})[label] = capture_save( + runtime, label, runtime / f"migration-{label}-worked.json") + write_json(report_path, report) + require_same_settings(report["nativeSaveFormat"]["converted"], report["nativeSaveFormat"]["restarted"]) + report["results"]["real_output_across_restarts"] = continued_output( + connected.read(runtime, "converted-loaded"), connected.read(runtime, "restarted-worked")) + report["status"] = "PASS" if all(result["status"] == "PASS" for result in report["results"].values()) else "FAIL" + except BaseException as error: + report.update(status="FAIL", failure=str(error)) + raise + finally: + source_after = fingerprint(args.source) + report["sourceRuntimeAfter"] = source_after + preserved = source_after == source_before + report["sourcePreservation"] = dict(status="PASS" if preserved else "FAIL") + if not preserved: + report.update(status="FAIL", failure="Protected source runtime changed") + write_json(report_path, report) + if not preserved: + raise AssertionError("Protected source runtime changed") + print(report["status"] + ": " + str(report_path), flush=True) + return int(report["status"] != "PASS") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--source-manifest", type=Path, required=True) + parser.add_argument("--source-key", required=True) + parser.add_argument("--source-fixture-key", help="Explicit retained assembly lineage when the source manifest is target113/target114") + parser.add_argument("--source-hashes", type=Path, required=True) + parser.add_argument("--source-checkpoint", default="source") + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, default=25576) + parser.add_argument("--ticks", type=int, default=512) + return run(parser.parse_args()) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_dimension_assessment.py b/test-rig/migration/native117_dimension_assessment.py new file mode 100644 index 00000000..ae114b84 --- /dev/null +++ b/test-rig/migration/native117_dimension_assessment.py @@ -0,0 +1,186 @@ +"""Separate, pinned native-wolf difference assessment; raw strict FAIL stays FAIL.""" + +import argparse +import copy +import hashlib +import json +from pathlib import Path +import uuid + +import dimensions +from native117_dimension_io import semantic_modifier_representation + + +CONTROL_LOG_SHA256 = "334DEC1307EFDAC8F30FEF60B6F61E60C13B0EF7F1F1F7CA5DD029CC33DCAD91" +HEALTH_PATH = ("entities", "passenger-wolf", "Health") +HEALTH_DIFFERENCE = {"path": ".entities.passenger-wolf.Health", "expected": 8.0, "actual": 20.0} + + +def load_native_proof(path): + raw = Path(path).read_bytes() + checksum = hashlib.sha256(raw).hexdigest().upper() + if checksum != CONTROL_LOG_SHA256: + raise AssertionError("Native wolf proof differs from the independently reviewed control log") + text = raw.decode("utf-8") + if "POWER_LEGACY_CLASS_ABSENT true" not in text or "WOLF_CONTROL_DONE no_server=true no_ticks=true native_entity_methods_unmodified=true" not in text: + raise AssertionError("Native no-Power control attestation missing") + records = {} + for line in text.splitlines(): + if line.startswith("WOLF_CONTROL "): + row = json.loads(line.removeprefix("WOLF_CONTROL ")) + if row["label"] in records: + raise AssertionError("Duplicate controlled wolf observation") + records[row["label"]] = row + expected = {"tame-health-8": (8.0, 20.0, 20.0), "tame-health-20": (20.0, 20.0, 20.0), + "wild-health-8": (8.0, 8.0, 8.0)} + if set(records) != set(expected): + raise AssertionError("Incomplete native wolf controls") + for name, health in expected.items(): + row = records[name] + if (row.get("currentWorldDataVersion") != 2730 or row.get("legacyDfuInvoked") is not False + or row.get("directNativeEntityLoadSave") is not True or row.get("uuidPreserved") is not True + or row.get("ownerPreserved") is not True + or tuple(row.get(key) for key in ("beforeHealth", "afterHealth", "restartHealth")) != health): + raise AssertionError("Native wolf control values differ") + return {"sha256": checksum, "worldDataVersion": 2730, "forgeVersion": "37.1.1", + "scope": "NATIVE_TAMED_WOLF_LOAD_8_TO_20_NO_POWER"} + + +def exact_except_native_health(expected, actual, path=()): + if path == HEALTH_PATH: + return type(expected) in (int, float) and type(actual) in (int, float) and expected == 8 and actual == 20 + if type(expected) is not type(actual): + return False + if isinstance(expected, dict): + return set(expected) == set(actual) and all(exact_except_native_health(value, actual[key], path + (key,)) + for key, value in expected.items()) + if isinstance(expected, list): + return len(expected) == len(actual) and all(exact_except_native_health(old, new, path + (index,)) + for index, (old, new) in enumerate(zip(expected, actual))) + return expected == actual + + +def semantic_player_spawn_defaults(state): + """Expose only the proven absent player respawn dimension's Overworld default.""" + result = copy.deepcopy(state) + for root in ("file", "adapter"): + player = result.get(root) + if (isinstance(player, dict) and "SpawnDimension" not in player + and all(type(player.get(key)) is int and -(1 << 31) <= player[key] < (1 << 31) + for key in ("SpawnX", "SpawnY", "SpawnZ"))): + player["SpawnDimension"] = 0 + return result + + +def _scope(value, scope, adapter): + if scope == "exact": + result = copy.deepcopy(value) + elif scope == "strict": + result = dimensions.strict_expected(value, adapter) + elif scope == "survival": + result = dimensions.survival_expected(value) + if not adapter: + result.pop("adapter", None) + result.pop("playerWorldDimension", None) + else: + raise ValueError("Unknown historical comparison scope") + return semantic_player_spawn_defaults(semantic_modifier_representation(result)) + + +def assess(expected, actual, strict, proof_path=None, scope="exact", adapter=True): + recomputed = dimensions.verdict(expected, actual) + if strict != recomputed: + raise AssertionError("Supplied raw strict result differs from the unchanged historical oracle") + result = {"status": strict["status"], "rawStrictStatus": strict["status"], + "rawStrictResult": copy.deepcopy(strict), "nativeDifferences": []} + if strict["status"] == "PASS": + return result + if strict.get("differences") != [HEALTH_DIFFERENCE] or proof_path is None: + return result + before = expected.get("entities", {}).get("passenger-wolf", {}) + after = actual.get("entities", {}).get("passenger-wolf", {}) + if before.get("id") != "minecraft:wolf" or after.get("id") != "minecraft:wolf": + return result + for key in ("UUIDMost", "UUIDLeast"): + old, new = before.get(key), after.get(key) + if (type(old) is not int or type(new) is not int or not -(1 << 63) <= old < (1 << 63) + or not -(1 << 63) <= new < (1 << 63) or old != new): + return result + try: + owner = str(uuid.UUID(before["OwnerUUID"])) + if before["OwnerUUID"] != owner or after.get("OwnerUUID") != owner: + return result + except (KeyError, ValueError, TypeError, AttributeError): + return result + if before.get("Attributes", {}).get("generic.maxHealth", {}).get("Base") != 20: + return result + old = _scope(expected, scope, adapter) + new = _scope(actual, scope, adapter) + if not exact_except_native_health(old, new): + return result + proof = load_native_proof(proof_path) + result.update(status="PASS_WITH_NATIVE_DIFFERENCE", allOtherFieldsStrictMatch=True, + comparisonScope=scope, controlledProof=proof, nativeDifferences=[dict(HEALTH_DIFFERENCE, entityUuid={ + key: before[key] for key in ("UUIDMost", "UUIDLeast")}, ownerUuid=owner)]) + return result + + +SEMANTIC_CHECKS = frozenset(("conversion", "adapter", "restart", "idempotence", "runtime", "runtime-restart")) +NATIVE_CHECKS = frozenset(prefix + phase for prefix in ('readiness-', 'saved-entities-') + for phase in ('conversion', 'restart', 'runtime-restart')) +DIMENSION_CHECKS = SEMANTIC_CHECKS | {"conversion-ticks", "runtime-ticks", "restart-ticks", "backup-stability"} | NATIVE_CHECKS + + +def summarize_checks(checks, assessments): + raw = "PASS" if checks and all(value.get("status") == "PASS" for value in checks.values()) else "FAIL" + result = {"status": "FAIL", "rawStrictStatus": raw, "allOtherFieldsStrictMatch": False} + if set(checks) != DIMENSION_CHECKS or set(assessments) != SEMANTIC_CHECKS: + return result + differences = [] + for key in sorted(checks): + check = checks[key] + assessment = assessments.get(key) + if assessment is not None: + if assessment.get("rawStrictResult") != check: + return result + if assessment.get("status") == "PASS_WITH_NATIVE_DIFFERENCE": + native = assessment.get("nativeDifferences", []) + if (check.get("status") != "FAIL" or assessment.get("allOtherFieldsStrictMatch") is not True + or assessment.get("controlledProof", {}).get("sha256") != CONTROL_LOG_SHA256 + or len(native) != 1 or {key: native[0].get(key) for key in HEALTH_DIFFERENCE} != HEALTH_DIFFERENCE + or any(type(native[0].get("entityUuid", {}).get(key)) is not int + or not -(1 << 63) <= native[0]["entityUuid"][key] < (1 << 63) + for key in ("UUIDMost", "UUIDLeast"))): + return result + differences.extend(dict(difference, check=key) for difference in assessment["nativeDifferences"]) + elif assessment.get("status") != "PASS" or check.get("status") != "PASS": + return result + elif check.get("status") != "PASS": + return result + result.update(status="PASS_WITH_NATIVE_DIFFERENCE" if differences else "PASS", + allOtherFieldsStrictMatch=True, nativeDifferences=differences) + return result + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("expected", "actual", "strict", "proof", "output"): + parser.add_argument("--" + name, type=Path, required=True) + parser.add_argument("--scope", choices=("exact", "strict", "survival"), default="exact") + parser.add_argument("--without-adapter", action="store_true") + args = parser.parse_args(argv) + if args.output.exists(): + raise FileExistsError("Never overwrite strict results or retained evidence") + if any(args.output.resolve() == path.resolve() for path in (args.expected, args.actual, args.strict, args.proof)): + raise ValueError("Assessment output overlaps a protected input") + read = lambda path: json.loads(path.read_text(encoding="utf-8")) + result = assess(read(args.expected), read(args.actual), read(args.strict), args.proof, + args.scope, not args.without_adapter) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(result["status"] + "; raw strict result remains " + result["rawStrictStatus"]) + return int(result["status"] == "FAIL") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_dimension_io.py b/test-rig/migration/native117_dimension_io.py new file mode 100644 index 00000000..e9024df4 --- /dev/null +++ b/test-rig/migration/native117_dimension_io.py @@ -0,0 +1,182 @@ +"""Observed native entity context and finite attribute-name representation only.""" + +import copy +import uuid + +from matrix import plain, parse_nbt +from native117_format import DIMENSIONS, actor_nbt, dimension_id, uuid_longs + + +VANILLA_ATTRIBUTES = { + "minecraft:generic.max_health": "generic.maxHealth", + "minecraft:generic.knockback_resistance": "generic.knockbackResistance", + "minecraft:generic.movement_speed": "generic.movementSpeed", + "minecraft:generic.armor": "generic.armor", + "minecraft:generic.armor_toughness": "generic.armorToughness", + "minecraft:generic.follow_range": "generic.followRange", + "minecraft:generic.attack_damage": "generic.attackDamage", + "minecraft:horse.jump_strength": "horse.jumpStrength", +} +# Exact registry identities checked against Forge's 1.10 through 1.16 sources. +FORGE_ATTRIBUTES = { + "forge:swim_speed": "forge.swimSpeed", + "forge:nametag_distance": "forge.nameTagDistance", + "forge:entity_gravity": "forge.entity_gravity", + "forge:reach_distance": "generic.reachDistance", +} +ATTRIBUTE_NAMES = dict(VANILLA_ATTRIBUTES, **FORGE_ATTRIBUTES) +KNOWN_ATTRIBUTES = frozenset(ATTRIBUTE_NAMES.values()) + + +def native_key(value): + if not isinstance(value, str) or value not in DIMENSIONS.values(): + raise ValueError("Unsupported observed native dimension key: " + repr(value)) + return value + + +def entity_uuid(value): + if "UUID" in value: + if "UUIDMost" in value or "UUIDLeast" in value: + raise ValueError("Duplicate native/legacy entity UUID encodings") + most, least = uuid_longs(value["UUID"]) + else: + most, least = value.get("UUIDMost"), value.get("UUIDLeast") + if any(isinstance(word, bool) or not isinstance(word, int) or not -(1 << 63) <= word < (1 << 63) + for word in (most, least)): + raise ValueError("Missing or invalid observed entity UUID") + return str(uuid.UUID(int=((most & ((1 << 64) - 1)) << 64) | (least & ((1 << 64) - 1)))) + + +def entity_tree(value): + if not isinstance(value, dict): + raise ValueError("Non-compound serialized entity") + yield value + if "Passengers" in value: + if not isinstance(value["Passengers"], list): + raise ValueError("Non-list serialized passengers") + for passenger in value["Passengers"]: + yield from entity_tree(passenger) + + +def observed_context(record): + if type(record.get("nativeEntityContextVersion")) is not int or record["nativeEntityContextVersion"] != 1: + raise ValueError("Missing versioned per-entity native world observation") + world = native_key(record.get("worldDimensionKey")) + if dimension_id(record.get("dimension")) != dimension_id(world): + raise AssertionError("Snapshot world key and observed dimension disagree") + if not isinstance(record.get("entities"), list): + raise ValueError("Missing native entity observations") + contexts, roots = {}, set() + for entry in record["entities"]: + identity = entity_uuid(plain(parse_nbt(entry))) + if identity in roots: + raise AssertionError("Native observation has duplicate/mismatched root UUID") + roots.add(identity) + for key, observed in entry_context(entry, world).items(): + if key in contexts and contexts[key] != observed: + raise AssertionError("Conflicting native world observations") + contexts[key] = observed + return contexts + + +def entry_context(entry, world): + if entry.get("sameWorld") is not True or native_key(entry.get("worldDimensionKey")) != world: + raise AssertionError("Entity was not observed in the same world instance") + payload = plain(parse_nbt(entry)) + identity = entity_uuid(payload) + if entry.get("uuid") != identity: + raise AssertionError("Mismatched native root UUID attestation") + tree = [entity_uuid(entity) for entity in entity_tree(payload)] + if len(tree) != len(set(tree)): + raise AssertionError("Duplicate UUID in serialized passenger tree") + worlds = entry.get("entityWorlds") + if not isinstance(worlds, dict) or set(worlds) != set(tree): + raise AssertionError("Passenger/entity world observation coverage differs") + for observed in worlds.values(): + if native_key(observed) != world: + raise AssertionError("Passenger was observed in another dimension") + return worlds + + +def native_entry(entry): + return (any(key in entry for key in ("worldDimensionKey", "sameWorld", "entityWorlds")) + or ("nbtBase64" in entry and "UUID" in plain(parse_nbt(entry)))) + + +def validate_record(record): + if isinstance(record, dict) and (any(key in record for key in + ("nativeEntityContextVersion", "worldDimensionKey", "sameWorld", "entityWorlds")) or + (isinstance(record.get("entities"), list) and any(native_entry(entry) for entry in record["entities"]))): + observed_context(record) + return record + + +def parse_dimension_nbt(entry): + value = plain(parse_nbt(entry)) + if "witness" in entry and native_entry(entry): + return project_entity(value, entry_context(entry, native_key(entry.get("worldDimensionKey")))) + return value + + +def attribute_list(attributes): + if not isinstance(attributes, list): + raise ValueError("Non-list entity attributes") + result, names = [], set() + for original in attributes: + if not isinstance(original, dict) or not isinstance(original.get("Name"), str): + raise ValueError("Malformed native attribute entry") + entry = copy.deepcopy(original) + name = ATTRIBUTE_NAMES.get(entry["Name"], entry["Name"]) + if name in names: + raise ValueError("Ambiguous native attribute identity: " + name) + names.add(name) + entry["Name"] = name + if "Modifiers" in entry and (not isinstance(entry["Modifiers"], list) + or any(not isinstance(modifier, dict) for modifier in entry["Modifiers"])): + raise ValueError("Malformed native attribute modifiers") + if name in KNOWN_ATTRIBUTES and "Modifiers" not in entry: + # Native AttributeInstance.save omits an empty permanent-modifier list. + entry["Modifiers"] = [] + result.append(entry) + return result + + +def project_entity(value, contexts): + """Project actual observations, never expected fixture values, on a copy.""" + result = copy.deepcopy(value) + identity = entity_uuid(result) + if identity not in contexts: + raise ValueError("No actual native world observation for entity " + identity) + observed = dimension_id(native_key(contexts[identity])) + if "Dimension" in result and dimension_id(result["Dimension"]) != observed: + raise AssertionError("Serialized entity dimension disagrees with its observed world") + result["Dimension"] = observed + if "Attributes" in result: + result["Attributes"] = attribute_list(result["Attributes"]) + if "Passengers" in result: + if not isinstance(result["Passengers"], list): + raise ValueError("Non-list native passengers") + result["Passengers"] = [project_entity(passenger, contexts) for passenger in result["Passengers"]] + return actor_nbt(result) + + +def semantic_modifier_representation(state): + """Make only known attributes' absent/empty modifier lists explicit on a copy.""" + result = copy.deepcopy(state) + + def entity(value): + attributes = value.get("Attributes", {}) + if not isinstance(attributes, dict): + raise ValueError("Malformed semantic attributes") + for name, attribute in attributes.items(): + if name in KNOWN_ATTRIBUTES: + if not isinstance(attribute, dict): + raise ValueError("Malformed semantic attribute") + if "Modifiers" not in attribute: + attribute["Modifiers"] = [] + for passenger in value.get("Passengers", []): + entity(passenger) + + for value in result.get("entities", {}).values(): + entity(value) + return result diff --git a/test-rig/migration/native117_endurance.py b/test-rig/migration/native117_endurance.py new file mode 100644 index 00000000..003a947a --- /dev/null +++ b/test-rig/migration/native117_endurance.py @@ -0,0 +1,353 @@ +"""Measured native117 endurance with historical and independent semantic checks.""" + +import argparse +from collections import Counter +from datetime import datetime, timezone +import json +from pathlib import Path +import shutil +import socket +import time +import traceback + +import nbtlib + +import endurance +import semantic_machine_audit +import native117_commands +from audit_endurance import expected_inventory_acknowledgements +from matrix import WORLD, digest, fingerprint, write_json +from native117 import compare_upgraded, output_path, pinned_profile, require_disjoint +from native117_identity import command_ids, target_id +from native117_lifecycle import (prepare, read_json, require_native_boot, + verify_source, verify_staged) +from native117_format import world_info as native_world_info, worldgen_settings +from native117_code import Server + + +NAMES = ("fresh117", "retained-upgrade-native117") +FRESH_FIXTURE_SEED = 5919679243107101036 +POI_POSITION = (-126, 5, 27) + + +def world_info(path): + data = nbtlib.load(path)["Data"] + return native_world_info(data.unpack(), allow_legacy=True) + + +def configure_new_fresh_fixture(runtime): + endurance.require(not (runtime / WORLD).exists(), "Fresh isolation refuses an existing/imported world") + properties = runtime / "server.properties" + before = digest(properties) + lines = [line for line in properties.read_text(encoding="utf-8").splitlines() + if line.strip().partition("=")[0] not in ("generate-structures", "level-seed")] + properties.write_text("\n".join(lines) + "\ngenerate-structures=false\n" + + f"level-seed={FRESH_FIXTURE_SEED}\n", encoding="utf-8") + proof = runtime / "fresh-fixture-proof" + proof.mkdir(exist_ok=False) + shutil.copy2(properties, proof / "server-before-first-creation.properties") + return dict(existing_world_absent=True, original_properties_sha256=before, + configured_properties_sha256=digest(properties)) + + +def verify_fresh_fixture_proof(runtime, proof): + endurance.require(proof.get("existing_world_absent") is True, "Missing new-world isolation guard") + for name, expected in proof["files"].items(): + endurance.require(digest(runtime / name) == expected, "Fresh isolation evidence changed: " + name) + properties = dict(line.split("=", 1) for line in + (runtime / "fresh-fixture-proof/server-before-first-creation.properties") + .read_text(encoding="utf-8").splitlines() if line and not line.startswith("#") and "=" in line) + endurance.require(properties.get("generate-structures") == "false" + and properties.get("level-seed") == str(FRESH_FIXTURE_SEED), "Wrong fresh creation settings") + actual = world_info(runtime / "fresh-fixture-proof/worldinfo-before-build.dat") + raw_settings = worldgen_settings(nbtlib.load(runtime / "fresh-fixture-proof/worldinfo-before-build.dat")["Data"].unpack()) + endurance.require(raw_settings == proof["native_worldgen_settings"], "Native WorldGenSettings proof changed") + endurance.require(actual["MapFeatures"] == 0, "Actual WorldInfo MapFeatures is not zero") + endurance.require(actual["RandomSeed"] == FRESH_FIXTURE_SEED, "Actual WorldInfo seed changed") + endurance.require(actual == proof["actual_world_info"], "WorldInfo report differs from raw NBT") + rows = read_json(runtime / "migration-fresh-isolation-before-build.json") + endurance.require(len(rows) == 1 and tuple(rows[0][key] for key in ("x", "y", "z")) == POI_POSITION, + "Missing exact original composter coordinate") + endurance.require(rows[0]["actualName"] == "minecraft:air", "Original composter coordinate is not AIR") + endurance.require(rows[0] == proof["critical_block"], "Critical block report differs from native snapshot") + return actual + + +def prove_fresh_fixture_before_build(rig, configuration): + runtime = rig.runtime + write_json(runtime / "migration-fixture.json", [dict(id="poi-target", x=POI_POSITION[0], + y=POI_POSITION[1], z=POI_POSITION[2])]) + rig.server.snapshot("fresh-isolation-before-build") + rig.command("save-all flush", "Saved the game") + saved = runtime / "fresh-fixture-proof/worldinfo-before-build.dat" + shutil.copy2(runtime / WORLD / "level.dat", saved) + files = ["fresh-fixture-proof/server-before-first-creation.properties", + "fresh-fixture-proof/worldinfo-before-build.dat", "migration-fresh-isolation-before-build.json"] + proof = dict(configuration, status="UNVERIFIED", actual_world_info=world_info(saved), + native_worldgen_settings=worldgen_settings(nbtlib.load(saved)["Data"].unpack()), + critical_block=read_json(runtime / files[-1])[0], files={p: digest(runtime / p) for p in files}, + scope="NEW fresh world only, before any Works build; no imported world or POI data edits") + path = runtime / "fresh-fixture-proof.json" + write_json(path, proof) + verify_fresh_fixture_proof(runtime, proof) + proof["status"] = "PASS" + write_json(path, proof) + return proof + + +class NativeEnduranceRig(endurance.Rig): + def command(self, command, response=None): + return super().command(native117_commands.native(command_ids(command)), response) + + def batch(self, label): + measured = semantic_machine_audit.endurance_batch( + {station: self.documents[station] for station in ("S-04", "E-03", "W-04", "W-03")}, + self.definitions, identity=target_id) + evidence = self.runtime / ("semantic-batch-" + label + ".json") + with evidence.open("x", encoding="utf-8") as stream: + json.dump(measured, stream, indent=2) + endurance.require(measured["status"] == "PASS", "Independent batch measurements failed: " + str(measured)) + return super().batch(label, power="basepower") + + 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) + if phase != "endurance-restart": + return + # Restore only chunk tickets, never station actions or machine NBT. + # Observing a real tick is transport readiness, not a desired output. + for station in endurance.CHECKED: + definition = self.definitions[station] + low = self.pos(station, definition["boundsMin"]) + high = self.pos(station, definition["boundsMax"]) + self.command(f"forceload add {low[0]} {low[2]} {high[0]} {high[2]}") + self.checkpoint("native-restart-immediate") + initial = self.documents["W-01"]["worldTime"] + started = time.monotonic() + for attempt in range(200): + self.checkpoint(f"native-restart-ready-{attempt:03}") + observed = self.documents["W-01"]["worldTime"] + if observed > initial: + write_json(self.runtime / "native-restart-readiness.json", { + "initial_world_time": initial, "selected_world_time": observed, + "selected_attempt": attempt, "elapsed_seconds": time.monotonic() - started, + "selection": "First measured tick advance; no inventory/output inspection", + }) + return + if time.monotonic() - started >= 20: + break + time.sleep(0.01) + raise TimeoutError("No actual native tick after restoring endurance tickets") + + +def audit(root, require_endurance=True): + report = read_json(root / "native117-endurance-results.json") + expected_status = "PASS" if require_endurance else "SMOKE_PASS_NOT_ENDURANCE" + endurance.require(report["status"] == expected_status, "No completed native endurance result") + endurance.require(report["interval_seconds"] == 60, "Not the one-minute batch protocol") + endurance.require(report["duration_seconds"] >= (1800 if require_endurance else 60), "Duration too short") + endurance.require(report["source_fixture_unchanged"] and report["helpers_unchanged"], "Source/protocol changed") + endurance.require(set(report["profiles"]) == set(NAMES), "Missing native environment") + endurance.require(report.get("fresh_fixture_proof", {}).get("status") == "PASS", "Missing fresh isolation proof") + verify_fresh_fixture_proof(root / NAMES[0], report["fresh_fixture_proof"]) + endurance.require(report["copied_world_info_before_build"] == report["source_world_info"], + "Imported world generation settings changed") + result = dict(status="PASS", scope="endurance" if require_endurance else "smoke-only", profiles={}) + failures = ("Unknown slot", "Could not replace", "Unknown or incomplete command", "Incorrect argument", + "Unknown item", "There is no container") + for name, profile in report["profiles"].items(): + endurance.require(profile["status"] == "PASS" and profile["restart"] == "PASS" + and profile["resumed_processing"] == "PASS", "Incomplete profile") + endurance.require(profile["elapsed_seconds"] >= report["duration_seconds"], "Wall time too short") + endurance.require(profile["game_ticks"] >= report["duration_seconds"] * 18, "Tick rate too low") + runtime = root / name + if name != NAMES[0]: + endurance.require(world_info(runtime / WORLD / "level.dat") == report["source_world_info"], + "Saved imported world generation settings changed") + counts = expected_inventory_acknowledgements(profile["endurance_batches"]) + for filename, count in zip(("console-endurance.log", "console-endurance-restart.log"), counts): + text = (runtime / filename).read_text(encoding="utf-8") + require_native_boot(runtime / filename) + endurance.require(not any(f in text for f in failures), "Native command failure") + endurance.require(text.count("Replaced a slot at") == count, "Inventory command acknowledgements differ") + if "restart" in filename: + endurance.require(not any(f"Started {s} " in text for s in ("S-04", "E-03", "E-02", "S-02", "E-01")), + "Restart replaced source machines") + endurance.require("Advantage Works built" not in text and "Reset 1 station" not in text, + "Restart rebuilt the rig") + pins = {item["file"]: item["sha256"] for item in report["artifacts"]} + actual = {jar.name: digest(jar) for jar in (runtime / "mods").glob("*.jar")} + endurance.require(actual == pins, "Staged native artifacts changed") + ledger = read_json(runtime / "endurance-ledger.json") + batches = [b for b in ledger if b["label"].startswith("cycle-")] + total = report["duration_seconds"] // report["interval_seconds"] + endurance.require(len(batches) == profile["endurance_batches"] == total, "Batch count differs") + endurance.require({b["label"] for b in batches} == {f"cycle-{i:03}" for i in range(1, total + 1)}, + "Repeated or missing batch") + endurance.require([b["label"] for b in ledger[-3:]] == ["before-restart", "after-restart", "restart-resumed"], + "Missing restart checkpoints") + result["profiles"][name] = dict(status="PASS", batches=total, inventory_acknowledgements=list(counts)) + return result + + +def run(args): + endurance.require(args.seconds >= 60 and args.seconds % 60 == 0, "Use whole one-minute batches") + output = output_path(args.output) + for protected in (args.source, args.source_manifest, args.source_hashes, args.target_manifest): + require_disjoint(output, protected) + source_before, history = verify_source(args) + target = pinned_profile(args.target_manifest.resolve(), "target117") + for protected in (target[0], *target[2]): + require_disjoint(output, protected) + for port in (args.port, args.port + 1): + with socket.socket() as listener: + listener.bind(("127.0.0.1", port)) + definition_path = Path(__file__).parents[1] / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json" + definitions = {s["id"]: s for s in read_json(definition_path)["stations"]} + endurance.require(sum(bool(definitions[s].get("exclusive")) for s in endurance.ACTIVE) == 1, + "Exclusive stations interfere") + helpers = [Path(endurance.__file__), Path(__file__), Path(semantic_machine_audit.__file__), + Path(native117_commands.__file__), definition_path] + before_helpers = {str(p): digest(p) for p in helpers} + output.mkdir(parents=True, exist_ok=False) + for path in helpers: + destination = output / "protocol-sources" / path.name + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, destination) + report_path = output / "native117-endurance-results.json" + report = dict(status="RUNNING", duration_seconds=args.seconds, interval_seconds=60, + feeding_protocol="IInventory replaceitem and fluid handlers; no inventory NBT writes", + source=str(args.source.resolve()), source_state_gates=history, + source_fingerprint=source_before, helper_sha256=before_helpers, + source_world_info=world_info(args.source / WORLD / "level.dat"), + fresh_fixture_policy=dict(seed=FRESH_FIXTURE_SEED, generate_structures=False, + scope="NEW fresh117 world only; imported world settings preserved"), + target_manifest=str(args.target_manifest.resolve()), target_manifest_sha256=digest(args.target_manifest), + artifacts=[dict(file=j.name, sha256=digest(j)) for j in target[2]], + active_stations=endurance.ACTIVE, checked_stations=endurance.CHECKED, profiles={}, + coverage="Rebuilt Works in fresh and migrated-copy worlds; not endurance of original source machinery", + manual_checks="Machine GUIs, weapons, sound and player interaction remain manual") + write_json(report_path, report) + rigs = [] + try: + for index, name in enumerate(NAMES): + runtime = output / name + launcher = prepare(runtime, target, [], args.port + index, args.source if index else None) + configuration = configure_new_fresh_fixture(runtime) if index == 0 else None + for marker in ("migration-operate.allow", "advantage-works.allow"): + (runtime / marker).touch() + rig = NativeEnduranceRig(name, runtime, args.java8, launcher, definitions) + rigs.append(rig) + if index: + rig.boot("frozen-import") + rig.server.snapshot("frozen-import") + rig.command("save-all flush") + rig.shutdown() + preserved = compare_upgraded(args.source / "migration-restarted.json", + runtime / "migration-frozen-import.json") + write_json(runtime / "native117-import-comparison.json", preserved) + endurance.require(preserved["status"] == "PASS", "Copied source did not preserve frozen state") + require_native_boot(runtime / "console-frozen-import.log") + rig.boot("endurance") + if index == 0: + report["fresh_fixture_proof"] = prove_fresh_fixture_before_build(rig, configuration) + else: + report["copied_world_info_before_build"] = world_info(runtime / WORLD / "level.dat") + endurance.require(report["copied_world_info_before_build"] == report["source_world_info"], + "Imported world generation settings changed") + write_json(report_path, report) + rig.start(build=True) + initial_ticks = {r.name: r.documents["W-01"]["worldTime"] for r in rigs} + started = time.monotonic() + report["started_at_utc"] = datetime.now(timezone.utc).isoformat() + write_json(report_path, report) + print(f"Measured {args.seconds}-second protocol started: " + report["started_at_utc"], flush=True) + 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: + rig.checkpoint(f"cycle-{cycle:03}") + rig.batch(f"cycle-{cycle:03}") + 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(report_path, report) + print(f"{cycle * 60}s: both native profiles passed batch {cycle}", flush=True) + elapsed = time.monotonic() - started + ending_ticks = {r.name: r.documents["W-01"]["worldTime"] for r in rigs} + for rig in rigs: + endurance.require(ending_ticks[rig.name] - initial_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") + 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") + 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, total_verified_checkpoints=len(rig.ledger), + station_statuses=statuses, restart="PASS", resumed_processing="PASS", + game_ticks=ending_ticks[rig.name] - initial_ticks[rig.name], + endurance_outputs=dict(totals)) + report["status"] = "PASS" if args.seconds >= 1800 else "SMOKE_PASS_NOT_ENDURANCE" + except BaseException as error: + report.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + raise + finally: + for rig in rigs: + try: + rig.shutdown() + verify_staged(rig.runtime, target) + for log in rig.runtime.glob("console-*.log"): + require_native_boot(log) + except Exception as error: + report.update(status="FAIL", shutdown_or_audit_error=str(error)) + report["source_fixture_unchanged"] = fingerprint(args.source.resolve()) == source_before + report["helpers_unchanged"] = {str(p): digest(p) for p in helpers} == before_helpers + if not report["source_fixture_unchanged"] or not report["helpers_unchanged"]: + report.update(status="FAIL", provenance_error="Protected source or protocol changed") + report["completed_at_utc"] = datetime.now(timezone.utc).isoformat() + write_json(report_path, report) + endurance.require(report["status"] != "FAIL", "See saved native endurance failure") + write_json(output / "qualification-audit.json", audit(output, require_endurance=args.seconds >= 1800)) + print(report["status"] + ": " + str(report_path), flush=True) + return 0 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--source-manifest", type=Path, required=True) + parser.add_argument("--source-key", default="target113") + parser.add_argument("--source-hashes", type=Path, required=True) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--seconds", type=int, default=1800) + parser.add_argument("--port", type=int, default=25590) + return run(parser.parse_args()) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_entity_storage.py b/test-rig/migration/native117_entity_storage.py new file mode 100644 index 00000000..7ad8a690 --- /dev/null +++ b/test-rig/migration/native117_entity_storage.py @@ -0,0 +1,290 @@ +"""Read-only corroboration of stopped 1.17.1 entity-region saves. + +Call audit(record, dimension_directory, NEW_evidence_directory) only after save, +flush and process shutdown. dimension_directory is WORLD, WORLD/DIM-1 or +WORLD/DIM1, not its region/entities child. Player files are outside this API. +The receipt proves exact saved witness NBT, not ticking or migration correctness. + +Source: Forge 37.1.1 injected-sources.jar, +net/minecraft/world/level/chunk/storage/EntityStorage.java, SHA-256 +1D597C2B42ADBE5C221B4F214386C42FD7C7C77A6BC3CC38F4ECA8B6D26E65C2. +Its storeEntities writes DataVersion, Entities and Position; Position is int[2]. +""" + +import base64 +import hashlib +import io +import json +import math +from pathlib import Path +import re +import uuid +import zlib + +import nbtlib + +from matrix import plain +from native117_format import actor_nbt, dimension_id, uuid_longs + +DATA_VERSION = 2730 +MAX_NBT_BYTES = 16 * 1024 * 1024 +ENTITY_ID = re.compile(r"[a-z0-9_.-]+:[a-z0-9_./-]+") +REGION_NAME = re.compile(r"r\.(-?\d+)\.(-?\d+)\.mca") + + +def _sha(data): + return hashlib.sha256(data).hexdigest().upper() + + +def _decompress(data, compression): + if compression == 3: + result = data + elif compression in (1, 2): + decoder = zlib.decompressobj(31 if compression == 1 else 15) + result = decoder.decompress(data, MAX_NBT_BYTES + 1) + if not decoder.eof or decoder.unused_data or decoder.unconsumed_tail: + raise ValueError("Truncated, trailing or excessive compressed entity NBT") + else: + raise ValueError("Unsupported entity compression (external .mcc is not supported)") + if len(result) > MAX_NBT_BYTES: + raise ValueError("Entity NBT exceeds the explicit 16 MiB audit limit") + return result + + +def _nbt(data): + stream = io.BytesIO(data) + result = nbtlib.File.parse(stream) + if stream.read(1) or result.root_name: + raise ValueError("Trailing bytes or named root in native entity NBT") + encoded = io.BytesIO() + result.write(encoded) + # A duplicate compound key must not disappear through a dictionary parser. + if encoded.getvalue() != data: + raise ValueError("Entity NBT cannot round-trip exactly (duplicate keys or unsupported encoding)") + return result + + +def _types(value): + if isinstance(value, nbtlib.Compound): + result = {key: _types(tag) for key, tag in value.items()} + if "UUID" in value: + result.pop("UUID") + result.update(UUIDMost=nbtlib.Long.tag_id, UUIDLeast=nbtlib.Long.tag_id) + if "Owner" in value and isinstance(value["Owner"], (nbtlib.List, nbtlib.IntArray)): + result.pop("Owner") + result["OwnerUUID"] = nbtlib.String.tag_id + for key in ("Dimension", "SpawnDimension"): + if key in result: + result[key] = "finite-dimension-identity" + return result + if isinstance(value, nbtlib.List): + return [value.tag_id, value.subtype.tag_id, [_types(entry) for entry in value]] + return value.tag_id + + +def _semantic(value): + # No actors.semantic/project_entity/normalized: those are expected-field projections. + # This preserves every value and tag type except actor_nbt's finite encodings. + return actor_nbt(plain(value.unpack())), _types(value) + + +def _identity(entity): + if (not isinstance(entity, nbtlib.Compound) or not isinstance(entity.get("id"), nbtlib.String) + or not ENTITY_ID.fullmatch(str(entity["id"]))): + raise ValueError("Saved entity needs a namespaced id") + words = entity.get("UUID") + if not isinstance(words, nbtlib.IntArray) or len(words) != 4 or any(key in entity for key in ("UUIDMost", "UUIDLeast")): + raise ValueError("Saved entity UUID must be exactly a native int[4]") + most, least = uuid_longs([int(word) for word in words]) + return str(uuid.UUID(int=((most & ((1 << 64) - 1)) << 64) | (least & ((1 << 64) - 1)))) + + +def _chunk(entity): + pos = entity.get("Pos") + if (not isinstance(pos, nbtlib.List) or pos.subtype is not nbtlib.Double or len(pos) != 3 + or any(not math.isfinite(float(value)) for value in pos)): + raise ValueError("Saved entity Pos must contain three finite native doubles") + result = (math.floor(float(pos[0]) / 16), math.floor(float(pos[2]) / 16)) + if any(not -(1 << 31) <= coordinate < (1 << 31) for coordinate in result): + raise ValueError("Saved entity chunk coordinate is outside signed int range") + return result + + +def _tree(entity, parent=None, seen=None): + seen = set() if seen is None else seen + identity = _identity(entity) + if identity in seen: + raise AssertionError("Duplicate UUID in saved passenger tree: " + identity) + seen.add(identity) + _chunk(entity) + yield identity, entity, parent + if "Passengers" in entity: + passengers = entity["Passengers"] + if not isinstance(passengers, nbtlib.List) or passengers.subtype not in (nbtlib.Compound, nbtlib.End): + raise ValueError("Passengers must be a native compound list") + for passenger in passengers: + yield from _tree(passenger, identity, seen) + + +def _payloads(data, rx, rz): + # Forge 37.1.1 RegionFile opens read probes with CREATE. An EOF header read + # is valid, and close's ceil(size/4096) padding leaves size zero unchanged. + # This is not a truncated header: any NONEMPTY short region still fails. + # RegionFile.java source SHA-256: 4C1A925B2868584E3F94198B146C7BE59D0000CAA88210E2381AF2FA9802D72B. + if not data: + return + if len(data) < 8192 or len(data) % 4096: + raise ValueError("Malformed entity region length") + occupied = {0, 1} + for slot in range(1024): + location = int.from_bytes(data[slot * 4:slot * 4 + 4], "big") + offset, count = location >> 8, location & 255 + if not offset and not count: + continue + if offset < 2 or not count or offset + count > len(data) // 4096: + raise ValueError("Invalid entity region sector allocation") + sectors = set(range(offset, offset + count)) + if occupied & sectors: + raise ValueError("Overlapping entity region sector allocation") + occupied.update(sectors) + start = offset * 4096 + length = int.from_bytes(data[start:start + 4], "big") + if length < 2 or length + 4 > count * 4096: + raise ValueError("Malformed entity chunk payload length") + yield (rx * 32 + (slot & 31), rz * 32 + (slot >> 5)), data[start:start + 4 + length] + + +def _entity_chunk(payload, coordinates): + 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") + 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") + entities = root["Entities"] + if not isinstance(entities, nbtlib.List) or entities.subtype not in (nbtlib.Compound, nbtlib.End): + raise ValueError("EntityStorage Entities must be a native compound list") + return entities + + +def _snapshots(record): + if not isinstance(record, dict) or not isinstance(record.get("entities"), list) or not record["entities"]: + raise ValueError("A nonempty actual native entity snapshot record is required") + expected, rows, metadata, children = {}, {}, {}, set() + for entry in record["entities"]: + if not isinstance(entry, dict) or not isinstance(entry.get("nbtBase64"), str): + raise ValueError("Snapshot needs actual nbtBase64, not a semantic projection") + entity = _nbt(_decompress(base64.b64decode(entry["nbtBase64"], validate=True), 1)) + identity = _identity(entity) + if identity in rows: + raise AssertionError("Duplicate snapshot UUID record: " + identity) + if "uuid" in entry and entry["uuid"] != identity: + raise AssertionError("Snapshot UUID attestation differs from actual NBT") + rows[identity] = entity + metadata[identity] = entry + for key, tag, parent in _tree(entity): + semantic = _semantic(tag) + if key in expected and expected[key] != semantic: + raise AssertionError("Inconsistent overlapping snapshot passenger NBT: " + key) + expected[key] = semantic + if parent is not None: + children.add(key) + roots = {key: tag for key, tag in rows.items() if key not in children} + if not roots: + raise ValueError("Snapshot has no serialized entity roots") + for identity, entry in metadata.items(): + if identity in roots and entry.get("vehicleUuid") is not None: + raise ValueError("Snapshot omits a passenger's serialized vehicle root") + return expected, rows, roots + + +def audit(snapshot_record, world_path, evidence_folder): + """Retain stopped raw regions and require complete NBT equality for every UUID. + + world_path is the explicit dimension directory. All its entity regions are + scanned, so duplicates in another region cannot hide outside witness chunks. + Unrelated entities are counted, not claimed as snapshot witnesses. External + .mcc and oversized/decompression-bomb payloads fail closed rather than skip. + Returns a JSON-safe receipt; malformed or mismatched data raises and retains + a FAIL receipt. Existing evidence is never overwritten. No source is written. + """ + world = Path(world_path).resolve(strict=True) + evidence = Path(evidence_folder).resolve() + if not world.is_dir() or evidence.is_relative_to(world) or world.is_relative_to(evidence): + raise ValueError("Entity evidence must be disjoint from the source dimension") + evidence.mkdir(parents=True, exist_ok=False) + receipt = dict(format=1, status="FAIL", scope="EXACT_STOPPED_ENTITY_NBT_ONLY", world=str(world), + callerMustStopServer=True, files={}, sourceFiles={}, chunks={}, matched={}) + + def retain(relative, data): + path = evidence / relative + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("xb") as output: + output.write(data) + receipt["files"][relative] = _sha(data) + + try: + expected, rows, roots = _snapshots(snapshot_record) + retain("snapshot.json", (json.dumps(snapshot_record, sort_keys=True, indent=2) + "\n").encode("utf-8")) + dimension = -1 if world.name == "DIM-1" else 1 if world.name == "DIM1" else 0 + for key in ("dimension", "worldDimensionKey"): + if key in snapshot_record and dimension_id(snapshot_record[key]) != dimension: + raise AssertionError("Snapshot dimension disagrees with the explicit dimension directory") + source = world / "entities" + if source.is_symlink() or not source.is_dir(): + raise ValueError("Missing regular native entities directory") + paths = sorted(source.glob("*.mca")) + if not paths: + raise ValueError("No native entity regions found") + saved, root_chunks = {}, {} + for path in paths: + match = REGION_NAME.fullmatch(path.name) + if path.is_symlink() or not path.is_file() or match is None: + raise ValueError("Unsafe native entity region path") + rx, rz = map(int, match.groups()) + if path.name != "r.%d.%d.mca" % (rx, rz) or any(not -(1 << 26) <= value < (1 << 26) for value in (rx, rz)): + raise ValueError("Noncanonical native entity region coordinates") + data = path.read_bytes() + receipt["sourceFiles"][path.name] = _sha(data) + retain("regions/" + path.name, data) + for coordinates, payload in _payloads(data, rx, rz): + 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): + root_id = _identity(root) + if _chunk(root) != coordinates: + raise AssertionError("Root entity saved Pos differs from its EntityStorage chunk") + root_chunks[root_id] = coordinates + for identity, tag, parent in _tree(root): + if identity in saved: + raise AssertionError("Duplicate UUID in raw entity storage: " + identity) + if "Dimension" in tag and dimension_id(plain(tag["Dimension"].unpack())) != dimension: + raise AssertionError("Raw entity NBT dimension differs from source directory") + saved[identity] = dict(nbt=_semantic(tag), root=root_id, chunk=coordinates) + for identity, entity in roots.items(): + coordinates = _chunk(entity) + if identity not in root_chunks or root_chunks[identity] != coordinates: + raise AssertionError("Snapshot root UUID is absent from the chunk of its actual saved Pos: " + identity) + for identity, semantic in expected.items(): + if identity not in saved or saved[identity]["nbt"] != semantic: + raise AssertionError("Complete saved entity NBT differs from actual snapshot: " + identity) + entry = saved[identity] + receipt["matched"][identity] = dict(rootUuid=entry["root"], chunk=list(entry["chunk"]), + payload="chunks/%d.%d.chunk" % entry["chunk"], separatelyObserved=identity in rows) + if sorted(source.glob("*.mca")) != paths or any(_sha(path.read_bytes()) != receipt["sourceFiles"][path.name] for path in paths): + raise AssertionError("Source entity storage changed during the stopped-world audit") + receipt.update(status="PASS", snapshotRecords=len(rows), matchedEntities=len(expected), rawEntities=len(saved), + unrelatedEntities=len(set(saved) - set(expected)), dimension=dimension) + except Exception as error: + receipt["error"] = type(error).__name__ + ": " + str(error) + raise + finally: + with (evidence / "receipt.json").open("x", encoding="utf-8") as output: + json.dump(receipt, output, sort_keys=True, indent=2) + output.write("\n") + return receipt diff --git a/test-rig/migration/native117_entity_storage_replay.py b/test-rig/migration/native117_entity_storage_replay.py new file mode 100644 index 00000000..8fe20108 --- /dev/null +++ b/test-rig/migration/native117_entity_storage_replay.py @@ -0,0 +1,241 @@ +"""Read-only replay of retained audit_stopped evidence, without the source world. + +Every allocated region payload is reparsed with native117_entity_storage. PASS +and matching hashes alone cannot replace complete NBT/type/UUID comparisons. +Optional external snapshot/receipt pins bind provenance: a self-contained archive +cannot detect a coherent replacement of every input and every claimed checksum. +This module never writes files, stages a world, calls audit(), or starts a server. +""" + +import argparse +import hashlib +import json +from pathlib import Path, PurePosixPath, PureWindowsPath +import re +import uuid + +from matrix import plain +import native117_entity_storage as native +from native117_format import dimension_id +from native117_stopped_entities import DIMENSIONS, SNAPSHOT + + +def _sha(data): + return hashlib.sha256(data).hexdigest().upper() + + +def _json(data): + def unique(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError("Duplicate retained JSON key: " + key) + result[key] = value + return result + + def invalid(value): + raise ValueError("Nonfinite retained JSON number: " + value) + + return json.loads(data, object_pairs_hook=unique, parse_constant=invalid) + + +def _same(expected, actual, label): + # Python equality would accept forged boolean counters such as True == 1. + def encoded(value): + return json.dumps(value, sort_keys=True, allow_nan=False, separators=(",", ":")) + if encoded(expected) != encoded(actual): + raise AssertionError("Retained metadata differs from rederived evidence: " + label) + + +def _tree(root): + files, directories, pending = {}, set(), [root] + while pending: + for path in sorted(pending.pop().iterdir()): + if (path.is_symlink() or getattr(path, "is_junction", lambda: False)() + or not path.resolve().is_relative_to(root)): + raise ValueError("Retained evidence contains an alias outside its regular tree") + name = path.relative_to(root).as_posix() + if path.is_dir(): + directories.add(name) + pending.append(path) + elif path.is_file(): + files[name] = path.read_bytes() + else: + raise ValueError("Retained evidence contains a non-regular file") + return files, directories + + +def _pin(data, expected, label): + if expected is not None and (not isinstance(expected, str) or not re.fullmatch(r"[A-Fa-f0-9]{64}", expected) + or _sha(data) != expected.upper()): + raise AssertionError("External frozen evidence pin differs: " + label) + + +def _world(value): + if not isinstance(value, str): + raise ValueError("Missing retained world directory") + path = PureWindowsPath(value) if PureWindowsPath(value).drive else PurePosixPath(value) + if not path.is_absolute() or ".." in path.parts or path.name != "AdvantageWorksMigration": + raise ValueError("Noncanonical retained world directory") + return path + + +def _dimension(files, prefix, record, dimension, world): + receipt_name = prefix + "/receipt.json" + stored = _json(files[receipt_name]) + snapshot_name = prefix + "/snapshot.json" + _same(record, _json(files[snapshot_name]), snapshot_name) + expected, rows, roots = native._snapshots(record) + if any(entry.get("id") != str(rows[entry["uuid"]]["id"]) for entry in record["entities"]): + raise AssertionError("Retained type attestation differs from actual native NBT") + source_files, payloads, retained = {}, {}, {snapshot_name} + saved, root_chunks = {}, {} + region_names = sorted(name for name in files if name.startswith(prefix + "/regions/")) + if not region_names: + raise ValueError("No retained native regions for nonempty dimension") + for name in region_names: + basename = name.removeprefix(prefix + "/regions/") + match = native.REGION_NAME.fullmatch(basename) + if match is None: + raise ValueError("Unsafe retained region filename") + rx, rz = map(int, match.groups()) + if basename != "r.%d.%d.mca" % (rx, rz) or any(not -(1 << 26) <= value < (1 << 26) for value in (rx, rz)): + raise ValueError("Noncanonical retained region coordinates") + source_files[basename] = _sha(files[name]) + retained.add(name) + for coordinates, payload in native._payloads(files[name], rx, rz): + relative = "chunks/%d.%d.chunk" % coordinates + retained_name = prefix + "/" + relative + if retained_name not in files: + raise AssertionError("Missing retained payload recovered from actual region: " + retained_name) + if files[retained_name] != payload: + raise AssertionError("Retained payload differs byte-for-byte from its region allocation") + retained.add(retained_name) + payloads[relative] = dict(position=list(coordinates), sha256=_sha(payload), source=basename) + for entity in native._entity_chunk(payload, coordinates): + root_id = native._identity(entity) + if native._chunk(entity) != coordinates: + raise AssertionError("Saved root Pos differs from retained entity chunk") + root_chunks[root_id] = coordinates + for identity, tag, parent in native._tree(entity): + if identity in saved: + raise AssertionError("Duplicate UUID in retained entity storage: " + identity) + if "Dimension" in tag and dimension_id(plain(tag["Dimension"].unpack())) != dimension: + raise AssertionError("Retained NBT dimension differs from observed dimension") + saved[identity] = dict(nbt=native._semantic(tag), root=root_id, chunk=coordinates) + for identity, entity in roots.items(): + if identity not in root_chunks or root_chunks[identity] != native._chunk(entity): + raise AssertionError("Retained snapshot root is absent from its actual saved Pos chunk") + matches = {} + for identity, semantic in expected.items(): + if identity not in saved or saved[identity]["nbt"] != semantic: + raise AssertionError("Complete retained entity NBT/type differs from stopping snapshot: " + identity) + entry = saved[identity] + matches[identity] = dict(rootUuid=entry["root"], chunk=list(entry["chunk"]), + payload="chunks/%d.%d.chunk" % entry["chunk"], separatelyObserved=identity in rows) + derived = dict(format=1, status="PASS", scope="EXACT_STOPPED_ENTITY_NBT_ONLY", world=str(world), + callerMustStopServer=True, + files={name.removeprefix(prefix + "/"): _sha(files[name]) for name in sorted(retained)}, + sourceFiles=source_files, chunks=payloads, matched=matches, snapshotRecords=len(rows), + matchedEntities=len(expected), rawEntities=len(saved), unrelatedEntities=len(set(saved) - set(expected)), + dimension=dimension) + _same(derived, stored, receipt_name) + return derived, retained | {receipt_name} + + +def replay(evidence_folder, *, expected_snapshot_sha256=None, expected_receipt_sha256=None): + """Rederive complete saved-state proof from a retained audit_stopped folder. + + Returns a JSON-safe result and hashes EVERY retained file, including receipts. + Raises on malformed, missing, extra, changed or semantically inconsistent data. + Nothing is written and the original runtime/world need not exist. All-empty + evidence remains NO_NONEMPTY_DIMENSIONS, never upgraded to an entity PASS. + """ + given = Path(evidence_folder) + if given.is_symlink() or getattr(given, "is_junction", lambda: False)(): + raise ValueError("Replay root must not be an alias") + root = given.resolve(strict=True) + files, directories = _tree(root) + if "receipt.json" not in files or SNAPSHOT not in files: + raise ValueError("Missing retained stopping snapshot or receipt") + _pin(files[SNAPSHOT], expected_snapshot_sha256, SNAPSHOT) + _pin(files["receipt.json"], expected_receipt_sha256, "receipt.json") + stored = _json(files["receipt.json"]) + document = _json(files[SNAPSHOT]) + 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("enumeration") != "ServerLevel.getAllEntities"): + raise ValueError("Not a complete retained native stopping-event snapshot") + if not isinstance(document.get("bootId"), str) or str(uuid.UUID(document["bootId"])) != document["bootId"]: + raise ValueError("Malformed retained boot UUID") + world = _world(document.get("worldDirectory")) + records = document.get("dimensions") + if not isinstance(records, dict) or set(records) != set(DIMENSIONS): + raise ValueError("Retained snapshot does not cover all three dimensions") + helper_lifecycle = None + if document['format'] == 2: + from native117_helper_contract import CONTRACT, LIFECYCLE, validate, raw_helpers_absent + helper_lifecycle = validate(document, files[CONTRACT], _json(files[LIFECYCLE]) if LIFECYCLE in files else None) + raw_helpers_absent(files, helper_lifecycle['helpers']) + dimensions, matched, count, audited = {}, set(), 0, 0 + retained = {"receipt.json", SNAPSHOT} + if document['format'] == 2: + retained.add(CONTRACT) + if LIFECYCLE in files: + retained.add(LIFECYCLE) + for key, (legacy, relative) in DIMENSIONS.items(): + record = records[key] + if (not isinstance(record, dict) or record.get("worldDimensionKey") != key + or type(record.get("dimension")) is not int or record["dimension"] != legacy + or type(record.get("gameTime")) is not int or not isinstance(record.get("entities"), list) + or type(record.get("entityCount")) is not int or record["entityCount"] != len(record["entities"])): + raise ValueError("Malformed retained dimension record: " + key) + count += len(record["entities"]) + for entry in record["entities"]: + if (not isinstance(entry, dict) or entry.get("worldDimensionKey") != key + or entry.get("id") == "minecraft:player" or not isinstance(entry.get("uuid"), str)): + raise ValueError("Invalid retained non-player dimension observation") + if not record["entities"]: + dimensions[key] = dict(status="SKIP_EMPTY_STOP_SNAPSHOT", entityCount=0) + continue + derived, paths = _dimension(files, relative or "overworld", record, legacy, world / relative) + identities = set(derived["matched"]) + if matched & identities: + raise AssertionError("Duplicate retained entity UUID across dimensions") + matched.update(identities) + dimensions[key] = derived + retained.update(paths) + audited += 1 + if type(document.get("entityCount")) is not int or document["entityCount"] != count: + raise AssertionError("Retained snapshot total entity count differs from actual records") + derived = dict(format=1, status="PASS" if audited else "NO_NONEMPTY_DIMENSIONS", + scope="STOP_BOUNDARY_SAVED_ENTITY_NBT_ONLY", dimensions=dimensions, + snapshotSha256=_sha(files[SNAPSHOT]), bootId=document["bootId"], + auditedDimensions=audited, snapshotRecords=count, matchedEntities=len(matched)) + if document['format'] == 2: + derived.update(format=2, helperLifecycle=helper_lifecycle) + _same(derived, stored, "root receipt") + if set(files) != retained: + raise AssertionError("Unexpected or missing retained files") + expected_dirs = {parent.as_posix() for name in retained for parent in PurePosixPath(name).parents if parent != PurePosixPath(".")} + if directories != expected_dirs: + raise AssertionError("Unexpected or missing retained directories") + if _tree(root) != (files, directories): + raise AssertionError("Retained evidence changed during read-only replay") + return dict(status=derived["status"], scope="READ_ONLY_STOPPED_ENTITY_STORAGE_REPLAY", + snapshotSha256=derived["snapshotSha256"], receiptSha256=_sha(files["receipt.json"]), + files={name: _sha(data) for name, data in sorted(files.items())}, bootId=document["bootId"], + auditedDimensions=audited, snapshotRecords=count, matchedEntities=len(matched), dimensions=dimensions, + externallyPinnedSnapshot=expected_snapshot_sha256 is not None, externallyPinnedReceipt=expected_receipt_sha256 is not None) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--evidence", type=Path, required=True) + parser.add_argument("--snapshot-sha256") + parser.add_argument("--receipt-sha256") + args = parser.parse_args() + print(json.dumps(replay(args.evidence, expected_snapshot_sha256=args.snapshot_sha256, + expected_receipt_sha256=args.receipt_sha256), sort_keys=True, indent=2)) diff --git a/test-rig/migration/native117_extended.py b/test-rig/migration/native117_extended.py new file mode 100644 index 00000000..409ac019 --- /dev/null +++ b/test-rig/migration/native117_extended.py @@ -0,0 +1,83 @@ +"""Four native moves, partial networks, blocked output, restart and redstone tests.""" + +import time + +import extended_lifecycle as historical +from native117_identity import fixture_ids +from native117_partial import NativeExtendedRun, campaign, common_parser, orders + + +def exercise(runner, progress): + definition = runner.definition + runner.boot("extended") + runner.build() + runner.probe("trace-loads", "trace-enabled") + result = dict(status="RUNNING") + low = runner.probe("low-prime", "low-before") + runner.release_drills(0) + time.sleep(2) + stalled = runner.probe("snapshot", "low-stalled") + result["insufficient_energy"] = historical.reserve_results(definition, low, stalled) + runner.hold_drills(0) + for step in range(4): + runner.probe("prime", f"prime-step-{step + 1}") + runner.release_drills(step) + moved = runner.wait_step(step + 1) + print(f"{runner.runtime.name}: native drill step {step + 1}/4 observed", flush=True) + result["moves"] = historical.movement_results(definition, moved) + progress(result) + time.sleep(50) + runner.processing("network-commissioned", 1000) + result["partial_networks"] = [] + for cycle, group in enumerate(("machine", "supply", "supply", "machine"), 1): + result["partial_networks"].append(runner.partial(group, cycle)) + runner.replenish() + runner.probe("offer", f"partial-fluid-{cycle}") + time.sleep(50) + runner.processing(f"partial-resumed-{cycle}", (cycle + 1) * 1000) + print(f"{runner.runtime.name}: {group}-only unload {cycle} and processing recorded", flush=True) + progress(result) + result["reload_orders"] = orders(runner, process=True) + runner.install_blocked_output() + time.sleep(45) + blocked = runner.probe("snapshot", "blocked-output") + result["blocked_output"] = historical.output_results(definition, blocked, True) + runner.command("save-all flush") + runner.close() + runner.boot("blocked-restart") + time.sleep(1) + retained = runner.probe("snapshot", "blocked-retained") + result["restart"] = historical.restart_results(definition, blocked, retained) + result["blocked_after_restart"] = historical.output_results(definition, retained, True) + runner.freed_output() + time.sleep(20) + result["resumed_output"] = historical.output_results(definition, runner.probe("snapshot", "freed-output"), False) + for f in definition: + if "route" in f: + runner.target_plane(f, "minecraft:obsidian") + time.sleep(4) + before = runner.probe("snapshot", "redstone-before") + runner.hold_drills(4) + time.sleep(2) + held = runner.probe("snapshot", "redstone-held") + runner.release_drills(4) + time.sleep(4) + after = runner.probe("snapshot", "redstone-resumed") + result["redstone"] = historical.redstone_results(definition, before, held, after) + runner.hold_drills(4) + runner.command("save-all flush") + runner.close() + result["network_processing"] = "PASS" + result["status"] = historical.qualification(result) + progress(result) + return result + + +def main(): + parser = common_parser(__doc__) + args = parser.parse_args() + return campaign(args, "extended", fixture_ids(historical.fixtures()), exercise, NativeExtendedRun) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_extended_reads.py b/test-rig/migration/native117_extended_reads.py new file mode 100644 index 00000000..8ef9216e --- /dev/null +++ b/test-rig/migration/native117_extended_reads.py @@ -0,0 +1,48 @@ +"""All eighteen extended drill routes through mining, reload and saved-read guards.""" + +from pathlib import Path +import shutil +import time + +import extended_lifecycle as historical +from matrix import digest +from native117_identity import fixture_ids +from native117_partial import campaign, common_parser + + +def exercise(runner, progress): + protocol = Path(__file__) + original_hash = digest(protocol) + archive = runner.runtime / "native117-extended-reads-protocol.py" + shutil.copy2(protocol, archive) + runner.boot("extended-reads") + runner.build() + result = dict(status="RUNNING", protocolSha256=original_hash) + for step in range(4): + runner.probe("prime", f"prime-step-{step + 1}") + runner.release_drills(step) + moved = runner.wait_step(step + 1) + result["moves"] = historical.movement_results(runner.definition, moved) + runner.mine() + time.sleep(45) + result["mining"] = runner.mining_results(runner.probe("snapshot", "reads-mining-complete")) + progress(result) + print(runner.runtime.name + ": four moves and normal mining observed", flush=True) + result["mid_block_progress"] = runner.midblock() + progress(result) + result["saved_read_guards"] = runner.save_read_guards() + runner.command("save-all flush") + runner.close() + groups = [result["moves"], result["mining"], *result["mid_block_progress"].values(), + *result["saved_read_guards"].values()] + result["status"] = "PASS" if all(c["status"] == "PASS" for group in groups for c in group.values()) else "FAIL" + result["own_protocol_unchanged"] = digest(protocol) == original_hash == digest(archive) + if not result["own_protocol_unchanged"]: + result["status"] = "FAIL" + progress(result) + return result + + +if __name__ == "__main__": + args = common_parser(__doc__).parse_args() + raise SystemExit(campaign(args, "extended-reads", fixture_ids(historical.fixtures()), exercise)) diff --git a/test-rig/migration/native117_format.py b/test-rig/migration/native117_format.py new file mode 100644 index 00000000..4bccf6ad --- /dev/null +++ b/test-rig/migration/native117_format.py @@ -0,0 +1,106 @@ +"""Strict 1.16 serialization boundaries; never rewrites a world or weakens an oracle.""" + +import copy + + +DIMENSIONS = {-1: "minecraft:the_nether", 0: "minecraft:overworld", 1: "minecraft:the_end"} + + +def dimension_id(value): + if isinstance(value, bool): + raise ValueError("Boolean is not a dimension") + if isinstance(value, int) and value in DIMENSIONS: + return value + for identity, key in DIMENSIONS.items(): + if value == key: + return identity + raise ValueError("Unsupported fixture dimension: " + repr(value)) + + +def signed(value, bits): + value &= (1 << bits) - 1 + return value - (1 << bits) if value & (1 << (bits - 1)) else value + + +def uuid_longs(words): + if not isinstance(words, (list, tuple)) or len(words) != 4: + raise ValueError("Native UUID must contain exactly four signed ints") + if any(isinstance(word, bool) or not isinstance(word, int) or not -(1 << 31) <= word < (1 << 31) for word in words): + raise ValueError("Malformed native UUID int array") + return (signed((words[0] & 0xffffffff) << 32 | words[1] & 0xffffffff, 64), + signed((words[2] & 0xffffffff) << 32 | words[3] & 0xffffffff, 64)) + + +def actor_nbt(value): + """Expose exact UUID/dimension identity in the historical oracle's representation.""" + if isinstance(value, list): + return [actor_nbt(entry) for entry in value] + if not isinstance(value, dict): + return value + result = {key: actor_nbt(entry) for key, entry in value.items()} + if "UUID" in result: + most, least = uuid_longs(result.pop("UUID")) + if "UUIDMost" in result or "UUIDLeast" in result: + raise ValueError("Ambiguous duplicate UUID encodings") + result.update(UUIDMost=most, UUIDLeast=least) + if "Owner" in result and isinstance(result["Owner"], list): + import uuid + most, least = uuid_longs(result.pop("Owner")) + if "OwnerUUID" in result: + raise ValueError("Ambiguous duplicate owner encodings") + result["OwnerUUID"] = str(uuid.UUID(int=((most & ((1 << 64) - 1)) << 64) | (least & ((1 << 64) - 1)))) + for key in ("Dimension", "SpawnDimension"): + if key in result: + result[key] = dimension_id(result[key]) + return result + + +def worldgen_settings(data): + settings = data.get("WorldGenSettings") + if not isinstance(settings, dict): + raise ValueError("Native117 level.dat is missing WorldGenSettings") + for key in ("seed", "generate_features", "bonus_chest", "dimensions"): + if key not in settings: + raise ValueError("Missing native WorldGenSettings field: " + key) + dimensions = settings["dimensions"] + if not isinstance(dimensions, dict) or not set(DIMENSIONS.values()).issubset(dimensions): + raise ValueError("Missing native dimension registry entries") + for entry in dimensions.values(): + if not isinstance(entry, dict) or "type" not in entry or not isinstance(entry.get("generator"), dict): + raise ValueError("Malformed native dimension generator") + return copy.deepcopy(settings) + + +def world_info(data, allow_legacy=False): + if "WorldGenSettings" not in data: + if not allow_legacy: + raise ValueError("Legacy generation fields cannot prove a native117 save") + return dict(MapFeatures=int(data["MapFeatures"]), RandomSeed=int(data["RandomSeed"]), + generatorName=str(data["generatorName"])) + settings = worldgen_settings(data) + generator = settings["dimensions"][DIMENSIONS[0]]["generator"] + kind = generator.get("type") + if kind not in ("minecraft:flat", "minecraft:noise", "minecraft:debug"): + raise ValueError("Unknown native generator type: " + repr(kind)) + return dict(MapFeatures=int(settings["generate_features"]), RandomSeed=int(settings["seed"]), + generatorName={"minecraft:flat": "flat", "minecraft:noise": "default", "minecraft:debug": "debug_all_block_states"}[kind]) + + +def decode_palette(palette, words, count=4096): + """1.16 pads each long; entries never straddle a 64-bit boundary.""" + size = len(palette) + if not 1 <= size <= count or count <= 0: + raise ValueError("Invalid chunk palette size") + if size == 1 and not words: + return [palette[0]] * count + bits = max(4, (size - 1).bit_length()) + per_long = 64 // bits + if len(words) != (count + per_long - 1) // per_long: + raise ValueError("Native117 BlockStates has the wrong padded length") + result = [] + for index in range(count): + identity = ((int(words[index // per_long]) & ((1 << 64) - 1)) >> ((index % per_long) * bits)) & ((1 << bits) - 1) + if identity >= size: + raise ValueError("BlockStates references an absent palette entry") + result.append(palette[identity]) + return result diff --git a/test-rig/migration/native117_geometry.py b/test-rig/migration/native117_geometry.py new file mode 100644 index 00000000..74924d6b --- /dev/null +++ b/test-rig/migration/native117_geometry.py @@ -0,0 +1,57 @@ +"""Forge 37 geometry contract; live agreement is required before qualification.""" + +import hashlib +from pathlib import Path +import zipfile + +FORCED_LEVEL = 31 +FULL_LEVEL = 33 +MAX_DISTANCE = 11 +LAST_HOLDER_LEVEL = FULL_LEVEL + MAX_DISTANCE +HOLDER_RADIUS = LAST_HOLDER_LEVEL - FORCED_LEVEL +MINECRAFT_VERSION = "1.17.1" +FORGE_VERSION = "37.1.1" +MCP_VERSION = "20210706.113038" +SOURCE_ARCHIVE_SHA256 = "ACB95ACEBB65CE2EE034E887F324F6CC18C7C628D0CF97358F4D2F64572AAA0B" +SOURCE_SHA256 = "5BB6FDF6E625CB86C5A7975F686DB89306729C0517D39350BFD88BD77C5EBBD4" +ENGINE_SOURCES = { + "net/minecraft/server/level/DistanceManager.java": SOURCE_SHA256, + "net/minecraft/server/level/TicketType.java": "E75C0250F0B78C5177E01FFCB19CBDB3B49F7F8B8842509DDA6AAB315C7017BF", + "net/minecraft/server/level/ChunkHolder.java": "C86B3183F14E85EA9DCD1A07D261A55A704B42C60D6AB3C8135928277E8892AB", + "net/minecraft/server/level/ChunkMap.java": "3795347D92F9DE3379218A2D08017ED6E72903FA55827A02CFC7E9A6E064C89F", + "net/minecraft/world/level/chunk/ChunkStatus.java": "E0316DF8268A3EA478E8162BCFF02511A6DAF81B373CA7C9CBCAC1B4F65586FD", + "net/minecraft/server/level/ServerChunkCache.java": "AC48BB72690AD00BF1ACEE880C000C2AE5C4DE12AE68F36B0BDF5844A942607E", +} + +# Raw Forge-patched injected-sources.jar entries, not re-labelled 1.16 sources. +# DistanceManager.m_140799_ (updateChunkForced) constructs TicketType.f_9445_ +# (FORCED) at 31. ChunkHolder.m_140074_ derives FULL status from level 33; +# m_140083_ indexes FullChunkStatus with clamp(33 - level + 1), so 31 ticks entities. +# ChunkStatus.m_62421_ (maxDistance) returns the size of its 11-entry f_62330_ list. +# ChunkMap.f_140127_ is 33 + that distance and creates holders through that level: +# 44 is the last holder, and 44 - 31 = 13 is the forced-ticket generation halo. +def verify_engine_sources(archive): + """Explicit local source-proof audit; live snapshots still require their own guard.""" + archive = Path(archive) + if hashlib.sha256(archive.read_bytes()).hexdigest().upper() != SOURCE_ARCHIVE_SHA256: + raise AssertionError("Native117 engine source archive differs from the verified Forge 37 bytes") + with zipfile.ZipFile(archive) as sources: + for name, expected in ENGINE_SOURCES.items(): + if hashlib.sha256(sources.read(name)).hexdigest().upper() != expected: + raise AssertionError("Native117 engine source entry differs: " + name) + return dict(minecraft=MINECRAFT_VERSION, forge=FORGE_VERSION, mcp=MCP_VERSION, + archivePath=str(archive.resolve()), archiveSha256=SOURCE_ARCHIVE_SHA256, + sources=dict(ENGINE_SOURCES)) + + +def require_geometry(snapshot, forced=False): + geometry = snapshot.get("ticketGeometry", {}) + expected = dict(fullLevel=FULL_LEVEL, entityTickingLevel=FORCED_LEVEL, + maxDistance=MAX_DISTANCE, lastHolderLevel=LAST_HOLDER_LEVEL, + holderRadius=HOLDER_RADIUS) + 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)) + 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)) + return geometry diff --git a/test-rig/migration/native117_harvest.py b/test-rig/migration/native117_harvest.py new file mode 100644 index 00000000..eecfa5b4 --- /dev/null +++ b/test-rig/migration/native117_harvest.py @@ -0,0 +1,160 @@ +"""Actual survival mining and silk-touch drops in two clean native117 boots.""" + +import argparse +from datetime import datetime, timezone +from pathlib import Path +import traceback + +from matrix import digest, write_json +from native117_code import Server +from native117 import output_path, pinned_profile, require_disjoint +from native117_lifecycle import prepare, read_json, require_native_boot, verify_staged +from native117_readiness import await_ready + + +EXPECTED = { + "sulfur/hand-fails": (0, False, False, 0, 0, 1), + "sulfur/axe-fails": (0, False, False, 0, 0, 1), + "sulfur/wood": (0, True, False, 1, 4, 8), + "sulfur/stone": (0, True, False, 1, 4, 8), + "sulfur/silk": (0, True, True, 1, 1, 1), + "lithium/hand-fails": (1, False, False, 0, 0, 1), + "lithium/wood-fails": (1, False, False, 0, 0, 1), + "lithium/gold-fails": (1, False, False, 0, 0, 1), + "lithium/axe-fails": (1, False, False, 0, 0, 1), + "lithium/stone": (1, True, False, 1, 1, 1), + "lithium/diamond": (1, True, False, 1, 1, 1), + "lithium/silk": (1, True, True, 1, 1, 1), + "frame/hand": (-1, True, False, 1, 1, 1), + "frame/silk": (-1, True, True, 1, 1, 1), +} +for _mask in range(64): + for _family in ("wire", "scaffold"): + EXPECTED[f"{_family}/{_mask}/hand"] = (-1, True, False, 1, 1, 1) + EXPECTED[f"{_family}/{_mask}/silk"] = (-1, True, True, 1, 1, 1) + +ISOLATION_PROTOCOL = "native117-live-drops-v1" +HARVEST_CHUNKS = (74, 63, 75, 64) + + +def require_checks(document, label): + rows = document.get("checks", []) + actual = {row["id"]: row for row in rows} + if (document.get("label") != label or document.get("minecraft") != "1.17.1" + or actual.keys() != EXPECTED.keys() | {"solar/native-solidity"} + or len(rows) != len(EXPECTED) + 1): + raise AssertionError("Incomplete native117 harvest witness set") + if document.get("status") != "PASS": + failures = [row["id"] + ": " + row.get("failure", row.get("status", "missing status")) + for row in rows if row.get("status") != "PASS"] + raise AssertionError("Native117 harvest witnesses failed: " + "; ".join(failures[:5])) + for name, (level, allowed, silk, minimum, maximum, trials) in EXPECTED.items(): + row = actual[name] + counts = row.get("actualDropCounts", []) + mask = int(name.split("/")[1]) if name.startswith(("wire/", "scaffold/")) else -1 + if (row.get("status") != "PASS" or row.get("historicalHarvestLevel") != level + or row.get("harvestAllowed") is not allowed or row.get("silkTouch") is not silk + or row.get("connectionMask") != mask + or row.get("actualSolid") is not name.startswith(("sulfur/", "lithium/", "wire/")) + or row.get("fullCollision") is not (not name.startswith("wire/")) + or row.get("trials") != trials or len(counts) != trials + or any(type(count) is not int or not minimum <= count <= maximum for count in counts)): + raise AssertionError("Historical mining/drop oracle failed: " + name) + if actual["solar/native-solidity"].get("status") != "PASS" or actual["solar/native-solidity"].get("actualSolid") is not True: + raise AssertionError("Historical photovoltaic solid=true property was lost") + + +def require_isolation(document): + if document.get("fixtureIsolation") != ISOLATION_PROTOCOL: + raise AssertionError("Missing native117 live-drop fixture isolation protocol") + seen_ids, seen_uuids = set(), set() + for row in document["checks"]: + if row["id"] == "solar/native-solidity": + continue + evidence = row.get("dropEvidence", []) + if len(evidence) != row["trials"]: + raise AssertionError("Incomplete per-trial drop isolation evidence: " + row["id"]) + for trial, entry in enumerate(evidence): + if (entry.get("trial") != trial or type(entry.get("liveAfterCleanup")) is not int + or entry["liveAfterCleanup"] != 0 + or any(type(entry.get(key)) is not int or entry[key] < 0 + for key in ("liveBeforeCleanup", "removedBeforeCleanup", "worldTime"))): + raise AssertionError("Harvest trial was not isolated: " + row["id"]) + drops = entry.get("actualDrops") + if not isinstance(drops, list): + raise AssertionError("Missing actual native drop identities: " + row["id"]) + count = 0 + for drop in drops: + entity_id, uuid = drop.get("entityId"), drop.get("uuid") + if (type(entity_id) is not int or entity_id in seen_ids or not isinstance(uuid, str) + or not uuid or uuid in seen_uuids or drop.get("removed") is not False + or not isinstance(drop.get("item"), str) or not drop["item"] + or type(drop.get("count")) is not int or drop["count"] <= 0): + raise AssertionError("Removed, repeated or invalid native harvest drop: " + row["id"]) + seen_ids.add(entity_id) + seen_uuids.add(uuid) + count += drop["count"] + if count != row["actualDropCounts"][trial]: + raise AssertionError("Native drop identities disagree with counted items: " + row["id"]) + + +def mark_runtime(output): + (output / "advantage-native-harvest.allow").touch() + (output / "advantage-works.allow").touch() + + +def run(args): + output = output_path(args.output) + require_disjoint(output, args.target_manifest) + target = pinned_profile(args.target_manifest.resolve(), "target117") + for protected in (target[0], *target[2]): + require_disjoint(output, protected) + launcher = prepare(output, target, [], args.port) + mark_runtime(output) + report = dict(status="RUNNING", started_at_utc=datetime.now(timezone.utc).isoformat(), phases={}, + fixture_isolation_protocol=ISOLATION_PROTOCOL, runner_sha256=digest(Path(__file__)), + target_manifest=str(args.target_manifest.resolve()), target_manifest_sha256=digest(args.target_manifest), + scope="Real survival PlayerInteractionManager harvesting, failed tools, exact legacy drop bounds, silk touch and full restart", + source_baseline="EA 1.10/1.12 sulfur pickaxe 0 quantity 1..4; lithium pickaxe 1 quantity 1; Power steel-frame normal harvesting") + server = None + try: + for label in ("exercise", "restored"): + server = Server(output, args.java8, launcher, label) + server.send("forceload add 1198 1022 1202 1026") + readiness = await_ready(server, output, HARVEST_CHUNKS, "harvest-" + label) + report.setdefault("readiness", {})[label] = readiness + server.send("advharvest " + label) + server.wait("ADVHARVEST COMPLETE " + label, 120) + document = read_json(output / "advantage-works-results" / f"native-harvest-{label}.json") + if (type(document.get("worldTime")) is not int + or document["worldTime"] < readiness["completedWorldTime"]): + raise AssertionError("Harvest preceded native chunk/entity readiness") + report["phases"][label] = document + server.send("save-all flush") + server.close() + server = None + require_native_boot(output / f"console-{label}.log") + require_checks(document, label) + require_isolation(document) + verify_staged(output, target) + report["status"] = "PASS" + except Exception as error: + report.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + finally: + if server: + try: + server.close() + except Exception as error: + report.update(status="FAIL", shutdown_error=str(error)) + report["completed_at_utc"] = datetime.now(timezone.utc).isoformat() + write_json(output / "native117-harvest-results.json", report) + return int(report["status"] != "PASS") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, default=25596) + raise SystemExit(run(parser.parse_args())) diff --git a/test-rig/migration/native117_helper_contract.py b/test-rig/migration/native117_helper_contract.py new file mode 100644 index 00000000..7e5f7487 --- /dev/null +++ b/test-rig/migration/native117_helper_contract.py @@ -0,0 +1,116 @@ +"""Strict format-2 partition and native no-save helper corroboration; no worlds are written.""" + +import base64 +import hashlib +import uuid +import nbtlib + +import native117_entity_storage as native + +CONTRACT = 'native117-helper-contract.json' +CONTRACT_SHA = '59B45FFB7D3CA1F726EFDE111D1716FB0583B092D4F7EAA1A2AE09FA71C7F73F' +POLICY = 'native117-leash-knot-nosave-v1' +CLASS = 'net.minecraft.world.entity.decoration.LeashFenceKnotEntity' +ID = 'minecraft:leash_knot' +LIFECYCLE = 'native117-helper-lifecycle.json' + + +def count(value, expected, name): + if type(value) is not int or value != expected: + raise AssertionError('Incorrect native partition count: ' + name) + + +def validate(document, contract, lifecycle=None): + if hashlib.sha256(contract).hexdigest().upper() != CONTRACT_SHA: + raise AssertionError('Native helper class-control evidence pin differs') + if document.get('helperPolicy') != POLICY or document.get('helperContractSha256') != CONTRACT_SHA: + raise AssertionError('Stopping observer helper policy differs from native contract') + seen, helpers, total, records = set(), {}, 0, {} + for key, record in document['dimensions'].items(): + entries, excluded = record['entities'], record.get('nonpersistentHelpers') + if not isinstance(excluded, list): + raise ValueError('Missing explicit nonpersistent helper partition') + count(record.get('entityCount'), len(entries), key + '/persistent') + count(record.get('nonpersistentHelperCount'), len(excluded), key + '/helpers') + count(record.get('observedEntityCount'), len(entries) + len(excluded), key + '/observed') + rows = native._snapshots(record)[1] if entries else {} + for identity in rows: + if identity in seen: + raise AssertionError('Duplicate observed UUID across classifications/dimensions') + if str(rows[identity]['id']) == ID: + raise AssertionError('No-save helper misclassified as persistent') + seen.add(identity) + records[identity] = rows[identity] + total += len(entries) + for helper in excluded: + identity = helper.get('uuid') + if str(uuid.UUID(identity)) != identity or identity in seen: + raise AssertionError('Duplicate or noncanonical helper UUID') + seen.add(identity) + if (helper.get('id') != ID or helper.get('nativeClass') != CLASS or helper.get('policy') != POLICY + or helper.get('worldDimensionKey') != key or helper.get('canSerialize') is not False + or helper.get('removed') is not False or helper.get('passenger') is not False): + raise AssertionError('Unconfirmed native nonpersistent helper type/state') + count(helper.get('passengerCount'), 0, 'helper passengers') + tag = native._nbt(native._decompress(base64.b64decode(helper['nbtBase64'], validate=True), 1)) + if native._identity(tag) != identity or str(tag['id']) != ID or 'Passengers' in tag: + raise AssertionError('Helper attestation differs from complete actual native NBT') + position = helper.get('attachmentPosition') + # LeashFenceKnotEntity overrides the HangingEntity additional writer + # with an empty method. Its native bounding-box recalculation writes + # Pos at attachment + (0.5, 0.375, 0.5); TileX/Y/Z are not serialized. + native_pos = tag.get('Pos') + if (not isinstance(position, list) or len(position) != 3 or any(type(v) is not int for v in position) + or not isinstance(native_pos, nbtlib.List) or native_pos.subtype is not nbtlib.Double + or len(native_pos) != 3 or any(name in tag for name in ('TileX', 'TileY', 'TileZ')) + or list(map(float, native_pos)) != [position[0] + 0.5, position[1] + 0.375, position[2] + 0.5]): + raise AssertionError('Helper native attachment coordinates differ') + helpers[identity] = dict(dimension=key, attachmentPosition=position) + count(document.get('entityCount'), total, 'total persistent') + count(document.get('nonpersistentHelperCount'), len(helpers), 'total helpers') + count(document.get('observedEntityCount'), total + len(helpers), 'total observed') + if not helpers: + if lifecycle is not None: + raise ValueError('Unexpected helper lifecycle evidence without helpers') + return dict(policy=POLICY, contractSha256=CONTRACT_SHA, status='NOT_REQUIRED', helpers={}) + if not isinstance(lifecycle, dict): + raise AssertionError('Missing separate actual dimension helper lifecycle observation') + key = lifecycle.get('worldDimensionKey') + record = document['dimensions'].get(key) + if (record is None or type(lifecycle.get('gameTime')) is not int + or not 0 <= lifecycle['gameTime'] <= record['gameTime'] + or lifecycle.get('nativeOnLoad') is not True or lifecycle.get('nativeEntityContextVersion') != 1): + raise AssertionError('Helper lifecycle belongs to another dimension/time/native context') + derived = {} + for identity, helper in helpers.items(): + if helper['dimension'] != key: + raise AssertionError('No actual lifecycle observation for helper dimension') + matches = [] + for row in lifecycle.get('entities', []): + if row.get('leashHolderUuid') != identity: + continue + holder = records.get(row.get('uuid')) + if (holder is None or row.get('worldDimensionKey') != key or row.get('sameWorld') is not True + or row.get('leashHolderType') != ID or row.get('leashHolderPosition') != helper['attachmentPosition'] + or 'Leash' not in holder + or [int(holder['Leash'][name]) for name in ('X', 'Y', 'Z')] != helper['attachmentPosition']): + raise AssertionError('Helper lifecycle is not corroborated by actual persistent holder NBT') + matches.append(row['uuid']) + if not matches: + raise AssertionError('No actual persistent holder for observed non-saving helper') + derived[identity] = dict(**helper, holderUuids=sorted(matches)) + return dict(policy=POLICY, contractSha256=CONTRACT_SHA, status='PASS_NATIVE_HELPER_LIFECYCLE', helpers=derived, + observationGameTime=lifecycle['gameTime']) + + +def raw_helpers_absent(files, helpers): + """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 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 new file mode 100644 index 00000000..9eadeb77 --- /dev/null +++ b/test-rig/migration/native117_holder_networks.py @@ -0,0 +1,84 @@ +"""Real holder-unload fixtures outside Forge's full generation dependency halo.""" + +from pathlib import Path +import shutil + +import extended_lifecycle as historical +from matrix import digest +import native117_extended +from native117_identity import fixture_ids +from native117_partial import campaign, common_parser, exercise_bridge, exercise_partial +from native117_ticket_networks import NativeTicketExtendedRun, NativeTicketNetworkRun + + +from native117_geometry import FORCED_LEVEL, LAST_HOLDER_LEVEL, HOLDER_RADIUS +ANCHOR_DISTANCE = 32 + + +def holder_halo(fixture, group): + 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])] + + +def linear_fixtures(): + result = [] + for index, name in enumerate(("C-STEAM", "C-ELECTRIC", "C-FLUID")): + base = 32768 + index * 1024 + origin, sink = [base + 8, 80, 2056], [base + 520, 80, 2056] + pipes = [[x, 80, 2056] for x in range(base + 9, base + 520)] + f = dict(id=name, nativeTicketNetwork=True, min=[base, 72, 2048], max=[base + 543, 90, 2079], + 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) + result.append(f) + return result + + +def bridge_fixtures(): + result = [] + for index, name in enumerate(("C-STEAM", "C-ELECTRIC", "C-FLUID")): + base = 36864 + index * 1024 + origin, sink = [base + 13, 80, 2056], [base + 9, 80, 2056] + path = [[base + 13, 80, z] for z in range(2057, 2569)] + path += [[base + x, 80, 2568] for x in range(12, 8, -1)] + path += [[base + 9, 80, z] for z in range(2567, 2056, -1)] + 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") + result.append(f) + return result + + +def extended_fixtures(): + return [f for f in fixture_ids(historical.fixtures()) if "route" in f] + linear_fixtures() + + +def main(): + parser = common_parser(__doc__) + parser.add_argument("--campaign", choices=("partial", "bridge", "extended"), required=True) + args = parser.parse_args() + definitions = {"partial": linear_fixtures, "bridge": bridge_fixtures, "extended": extended_fixtures} + operations = {"partial": exercise_partial, "bridge": exercise_bridge, "extended": native117_extended.exercise} + runner_type = NativeTicketNetworkRun if args.campaign == "bridge" else NativeTicketExtendedRun + own, own_hash = Path(__file__), digest(Path(__file__)) + transport = Path(__file__).with_name("native117_ticket_networks.py") + transport_hash = digest(transport) + def exercise(runner, progress): + shutil.copy2(own, runner.runtime / own.name) + shutil.copy2(transport, runner.runtime / transport.name) + result = operations[args.campaign](runner, progress) + result.update(holder_protocol_sha256=own_hash, holder_transport_sha256=transport_hash, + holder_protocol_unchanged=(digest(own) == own_hash == digest(runner.runtime / own.name) + and digest(transport) == transport_hash == digest(runner.runtime / transport.name))) + if not result["holder_protocol_unchanged"]: + result["status"] = "FAIL" + progress(result) + return result + return campaign(args, "holder-" + args.campaign, definitions[args.campaign](), exercise, runner_type) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_identity.py b/test-rig/migration/native117_identity.py new file mode 100644 index 00000000..5e577620 --- /dev/null +++ b/test-rig/migration/native117_identity.py @@ -0,0 +1,32 @@ +"""Translate legacy fixture expectations to the released base* identities.""" + +import re + +NAMESPACES = {"poweradvantage": "basepower", "steamadvantage": "basesteam", + "electricadvantage": "baseelectric"} +_ID = re.compile(r"^(?:minecraft:)?(poweradvantage|steamadvantage|electricadvantage)[:.](.+)$") +_COMMAND_ID = re.compile(r"(?> 4, (fixture["max"][0] >> 4) + 1) + for z in range(fixture["min"][2] >> 4, (fixture["max"][2] >> 4) + 1)} + + +def loaded_issues(snapshot, definition): + issues = [] + chunks = snapshot.get("chunks", []) + if len(chunks) != len(required_chunks(definition)) or {tuple(c["pos"]) for c in chunks} != required_chunks(definition): + issues.append("fixture chunk witness set differs") + if any(not chunk.get("loaded") for chunk in chunks): + issues.append("fixture chunks unloaded") + for fixture in definition: + rows = snapshot.get("cases", {}).get(fixture["id"], []) + if len(rows) != len(fixture["witnesses"]) or {tuple(row["pos"]) for row in rows} != {tuple(p) for p in fixture["witnesses"]}: + issues.append(fixture["id"] + ": witness set differs") + if any(not row.get("loaded") for row in rows): + issues.append(fixture["id"] + ": witnesses unloaded") + return issues + + +def load_event_issues(before, after): + old = {tuple(chunk["pos"]): chunk for chunk in before["chunks"]} + events = {tuple(event["pos"]) for event in after.get("chunkEvents", []) if event["operation"] == "load"} + issues = [] + for chunk in after["chunks"]: + pos = tuple(chunk["pos"]) + previous = old.get(pos) + if (previous is None or pos not in events or chunk["loads"] <= 0 + or (not previous["loaded"] and chunk["loads"] <= previous["loads"])): + issues.append("missing actual Chunk.Load: " + str(pos)) + return issues + + +def unload_event_issues(before, after): + old = {tuple(chunk["pos"]): chunk for chunk in before["chunks"]} + current = {tuple(chunk["pos"]): chunk for chunk in after["chunks"]} + if not old or old.keys() != current.keys() or len(current) != len(after["chunks"]): + return ["unload chunk witness set differs"] + events = {tuple(event["pos"]) for event in after.get("chunkEvents", []) if event["operation"] == "unload"} + return ["missing actual Chunk.Unload: " + str(pos) for pos, chunk in current.items() + if pos not in events or chunk["unloads"] <= old[pos]["unloads"]] + + +class NativeLifecycleRun(lifecycle.LifecycleRun): + """Native transport readiness only; inherited fixtures, operations and oracles.""" + + def __init__(self, *args): + super().__init__(*args) + self.observations = [] + self._unload_context = None + + def command(self, command): + return super().command(native_command(command_ids(command))) + + def _raw_probe(self, operation, label): + path = self.runtime / "advantage-works-results" / f"lifecycle-{label}.json" + if path.exists(): + raise FileExistsError("Refusing to overwrite native observation: " + str(path)) + result = super().probe(operation, label) + require_geometry(result, forced=operation == "load") + return result + + def _record(self): + write_json(self.runtime / "native117-readiness.json", self.observations) + + def _round_trip_ready(self, label): + areas = dict.fromkeys(tuple(fixture[key][axis] >> 4 + for key in ("min", "max") for axis in (0, 2)) + 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"] + for index, bounds in enumerate(areas)] + + def _wait_ready(self, label, initial, reload_before=None): + started = time.monotonic() + record = dict(label=label, status="PENDING", min_real_ticks=DEFERRED_TICKS, + max_seconds=READINESS_SECONDS, max_ticks=READINESS_TICKS, + initial_world_time=initial["worldTime"], samples=[]) + self.observations.append(record) + loaded_at = None + snapshot = initial + try: + for attempt in range(READINESS_SAMPLES): + issues = loaded_issues(snapshot, self.definition) + issues += load_event_issues(reload_before if reload_before is not None else initial, snapshot) + now = snapshot["worldTime"] + if now < initial["worldTime"]: + raise AssertionError("Native worldTime moved backwards") + loaded_at = None if issues else (now if loaded_at is None else loaded_at) + sample = dict(label=label + ("-initial" if attempt == 0 else f"-pending-{attempt:03}"), + world_time=now, elapsed_seconds=time.monotonic() - started, issues=issues, + loaded_since_world_time=loaded_at) + record["samples"].append(sample) + if sample["elapsed_seconds"] >= READINESS_SECONDS or now - initial["worldTime"] >= READINESS_TICKS: + break + ready = loaded_at is not None and now - loaded_at >= DEFERRED_TICKS + if ready: + record.update(status="PASS", first_ready=sample, selected_label=sample["label"]) + self._record() + return snapshot + self._record() + if attempt + 1 == READINESS_SAMPLES: + break + # Yield between observations; elapsed sleep is never evidence of a native tick. + time.sleep(0.01) + snapshot = self._raw_probe("snapshot", label + f"-pending-{attempt + 1:03}") + raise TimeoutError("Native readiness did not observe loaded witnesses and actual ticks: " + label) + except Exception as error: + record.update(status="FAIL", failure=str(error)) + self._record() + 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) + if label not in ("lifecycle-restart", "midblock-restart"): + return + require_native_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) + self.observations[-1]["reload_before_label"] = label + "-native-unrestored" + self._record() + + def unload_reload(self, name): + self._unload_context = dict(name=name, before=None, deadline=None) + try: + return super().unload_reload(name) + finally: + self._unload_context = None + + def _unload_observation(self, label): + context = self._unload_context + selected_label = label + "-native-initial" + snapshot = self._raw_probe("snapshot", selected_label) + record = dict(label=label, phase="native-unload-callback-readiness", samples=[]) + self.observations.append(record) + # Verified Forge 1.15 behavior queues Unload after holder removal. Keep the + # inherited 45-second budget; never fabricate flags or callback counters. + for attempt in range(45): + pending = unload_event_issues(context["before"], snapshot) + absent = all(not chunk["loaded"] for chunk in snapshot["chunks"]) + record["samples"].append(dict(label=selected_label, world_time=snapshot["worldTime"], + all_absent=absent, pending_events=pending)) + if not absent or not pending: + break + remaining = context["deadline"] - time.monotonic() + if remaining <= 0 or attempt == 44: + break + self._record() + time.sleep(min(1, remaining)) + selected_label = label + f"-native-pending-{attempt + 1:02}" + snapshot = self._raw_probe("snapshot", selected_label) + source = self.runtime / "advantage-works-results" / f"lifecycle-{selected_label}.json" + canonical = self.runtime / "advantage-works-results" / f"lifecycle-{label}.json" + if canonical.exists(): + raise FileExistsError(canonical) + shutil.copy2(source, canonical) + record.update(selected_label=selected_label, canonical_label=label, + status="READY" if absent and not pending else "PENDING") + self._record() + return snapshot + + def probe(self, operation, label): + context = self._unload_context + if context is not None: + prefix = context["name"] + "-absent-" + if operation == "snapshot" and label.startswith(prefix) and label[len(prefix):].isdigit(): + return self._unload_observation(label) + if operation == "snapshot" and label == context["name"] + "-before": + result = self._raw_probe(operation, label) + context["before"] = result + return result + if operation == "release" and label == context["name"] + "-released": + result = self._raw_probe(operation, label) + context["deadline"] = time.monotonic() + 45 + return result + if operation == "snapshot" and label == context["name"] + "-absent-confirmed": + result = self._raw_probe(operation, label) + context["absent"] = result + return result + if operation == "load" and label == context["name"] + "-loaded": + initial = self._raw_probe(operation, label + "-native-initial") + selected = self._wait_ready(label + "-native", initial, reload_before=context["absent"]) + source = self.runtime / "advantage-works-results" / ( + "lifecycle-" + self.observations[-1]["selected_label"] + ".json") + canonical = self.runtime / "advantage-works-results" / f"lifecycle-{label}.json" + if canonical.exists(): + raise FileExistsError(canonical) + shutil.copy2(source, canonical) + return selected + if operation not in ("legacy-read", "changed-target-read"): + return self._raw_probe(operation, label) + record = dict(label=label, operation=operation, phase=ROUND_TRIP_PROTOCOL, status="PENDING") + self.observations.append(record) + try: + record["chunk_readiness"] = self._round_trip_ready(label) + selected = self._raw_probe(operation, label) + require_round_trip(selected, self.definition, operation, label) + record.update(selected_label=label, canonical_label=label, round_trip=selected["roundTrip"], status="PASS") + if operation == "legacy-read": + for fixture in self.definition: + if "to" not in fixture: + continue + tag = lifecycle.parse(lifecycle.at(selected, fixture["id"], fixture["to"])) + before = lifecycle.parse(selected["roundTrip"]["callbacks"][fixture["id"]]["before"]) + target_keys = ("targetX", "targetY", "targetZ") + if (("TargetState" not in tag and "TargetStates" not in tag) + or before.get("progress", 0) <= 0 or tag.get("progress", 0) < before["progress"] + or any(key not in tag or tag[key] != before.get(key) for key in target_keys)): + record["classification"] = "ELIGIBLE_FIRST_CALLBACK_PRODUCTION_GUARD_FAILURE" + raise AssertionError("First eligible natural callback failed legacy restoration: " + fixture["id"]) + return selected + except Exception as error: + record.update(status="FAIL", failure=str(error)) + raise + finally: + self._record() + + +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") + if trace_loads: + runner.probe("trace-loads", "tracing-loads") + runner.build() + moved = runner.wait_moves() + movements = {movement["id"]: movement for movement in moved["movements"]} + if len(movements) != len(moved["movements"]): + raise AssertionError("Duplicate movement witnesses") + movement_results = {fixture["id"]: lifecycle.move_result(fixture, movements[fixture["id"]]) + for fixture in definition if "from" in fixture} + result = dict(status="RUNNING", move_cases=movement_results, chunk_cycles=0) + progress(result) + print(f"{label}: twelve finite-energy moves observed", flush=True) + time.sleep(60) + runner.processing("commissioned", 1000) + for cycle in range(1, 4): + runner.unload_reload(f"chunk-cycle-{cycle}") + runner.replenish() + runner.probe("offer", f"fluid-offer-{cycle}") + time.sleep(60) + runner.processing(f"chunk-resumed-{cycle}", (cycle + 1) * 1000) + result["chunk_cycles"] = cycle + progress(result) + print(f"{label}: real chunk unload/reload {cycle} and resumed processing PASS", flush=True) + before = runner.probe("snapshot", "before-restart") + runner.command("save-all flush") + runner.close() + runner.boot("lifecycle-restart") + require_native_boot(runner.runtime / "console-lifecycle-restart.log") + time.sleep(1) + after = runner.probe("snapshot", "after-restart") + for fixture in definition: + if "to" not in fixture: + continue + old = lifecycle.at(before, fixture["id"], fixture["to"]) + new = lifecycle.at(after, fixture["id"], fixture["to"]) + if not (lifecycle.contents(old) == lifecycle.contents(new) and old["block"] == new["block"] + and "facing=" + fixture["facing"] in new["state"]): + raise AssertionError("Moved drill changed across full restart: " + fixture["id"]) + runner.processing("restart-retained", 4000) + runner.replenish() + runner.probe("offer", "restart-fluid-offer") + runner.mine() + time.sleep(60) + mined = runner.processing("restart-resumed", 5000) + mining_results = runner.mining_results(mined) + runner.unload_reload("mining-chunk-reload") + time.sleep(5) + mining_after = runner.mining_results(runner.probe("snapshot", "mining-retained")) + result.update(mining=mining_results, mining_after_chunk_reload=mining_after) + progress(result) + midblock_results = runner.midblock() + result["mid_block_progress"] = midblock_results + progress(result) + save_read_results = runner.save_read_guards() + failed_changed = [name for name, row in save_read_results["changed_target_read"].items() if row["status"] != "PASS"] + if failed_changed: + result["changed_target_observation_gap"] = dict(cases=failed_changed, detail= + "Strict oracle failed on retained first eligible natural tile callbacks. START prerequisites, immediate state, native dispatch stacks and matching END retained; no verdict-based resampling.", + classification="ELIGIBLE_FIRST_CALLBACK_PRODUCTION_GUARD_FAILURE") + runner.command("save-all flush") + runner.close() + groups = (movement_results, mining_results, mining_after, midblock_results["chunk_reload"], + midblock_results["full_restart"], save_read_results["legacy_read"], + save_read_results["changed_target_read"]) + result.update(status="PASS" if require_groups(definition, groups) else "FAIL", chunk_cycles=5, + full_restart="PASS", network_processing="PASS", fluid_mB=5000, + mining=mining_results, mining_after_chunk_reload=mining_after, + mid_block_progress=midblock_results, save_read_guards=save_read_results) + progress(result) + print(f"{label}: {result['status']}; movement, mining, chunk and restart oracles retained", flush=True) + return result + + +def frozen_import(runner, source): + # This is a separate frozen boot, before construction authority or live ticks. + runner.boot("retained13-import") + try: + require_native_boot(runner.runtime / "console-retained13-import.log") + runner.server.snapshot("retained13-import") + runner.command("save-all flush") + time.sleep(2) + finally: + runner.close() + result = compare_upgraded(source / "migration-restarted.json", + runner.runtime / "migration-retained13-import.json") + write_json(runner.runtime / "native117-import-comparison.json", result) + if result["status"] != "PASS": + raise AssertionError("Frozen retained13-to-native117 preservation failed") + return result + + +def run(args): + if not 1 <= args.port <= 65535 or not args.java8.is_file(): + raise ValueError("Supply an available TCP port and an existing Java 8 executable") + output = output_path(args.output) + for protected in (args.source, args.source_manifest, args.source_hashes, args.target_manifest): + require_disjoint(output, protected) + source_before, history = verify_source(args) + target = pinned_profile(args.target_manifest.resolve(), "target117") + identities = {entry["id"] for entry in read_json(args.target_manifest)["target117"]["jars"]} + if not {"basesteam", "baseelectric", "basemetals"}.issubset(identities): + raise ValueError("Full lifecycle requires native Steam, Electric and Base Metals") + for protected in (target[0], *target[2]): + require_disjoint(output, protected) + with socket.socket() as listener: + listener.bind(("127.0.0.1", args.port)) + definition = fixture_ids(lifecycle.fixtures()) + helpers = (Path(lifecycle.__file__), Path(__file__), Path(__file__).with_name("native117_readiness.py")) + helper_before = {path.name: digest(path) for path in helpers} + output.mkdir(parents=True, exist_ok=False) + report_path = output / "native117-lifecycle-results.json" + report = dict(status="RUNNING", started_at_utc=datetime.now(timezone.utc).isoformat(), + scope="independent-native117-movement-aware-lifecycle", + protocol="Inherited LifecycleRun operations, unchanged fixtures/oracles; bounded native loaded/tick readiness; genuine chunk events; no source process", + source=str(args.source.resolve()), sourceKey=args.source_key, + sourceManifest=str(args.source_manifest.resolve()), sourceManifestSha256=digest(args.source_manifest), + sourceHashes=str(args.source_hashes.resolve()), sourceHashesSha256=digest(args.source_hashes), + sourceRuntimeBefore=source_before, sourceWorldExpected=read_json(args.source_hashes), + sourceStateGates=history, targetManifest=str(args.target_manifest.resolve()), + targetManifestSha256=digest(args.target_manifest), targetProfile=read_json(args.target_manifest)["target117"], + helperSha256=helper_before, port=args.port, profiles={}) + report["load_stack_tracing"] = getattr(args, "trace_loads", False) + for path in helpers: + archive = output / "protocol-sources" / path.name + archive.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, archive) + write_json(output / "lifecycle-fixture.json", definition) + write_json(report_path, report) + runner = None + try: + for name, source in (("fresh117", None), ("original110-chained113-native117", args.source)): + runner = None + result = dict(status="RUNNING") + report["profiles"][name] = result + write_json(report_path, report) + try: + runtime = output / name + launcher = prepare(runtime, target, definition, args.port, source) + runner = NativeLifecycleRun(runtime, args.java8, launcher, definition) + if source is not None: + result["frozen_import"] = frozen_import(runner, source) + authorize_lifecycle(runtime) + + def progress(value): + result.update(value) + write_json(report_path, report) + + exercise(runner, name, progress, trace_loads=getattr(args, "trace_loads", False)) + except Exception as error: + result.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + print(f"{name}: FAIL: {error}", flush=True) + finally: + if runner is not None: + result["native_readiness"] = dict(file="native117-readiness.json", stages=len(runner.observations)) + try: + runner.close() + except Exception as error: + result.update(status="FAIL", shutdown_error=str(error)) + runtime = output / name + if (runtime / "input-jars.json").is_file(): + try: + verify_staged(runtime, target) + logs = sorted(runtime.glob("console-*.log")) + for log in logs: + require_native_boot(log) + result["native_boots"] = [log.name for log in logs] + result["forge_config_reload_notices"] = { + log.name: log.read_text(encoding="utf-8", errors="replace").count( + "[ne.mi.co.ForgeConfig/CORE]: Forge config just got changed on the file system!") + for log in logs} + except Exception as error: + result.update(status="FAIL", runtime_audit_error=str(error)) + write_json(report_path, report) + report["status"] = "PASS" if all(row["status"] == "PASS" for row in report["profiles"].values()) else "FAIL" + except BaseException as error: + report.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + raise + finally: + if runner is not None: + try: + runner.close() + except Exception as error: + report.update(status="FAIL", shutdown_error=str(error)) + after = fingerprint(args.source.resolve()) + report["sourceRuntimeAfter"] = after + report["source_fixture_unchanged"] = after == source_before + report["sourceWorldAfter"] = fingerprint(args.source.resolve() / WORLD) + if after != source_before: + report.update(status="FAIL", source_error="Protected retained13 source changed") + helper_after = {path.name: digest(path) for path in helpers} + report["helpers_unchanged"] = helper_after == helper_before + if helper_after != helper_before: + report.update(status="FAIL", helper_error="Lifecycle code changed during execution") + report["completed_at_utc"] = datetime.now(timezone.utc).isoformat() + write_json(report_path, report) + print(report["status"] + ": " + str(report_path), flush=True) + return int(report["status"] != "PASS") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--source-manifest", type=Path, required=True) + parser.add_argument("--source-key", default="target113") + parser.add_argument("--source-hashes", type=Path, required=True) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, default=25580) + parser.add_argument("--trace-loads", action="store_true", + help="Record native chunk-load caller stacks; does not change any lifecycle oracle or timeout") + return run(parser.parse_args()) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_machine_items.py b/test-rig/migration/native117_machine_items.py new file mode 100644 index 00000000..363f8f16 --- /dev/null +++ b/test-rig/migration/native117_machine_items.py @@ -0,0 +1,903 @@ +"""Fresh old-machine item witnesses; source seeding and native destination are separate leases. + +Only this new module owns the six explicit vanilla item expectations. Historical +helpers/oracles are imported unchanged. No destination construction or refill API +is exposed. A measured idle interval proves preservation, not recipe throughput. +""" + +import argparse +import copy +from collections import Counter +import gzip +import hashlib +import io +import json +from pathlib import Path +import re +import shutil +import socket +import uuid +import zipfile +import zlib + +import nbtlib + +import matrix +import native117 +from native117_code import Server as NativeServer, code_files +from native117_connected import require_native_boot +from transformer_order import read_region + + +SOURCES = ("original110", "early110", "maintained110", "legacy112", "maintained112") +CASES = ("wool14", "coal1", "dye4", "enchanted-pick", "enchanted-book", "nested-chest") +MACHINES = ("poweradvantage:item_conveyor", "poweradvantage:item_filter_inventory", + "electricadvantage:electric_furnace", "steamadvantage:steam_furnace") +OLD_POWER_C = "DBE2226C4A1FE505A94A411EA68540CAEC40186598847BAB936CE16EC8E2FA01" +FROZEN_SOURCE_PROBE = "38565B6671E249B6E12D5B238355FD7D48218B261789E7E92316F9D9DC29F94A" +ENCHANTMENTS = {32: "minecraft:efficiency", 34: "minecraft:unbreaking"} +HISTORY = Path(__file__).with_name("native117-historical-inputs.json") +PLAYER_UUID = uuid.UUID("a614e658-6270-33c3-9297-7ce80908964f") +SOURCE_CONSOLE_ROOT = native117.LEGACY_POWER / "run-actor-migration/20261007-bridge112-strict-r2" +SOURCE_CONSOLE_PINS = { + "original110": "B64C0E7C0845F00AA2843DD68810E74294F85C51891AFB2AFE7F0DC6881CD2D6", + "legacy112": "FD6415C1B73609EF2B18394158B1F0DFEF3FC29F9388545C010F03CEE476175E", +} +KNOWN_SOURCE_LINES = ( + "[main/ERROR] [FML]: FML appears to be missing any signature data. This is not a good thing", + "[Server thread/ERROR] [FML]: Trying to activate the universal filled bucket too late. Call it statically in your Mods class. Mod: Base Metals", +) +LEGACY112_INTEGRATION_LINES = ( + "[Server thread/FATAL] [orespawn]: Loading Integration For orespawn", + "[Server thread/FATAL] [orespawn]: Loading Integration For basemetals", +) +SOURCE_DIAGNOSTIC_POLICY = {"original110": KNOWN_SOURCE_LINES, "legacy112": LEGACY112_INTEGRATION_LINES} +SOURCE_WITNESS = "SOURCE_WITNESS" +SOURCE_WITH_DIAGNOSTICS = "SOURCE_WITNESS_WITH_KNOWN_DIAGNOSTICS" +SOURCE_SUCCESS = (SOURCE_WITNESS, SOURCE_WITH_DIAGNOSTICS) +SOURCE_FAILURE = re.compile(r"\b(?:ERROR|FATAL|NoSuch[\w.$]*|[\w.$]*Exception[\w.$]*|[\w.$]+Error)\b", re.IGNORECASE) + + +def read(path): + return json.loads(Path(path).read_text(encoding="utf-8")) + + +def checked(path, checksum): + path = Path(path).resolve() + if not re.fullmatch(r"[0-9A-Fa-f]{64}", checksum) or matrix.digest(path) != checksum.upper(): + raise AssertionError("Pinned file changed: " + str(path)) + return path + + +def history_custody(): + hashes = read(HISTORY)["hashes"] + for name, checksum in hashes.items(): + checked(native117.RIG / name, checksum) + return {"receiptSha256": matrix.digest(HISTORY), "files": hashes} + + +def source_probe(profile): + probes = [jar for jar in profile[2] if jar.name.startswith("AdvantageMigrationProbe-")] + if len(probes) != 1 or matrix.digest(probes[0]) != FROZEN_SOURCE_PROBE: + raise AssertionError("Source requires the inspected frozen advprobe serializer/freeze jar; no replacement inferred") + with zipfile.ZipFile(probes[0]) as archive: + probe = archive.read("zone/moddev/mc/advantageworks/migration/LegacyProbe.class") + if any(token not in probe for token in (b"migration-operate.allow", b"field_175730_i", b"nbtBase64")): + raise AssertionError("Frozen source serializer/freeze channel missing") + actors = archive.read("zone/moddev/mc/advantageworks/migration/LegacyActors.class") + if b"StoredEnchantments" in actors: + raise AssertionError("Uninspected source actor adapter changed") + if b"playerAdapter" not in actors or b"AdvWorksPlayer" not in actors: + raise AssertionError("Frozen source native player read/snapshot adapter missing") + return dict(jar=str(probes[0]), sha256=matrix.digest(probes[0]), command="advprobe snapshot", + nativeSourceHeldFlag="NOT_EXPOSED_BY_ARCHIVED_PROBE", + freeze="Pinned WorldTickEvent.START clears tickable tiles while migration-operate.allow is absent", + proof="Actual old tile.writeToNBT ItemStacks plus stopped raw chunk NBT; no synthetic native fields", + playerCommand="advprobe actors-read") + + +def items(case, old_chest="Chest", stored_enchantments=None): + def stack(identity, count, damage=0, tag=None): + result = dict(id="minecraft:" + identity, Count=count, Damage=damage) + if tag is not None: + result["tag"] = tag + return result + ench = [dict(id=32, lvl=3), dict(id=34, lvl=2)] + values = { + "wool14": stack("wool", 7, 14), + "coal1": stack("coal", 5, 1), + "dye4": stack("dye", 9, 4), + "enchanted-pick": stack("diamond_pickaxe", 1, 17, { + "ench": ench, "RepairCost": 4, "AdvMachineItems": {"note": "unchanged", "number": 19}}), + "enchanted-book": stack("enchanted_book", 1, tag={"StoredEnchantments": copy.deepcopy(stored_enchantments or ench)}), + } + nested = [dict(Slot=index, **copy.deepcopy(values[name])) for index, name in enumerate(CASES[:-1])] + values["nested-chest"] = stack("chest", 1, tag={"BlockEntityTag": { + "id": old_chest, "Items": nested}, "AdvMachineItems": "nested-vanilla-inventory"}) + if case not in values: + raise ValueError("Unknown machine item case: " + case) + return copy.deepcopy(values[case]) + + +def modern_item(value, registry=None): + """Exact fixed expectation for these witnesses, never a general DFU substitute.""" + result = copy.deepcopy(value) + identity, damage = result["id"], result.pop("Damage", 0) + variants = {("minecraft:wool", 14): "minecraft:red_wool", + ("minecraft:coal", 1): "minecraft:charcoal", + ("minecraft:dye", 4): "minecraft:lapis_lazuli"} + if (identity, damage) in variants: + result["id"] = variants[(identity, damage)] + elif identity == "minecraft:diamond_pickaxe": + result.setdefault("tag", {})["Damage"] = damage + elif damage != 0: + raise ValueError("Unexpected metadata in a fixed witness") + tag = result.get("tag", {}) + for old, new in (("ench", "Enchantments"), ("StoredEnchantments", "StoredEnchantments")): + if old in tag: + tag[new] = [dict(entry, id=(registry or ENCHANTMENTS)[entry["id"]]) for entry in tag.pop(old)] + if "BlockEntityTag" in tag: + chest = tag["BlockEntityTag"] + if chest["id"] not in ("Chest", "minecraft:chest"): + raise ValueError("Unexpected nested block entity") + chest["id"] = "minecraft:chest" + chest["Items"] = [modern_item(entry, registry) for entry in chest["Items"]] + return result + + +def snbt(value, key=None): + if isinstance(value, dict): + return "{" + ",".join(name + ":" + snbt(entry, name) for name, entry in value.items()) + "}" + if isinstance(value, list): + return "[" + ",".join(snbt(entry) for entry in value) + "]" + if isinstance(value, str): + return json.dumps(value, ensure_ascii=True) + if type(value) is int: + return str(value) + ("b" if key in ("Count", "Slot") else "s" if key in ("Damage", "lvl") or key == "id" else "") + raise TypeError("Unsupported witness NBT value") + + +def inventory(case, machine, old_chest, stored_enchantments=None): + result = [dict(Slot=0, **items(case, old_chest, stored_enchantments))] + if machine == "poweradvantage:item_filter_inventory": + template = dict(Slot=1, **items(case, old_chest, stored_enchantments)) + template["Count"] = 1 + result.append(template) + return result + + +def enchantment_registry(level_file): + level = nbtlib.load(level_file) + entries = level["FML"]["Registries"]["minecraft:enchantments"]["ids"] + pairs = [(int(entry["V"]), str(entry["K"])) for entry in entries] + if len({number for number, _ in pairs}) != len(pairs) or len({name for _, name in pairs}) != len(pairs): + raise AssertionError("Ambiguous saved source enchantment registry") + by_name = {name: number for number, name in pairs} + steam = sorted((number, name) for number, name in pairs if name.startswith("steamadvantage:")) + if not steam or by_name.get("minecraft:efficiency") != 32 or by_name.get("minecraft:unbreaking") != 34: + raise AssertionError("Saved registry cannot support the exact numeric book/pick witnesses") + return dict(names={str(number): name for number, name in pairs}, + stored=[dict(id=by_name["minecraft:efficiency"], lvl=3)] + [dict(id=number, lvl=1) for number, _ in steam]) + + +def registry_sha256(registry): + return hashlib.sha256(json.dumps(registry, sort_keys=True, separators=(",", ":")).encode("ascii")).hexdigest().upper() + + +def names(definition): + return {int(number): name for number, name in definition.get("enchantmentNames", ENCHANTMENTS).items()} + + +def select_definition(snapshot, case, source_key): + selected = [] + for index, name in enumerate(MACHINES): + available = sorted((entry for entry in snapshot if entry["actualName"] == name), + key=lambda entry: (entry["x"], entry["y"], entry["z"])) + if not available: + raise AssertionError("Approved source lacks an existing machine: " + name) + entry = available[0] + if "nbtBase64" not in entry: + raise AssertionError("Existing machine has no serializer evidence") + selected.append(dict(id="MI-" + str(index), station="machine-items", kind=name.split(":")[1], + name=name, **{key: entry[key] for key in ("x", "y", "z")})) + positions = {(entry["x"], entry["y"], entry["z"]) for entry in selected} + if len(positions) != len(MACHINES): + raise AssertionError("Duplicate existing machine position") + low = [min(entry[key] for entry in selected) for key in ("x", "y", "z")] + high = [max(entry[key] for entry in selected) for key in ("x", "y", "z")] + if low[1] < 64 or high[1] > 160 or any(b - a > 512 for a, b in zip(low, high)): + raise ValueError("Existing witnesses exceed unchanged ConnectedSession bounds") + return dict(format=1, sourceProfile=source_key, case=case, bounds=low + high, blocks=selected) + + +def seed_commands(definition): + chest = "Chest" if definition["sourceProfile"].endswith("110") else "minecraft:chest" + return ["blockdata %d %d %d %s" % (entry["x"], entry["y"], entry["z"], snbt({ + "Items": inventory(definition["case"], entry["name"], chest, definition.get("storedEnchantments")), + "ForgeData": {"AdvConnectedWitness": entry["id"]}})) for entry in definition["blocks"]] + + +def require_command(command, *, source_seeds=(), operated=False): + if command in ("stop", "save-all flush") or command in source_seeds: + return + if source_seeds: + if re.fullmatch(r"advprobe (snapshot|actors-read) [A-Za-z0-9_-]+", command): + return + raise AssertionError("Forbidden old source command: " + command) + if re.fullmatch(r"advconnected snapshot [A-Za-z0-9_-]+", command): + return + if re.fullmatch(r"advactors read [A-Za-z0-9_-]+", command): + return + match = re.fullmatch(r"advconnected step ([0-9]+) [A-Za-z0-9_-]+", command) + if operated and match and 1 <= int(match[1]) <= 4000: + return + raise AssertionError("Forbidden machine-items command: " + command) + + +class ObservedServer(matrix.Server): + def __init__(self, *args, source_seeds=(), **kwargs): + self.source_seeds = tuple(source_seeds) + self.operated = False + self.commands = [] + super().__init__(*args, **kwargs) + + def send(self, command): + require_command(command, source_seeds=self.source_seeds, operated=self.operated) + self.commands.append(command) + super().send(command) + + def close(self): + try: + super().close() + finally: + matrix.write_json(Path(self.log.name).with_suffix(".commands.json"), self.commands) + + +class DestinationServer(ObservedServer, NativeServer): + pass + + +def document_items(document, definition, *, modern, ticks=0, previous=None): + if document.get("held") is not True or document.get("buildCallsThisProcess") != 0: + raise AssertionError("Construction or unfrozen observation is not preservation") + if (document.get("requestedTicks"), document.get("advancedTicks")) != (ticks, ticks): + raise AssertionError("Missing exact native tick/freeze interval") + return serialized_items(document["snapshot"], definition, modern=modern, previous=previous) + + +def source_items(rows, definition): + if not isinstance(rows, list): + raise AssertionError("Old advprobe proof must be its actual snapshot array, not synthesized native protocol flags") + return serialized_items(rows, definition, modern=False, native_fields=False) + + +def serialized_items(rows, definition, *, modern, previous=None, native_fields=True): + expected = {entry["id"]: entry for entry in definition["blocks"]} + if len(rows) != len(expected) or {row["id"] for row in rows} != expected.keys(): + raise AssertionError("Missing, duplicate or unexpected machine witness") + result = {} + chest = "Chest" if definition["sourceProfile"].endswith("110") else "minecraft:chest" + prior = {row["id"]: row for row in previous["snapshot"]} if previous else {} + for row in rows: + spec = expected[row["id"]] + if any(row[key] != spec[key] for key in ("name", "kind", "station", "x", "y", "z")): + raise AssertionError("Machine witness definition moved/changed") + if row["actualName"] != spec["name"] or (native_fields and row.get("witness") != spec["id"]): + raise AssertionError("Machine/witness lost or replaced") + tile = matrix.plain(matrix.parse_nbt(row)) + if tile.get("ForgeData", {}).get("AdvConnectedWitness") != spec["id"]: + raise AssertionError("Actual tile NBT source witness lost") + if [tile.get(key) for key in ("x", "y", "z")] != [spec[key] for key in ("x", "y", "z")]: + raise AssertionError("Tile coordinates changed") + expected_items = inventory(definition["case"], spec["name"], chest, definition.get("storedEnchantments")) + if modern: + expected_items = [modern_item(stack, names(definition)) for stack in expected_items] + observed = tile.get("Items") + if not isinstance(observed, list) or observed != expected_items: + raise AssertionError("Exact item mismatch at " + spec["id"] + ": " + json.dumps({ + "expected": expected_items, "actual": observed}, sort_keys=True)) + if spec["id"] in prior and row.get("objectIdentity") != prior[spec["id"]].get("objectIdentity"): + raise AssertionError("Tile object replaced during the same measured interval") + if native_fields and not isinstance(row.get("objectIdentity"), int): + raise AssertionError("Missing actual tile identity") + result[spec["id"]] = dict(items=observed, properties=row["properties"], tileId=tile["id"]) + return result + + +def observe_source(server, runtime, label): + server.send("advprobe snapshot " + label) + server.wait("ADVPROBE COMPLETE snapshot", 120) + return read(runtime / ("migration-" + label + ".json")) + + +def seed_player(runtime, definition): + """A NEW disposable file only, before the old native player-file read.""" + path = runtime / matrix.WORLD / "playerdata" / (str(PLAYER_UUID) + ".dat") + if path.exists(): + raise FileExistsError("Never overwrite an inherited player file, even on a disposable copy: " + str(path)) + path.parent.mkdir(parents=True, exist_ok=True) + book = dict(Slot=0, **items("enchanted-book", stored_enchantments=definition["storedEnchantments"])) + most, least = PLAYER_UUID.int >> 64, PLAYER_UUID.int & ((1 << 64) - 1) + signed = lambda number: number - (1 << 64) if number >= 1 << 63 else number + data = nbtlib.parse_nbt('{DataVersion:%d,UUIDMost:%dL,UUIDLeast:%dL,Inventory:%s,EnderItems:[],' + 'Pos:[144.5d,80.0d,8.5d],Motion:[0.0d,0.0d,0.0d],Rotation:[0.0f,0.0f],' + 'Dimension:0,Health:17.0f,foodLevel:16,SelectedItemSlot:0}' % ( + 512 if definition["sourceProfile"].endswith("110") else 1343, signed(most), signed(least), snbt([book]))) + nbtlib.File(data, gzipped=True).save(path) + matrix.write_json(runtime / "actor-fixture.json", dict(playerName="AdvWorksPlayer", playerUuid=str(PLAYER_UUID), + minX=128, maxX=191, universalBucket=False)) + return matrix.digest(path) + + +def player_items(data, definition, *, modern): + expected = dict(Slot=0, **items("enchanted-book", stored_enchantments=definition["storedEnchantments"])) + if modern: + expected = modern_item(expected, names(definition)) + if data.get("Inventory") != [expected] or data.get("EnderItems", []) != []: + raise AssertionError("Exact player StoredEnchantments/inventory mismatch: " + json.dumps({ + "expected": [expected], "actual": data.get("Inventory")}, sort_keys=True)) + if "UUID" in data: + actual_uuid = uuid.UUID(int=sum((int(word) & 0xffffffff) << (96 - index * 32) + for index, word in enumerate(data["UUID"]))) + else: + actual_uuid = uuid.UUID(int=((int(data["UUIDMost"]) & ((1 << 64) - 1)) << 64) + | (int(data["UUIDLeast"]) & ((1 << 64) - 1))) + if actual_uuid != PLAYER_UUID: + raise AssertionError("Player UUID changed") + return expected + + +def observe_player(server, runtime, label, definition, *, modern): + server.send(("advactors read " if modern else "advprobe actors-read ") + label) + server.wait("ADVACTORS COMPLETE " + label, 120) + record = read(runtime / ("actors-" + label + ".json")) + if record.get("playerAdapter") is not True or record.get("frozen") is not True: + raise AssertionError("No actual native player-file read was performed") + return player_items(matrix.plain(matrix.parse_nbt(record["player"])), definition, modern=modern) + + +def stopped_player(runtime, label, definition, *, modern): + original = runtime / matrix.WORLD / "playerdata" / (str(PLAYER_UUID) + ".dat") + target = runtime / "machine-items-evidence" / label / "player.dat" + shutil.copy2(original, target) + player_items(matrix.plain(nbtlib.load(target).unpack()), definition, modern=modern) + return matrix.digest(target) + + +def observe(server, runtime, label, ticks=0): + server.send(f"advconnected step {ticks} {label}" if ticks else f"advconnected snapshot {label}") + server.wait("ADVCONNECTED COMPLETE " + label, max(120, ticks / 20 + 90)) + document = read(runtime / ("connected-" + label + ".json")) + if document["snapshot"] != read(runtime / ("migration-" + label + ".json")): + raise AssertionError("Probe snapshot channels disagree") + return document + + +def decode_chunk(payload): + if len(payload) < 5 or int.from_bytes(payload[:4], "big") != len(payload) - 4: + raise ValueError("Malformed saved chunk length") + decoder = {1: gzip.decompress, 2: zlib.decompress, 3: lambda raw: raw}[payload[4]] + return matrix.plain(nbtlib.File.parse(io.BytesIO(decoder(payload[5:]))).unpack()) + + +def stopped_evidence(runtime, label, definition, document, *, modern): + """Call only after close: retain exact raw chunks and independently read tile items.""" + folder = runtime / "machine-items-evidence" / label + folder.mkdir(parents=True, exist_ok=False) + matrix.write_json(folder / "snapshot.json", document) + cache = {} + for spec in definition["blocks"]: + cx, cz = spec["x"] // 16, spec["z"] // 16 + key = (cx, cz) + if key not in cache: + region = runtime / matrix.WORLD / "region" / ("r.%d.%d.mca" % (cx // 32, cz // 32)) + _, chunks = read_region(region) + payload = chunks[(cx & 31) + (cz & 31) * 32] + (folder / ("%d_%d.chunk" % key)).write_bytes(payload) + cache[key] = decode_chunk(payload) + audit_raw(folder, definition, document, modern=modern) + if definition.get("playerBook"): + stopped_player(runtime, label, definition, modern=modern) + hashes = matrix.fingerprint(folder) + matrix.write_json(folder / "receipt.json", dict(status="PASS", files=hashes, closedBeforeCapture=True)) + return hashes + + +def audit_raw(folder, definition, document, *, modern): + rows = document["snapshot"] if modern else document + live = {row["id"]: matrix.plain(matrix.parse_nbt(row)) for row in rows} + for spec in definition["blocks"]: + key = (spec["x"] // 16, spec["z"] // 16) + chunk = decode_chunk((folder / ("%d_%d.chunk" % key)).read_bytes()) + expected_version = 2730 if modern else 512 if definition["sourceProfile"].endswith("110") else 1343 + if chunk.get("DataVersion") != expected_version: + raise AssertionError("Wrong native saved chunk DataVersion") + level = chunk["Level"] + if (level["xPos"], level["zPos"]) != key: + raise AssertionError("Saved chunk coordinates changed") + matches = [tile for tile in level["TileEntities"] if all(tile.get(k) == spec[k] for k in ("x", "y", "z"))] + if len(matches) != 1 or matches[0].get("Items") != live[spec["id"]].get("Items"): + raise AssertionError("Stopped raw tile differs from live old/native ItemStacks") + if matches[0].get("id") != live[spec["id"]].get("id"): + raise AssertionError("Stopped raw tile identity differs") + + +def plan(routes_path, routes_sha256, source_keys): + routes_path = checked(routes_path, routes_sha256) + custody = history_custody() + rows = read(routes_path)["routes"] + if not source_keys or len(source_keys) != len(set(source_keys)) or set(source_keys) - set(SOURCES): + raise ValueError("Explicit unique approved old110/112 profiles required") + result = dict(format=1, scope="NEW_DISPOSABLE_MACHINE_ITEMS_NOT_HISTORICAL_ORACLE", + historical=custody, routePlan=str(routes_path), routePlanSha256=matrix.digest(routes_path), + runnerSha256=matrix.digest(Path(__file__)), sources=[], status="PLAN_ONLY_NO_MC") + for key in source_keys: + selected = [row for row in rows if row["kind"] == "frozen" and row["id"] == "legacy/" + key] + if len(selected) != 1 or selected[0]["sourceKey"] != key: + raise AssertionError("Missing unique approved frozen source route") + row = selected[0] + source = Path(row["source"]).resolve() + before = matrix.fingerprint(source) + for name in ("sourceManifest", "sourceHashes"): + checked(row[name], row[name + "Sha256"]) + checked(source / row["sourceSnapshot"], row["sourceSnapshotSha256"]) + if matrix.fingerprint(source / matrix.WORLD) != read(row["sourceHashes"]): + raise AssertionError("Canonical world differs from approved frozen receipt") + profile = native117.pinned_profile(Path(row["sourceManifest"]), key) + channel = source_probe(profile) + expected = sorted((jar.name, matrix.digest(jar)) for jar in profile[2]) + # Approved frozen fixtures may contain only worlds/snapshots, not server code. + # The separately sealed archived profile supplies the exact old runtime. + if (source / "mods").exists() and sorted((jar.name, matrix.digest(jar)) for jar in (source / "mods").glob("*.jar")) != expected: + raise AssertionError("Canonical old jar set differs from approved source profile") + registry = enchantment_registry(source / matrix.WORLD / "level.dat") + console = canonical_source_console(key) + definitions = {case: select_definition(read(source / row["sourceSnapshot"]), case, key) for case in CASES} + for case, definition in definitions.items(): + definition.update(enchantmentNames=registry["names"], storedEnchantments=registry["stored"], + playerBook=case == "enchanted-book") + if matrix.fingerprint(source) != before: + raise AssertionError("Canonical source changed during plan") + result["sources"].append(dict(route=row, runtimeFingerprint=before, definitions=definitions, + seedCode=code_files(profile[0]), launcher=profile[1], jars=expected, + sourceChannel=channel, + canonicalConsole=console, sourceRegistrySha256=registry_sha256(registry), + canonicalLayout="world-and-snapshots" if not (source / "mods").exists() else "runtime")) + result["sourceBoots"] = len(source_keys) * len(CASES) + result["destinationBoots"] = 3 * result["sourceBoots"] + result["manualGaps"] = ["Source blockdata acceptance needs leased old native boot proof", + "No recipe throughput assertion: exact item preservation across measured 512 ticks only", + "Destination requires separately frozen PowerD manifest and fresh explicit lease"] + result["playerBook"] = "One NEW disposable player file in each source enchanted-book case; actual old read/save before fingerprint; native read/save on all destination boots" + return result + + +def load_plan(path, checksum): + value = read(checked(path, checksum)) + if value.get("format") != 1 or value.get("runnerSha256") != matrix.digest(Path(__file__)): + raise AssertionError("Plan format/runner changed; prepare a new plan") + if value["historical"] != history_custody(): + raise AssertionError("Historical source helpers/oracles changed") + checked(value["routePlan"], value["routePlanSha256"]) + for source in value["sources"]: + if source["canonicalConsole"] != canonical_source_console(source["route"]["sourceKey"]): + raise AssertionError("Pinned canonical source console changed") + return value + + +def new_root(path, protected): + root = Path(path).resolve() + for value in protected: + native117.require_disjoint(root, Path(value)) + if root.exists(): + raise FileExistsError(root) + return root + + +def configure(runtime, definition, port, *, destination=True): + matrix.write_json(runtime / "connected-fixture.json", definition) + matrix.write_json(runtime / "migration-fixture.json", definition["blocks"]) + if destination: + for marker in ("advantage-connected.allow", "migration-operate.allow"): + (runtime / marker).touch() + elif any((runtime / marker).exists() for marker in ("advantage-connected.allow", "migration-operate.allow")): + raise AssertionError("Old source must retain the archived probe's frozen marker state") + if not destination: + # Old dedicated servers log FileNotFoundException for absent admin lists. + for filename in ("banned-players.json", "banned-ips.json", "ops.json", "whitelist.json"): + path = runtime / filename + if not path.exists(): + matrix.write_json(path, []) + elif read(path) != []: + raise AssertionError("Disposable old source admin list is not empty: " + filename) + props = runtime / "server.properties" + props.write_text(props.read_text().replace("server-port=25584", f"server-port={port}")) + if any((runtime / name).exists() for name in ("connected-build.allow", "connected-built.json")): + raise AssertionError("Source construction authority must never be staged") + + +def clean_log(path, *, modern): + if modern: + require_native_boot(path) + failures = [line for line in path.read_text(encoding="utf-8", errors="strict").splitlines() if SOURCE_FAILURE.search(line)] + if failures: + raise AssertionError("Target log has no source-diagnostic allowance: " + json.dumps(failures)) + else: + native117.require_clean_log(path) + if "Done (" not in path.read_text(encoding="utf-8", errors="replace"): + raise AssertionError("Missing old source startup proof") + + +def source_line_identity(line): + # The clock changes on every boot; all remaining bytes stay exact. + return re.sub(r"^\[(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\] ", "", line, count=1) + + +def canonical_source_console(profile): + if profile not in SOURCE_CONSOLE_PINS: + raise ValueError("No approved retained source-console pin for profile: " + profile) + path = checked(SOURCE_CONSOLE_ROOT / profile / "console-source.log", SOURCE_CONSOLE_PINS[profile]) + lines = path.read_text(encoding="utf-8", errors="strict").splitlines() + return dict(profile=profile, path=str(path), sha256=matrix.digest(path), + knownRawLines=[line for line in lines if source_line_identity(line) in SOURCE_DIAGNOSTIC_POLICY[profile]]) + + +def source_log_report(path, profile, authority): + """Only hash-pinned, profile-specific historical diagnostics may be witnessed.""" + if authority != canonical_source_console(profile): + raise AssertionError("Source diagnostic authority/profile changed") + raw = path.read_text(encoding="utf-8", errors="strict").splitlines() + if not any("Done (" in line for line in raw): + raise AssertionError("Missing old source startup proof") + historical = Counter(source_line_identity(line) for line in authority["knownRawLines"]) + actual = [line for line in raw if SOURCE_FAILURE.search(line)] + counts = Counter(source_line_identity(line) for line in actual) + failures = [line for line in actual if source_line_identity(line) not in historical] + repeated = {line: count for line, count in counts.items() if count > historical[line]} + if failures or repeated: + raise AssertionError("Unapproved source ERROR/exception or diagnostic count: " + json.dumps({ + "rawLines": failures, "excessCounts": repeated}, sort_keys=True)) + diagnostics = [dict(status="KNOWN_SOURCE_DIAGNOSTIC", identity=identity, + canonicalSourceLog=authority["path"], canonicalSourceLogSha256=authority["sha256"], + canonicalRawLines=[line for line in authority["knownRawLines"] if source_line_identity(line) == identity], + canonicalCount=historical[identity], actualRawLines=[line for line in actual if source_line_identity(line) == identity], + actualCount=count) for identity, count in sorted(counts.items())] + return dict(status=SOURCE_WITH_DIAGNOSTICS if diagnostics else SOURCE_WITNESS, profile=profile, + canonicalSourceLog=authority["path"], canonicalSourceLogSha256=authority["sha256"], + actualLogSha256=matrix.digest(path), diagnostics=diagnostics, actualDiagnosticCount=len(actual), + canonicalDiagnosticCount=sum(historical.values()), timestampPolicy="Clock prefix only; all other text and count limits exact") + + +def hold_gate(root): + if (root / "HOLD").exists(): + raise InterruptedError("HOLD requested; previous server closed, no further boots") + + +def staged_inputs(runtime, profile): + if code_files(runtime) != staging_code(profile[0]): + raise AssertionError("Staged runtime code changed") + expected = sorted((jar.name, matrix.digest(jar)) for jar in profile[2]) + if sorted((jar.name, matrix.digest(jar)) for jar in (runtime / "mods").glob("*.jar")) != expected: + raise AssertionError("Staged runtime mods changed") + + +def staging_code(seed): + return {name: value for name, value in code_files(seed).items() if "/" in name or "installer" not in name} + + +def require_lease(port): + if port != 25574: + raise ValueError("Only an explicitly granted machine-items port25574 lease is accepted") + with socket.socket() as listener: + listener.bind(("127.0.0.1", port)) + + +def run_source(args): + approval = load_plan(args.plan, args.plan_sha256) + protected = [args.plan, approval["routePlan"]] + for source in approval["sources"]: + protected += [source["route"]["source"], source["route"]["sourceManifest"], source["route"]["sourceHashes"], source["canonicalConsole"]["path"]] + root = new_root(args.output, protected) + require_lease(args.lease_port) + receipt = dict(status="RUNNING", scope="NEW_STOPPED_OLD_MACHINE_ITEMS_SOURCE_ONLY", + plan=str(args.plan.resolve()), planSha256=args.plan_sha256.upper(), rows=[]) + root.mkdir(parents=True) + try: + for source in approval["sources"]: + row = source["route"] + canonical = Path(row["source"]) + profile = native117.pinned_profile(Path(row["sourceManifest"]), row["sourceKey"]) + if source_probe(profile) != source["sourceChannel"]: + raise AssertionError("Old source command channel changed") + for case, definition in source["definitions"].items(): + hold_gate(root) + if matrix.fingerprint(canonical) != source["runtimeFingerprint"] or code_files(profile[0]) != source["seedCode"]: + raise AssertionError("Approved canonical source/code changed") + runtime = root / row["sourceKey"] / case + launcher = matrix.stage(runtime, profile) + staged_jars = sorted((jar.name, matrix.digest(jar)) for jar in (runtime / "mods").glob("*.jar")) + expected_code = {name: value for name, value in source["seedCode"].items() if "/" in name or "installer" not in name} + if staged_jars != [tuple(entry) for entry in source["jars"]] or code_files(runtime) != expected_code: + raise AssertionError("Fresh staged old runtime differs from sealed plan") + matrix.copy_fixture(canonical, runtime) + configure(runtime, definition, args.lease_port, destination=False) + player_seed = seed_player(runtime, definition) if definition.get("playerBook") else None + commands = seed_commands(definition) + try: + hold_gate(root) + server = ObservedServer(runtime, args.java8, launcher, "source", source_seeds=commands) + try: + observe_source(server, runtime, "before-seed") + for command in commands: + server.send(command) + captured = observe_source(server, runtime, "source") + source_items(captured, definition) + if definition.get("playerBook"): + observe_player(server, runtime, "source-book", definition, modern=False) + server.send("save-all flush") + finally: + server.close() + staged_inputs(runtime, profile) + source_log = source_log_report(runtime / "console-source.log", row["sourceKey"], source["canonicalConsole"]) + matrix.write_json(runtime / "source-log-diagnostics.json", source_log) + registry = enchantment_registry(runtime / matrix.WORLD / "level.dat") + if registry_sha256(registry) != source["sourceRegistrySha256"]: + raise AssertionError("Stopped source saved Forge enchantment registry changed") + version = "1.10.2" if row["sourceKey"].endswith("110") else "1.12.2" + if "Starting minecraft server version " + version not in (runtime / "console-source.log").read_text(encoding="utf-8", errors="replace"): + raise AssertionError("Missing exact old native Minecraft boot proof") + evidence = stopped_evidence(runtime, "source", definition, captured, modern=False) + receipt["rows"].append(dict(runtime=str(runtime), sourceKey=row["sourceKey"], case=case, + definition=definition, status=source_log["status"], sourceLog=source_log, + sourceRegistrySha256=source["sourceRegistrySha256"], witnessChecks="PASS", + stoppedCleanly=True, rawEvidence=evidence, + playerSeedSha256=player_seed, + worldFingerprint=matrix.fingerprint(runtime / matrix.WORLD), + runtimeFingerprint=matrix.fingerprint(runtime))) + finally: + if matrix.fingerprint(canonical) != source["runtimeFingerprint"]: + raise AssertionError("Protected canonical source changed") + matrix.write_json(root / "source-receipt.json", receipt) + print(row["sourceKey"] + "/" + case + ": " + source_log["status"], flush=True) + receipt["status"] = SOURCE_WITH_DIAGNOSTICS if any(row["status"] == SOURCE_WITH_DIAGNOSTICS for row in receipt["rows"]) else SOURCE_WITNESS + except BaseException as error: + receipt.update(status="FAIL", error=repr(error)) + raise + finally: + matrix.write_json(root / "source-receipt.json", receipt) + return receipt + + +def source_receipt(path, checksum): + receipt = read(checked(path, checksum)) + approval = load_plan(Path(receipt["plan"]), receipt["planSha256"]) + expected = {(source["route"]["sourceKey"], case): definition for source in approval["sources"] + for case, definition in source["definitions"].items()} + if receipt.get("status") not in SOURCE_SUCCESS or receipt.get("scope") != "NEW_STOPPED_OLD_MACHINE_ITEMS_SOURCE_ONLY": + raise AssertionError("Source preparation incomplete or failed") + for source in approval["sources"]: + if matrix.fingerprint(Path(source["route"]["source"])) != source["runtimeFingerprint"]: + raise AssertionError("Protected canonical source changed after generation") + rows = receipt["rows"] + if len(rows) != len(expected) or {(row["sourceKey"], row["case"]) for row in rows} != expected.keys(): + raise AssertionError("Fresh source receipt case coverage changed") + for row in rows: + runtime = Path(row["runtime"]) + if row.get("status") not in SOURCE_SUCCESS or row.get("witnessChecks") != "PASS" or row.get("stoppedCleanly") is not True: + raise AssertionError("Unclosed/unverified source") + if row["definition"] != expected[(row["sourceKey"], row["case"])]: + raise AssertionError("Fresh source definition differs from approved plan") + if matrix.fingerprint(runtime) != row["runtimeFingerprint"] or matrix.fingerprint(runtime / matrix.WORLD) != row["worldFingerprint"]: + raise AssertionError("Fresh stopped source changed") + authority = next(source for source in approval["sources"] if source["route"]["sourceKey"] == row["sourceKey"]) + log_report = source_log_report(runtime / "console-source.log", row["sourceKey"], authority["canonicalConsole"]) + if log_report != row["sourceLog"] or read(runtime / "source-log-diagnostics.json") != log_report or row["status"] != log_report["status"]: + raise AssertionError("Source diagnostic custody/status changed") + if registry_sha256(enchantment_registry(runtime / matrix.WORLD / "level.dat")) != row["sourceRegistrySha256"] or row["sourceRegistrySha256"] != authority["sourceRegistrySha256"]: + raise AssertionError("Saved Forge enchantment registry SHA changed") + document = read(runtime / "migration-source.json") + source_items(document, row["definition"]) + if any((runtime / marker).exists() for marker in ("advantage-connected.allow", "migration-operate.allow")): + raise AssertionError("Stopped old source no longer has its frozen marker state") + expected_commands = ["advprobe snapshot before-seed", *seed_commands(row["definition"]), + "advprobe snapshot source"] + if row["definition"].get("playerBook"): + expected_commands.append("advprobe actors-read source-book") + record = read(runtime / "actors-source-book.json") + if record.get("playerAdapter") is not True or record.get("frozen") is not True: + raise AssertionError("Missing frozen actual old player read") + player_items(matrix.plain(matrix.parse_nbt(record["player"])), row["definition"], modern=False) + player_items(matrix.plain(nbtlib.load(runtime / "machine-items-evidence/source/player.dat").unpack()), row["definition"], modern=False) + expected_commands += ["save-all flush", "stop"] + if read(runtime / "console-source.commands.json") != expected_commands: + raise AssertionError("Source command custody differs from permitted source seeding") + audit_raw(runtime / "machine-items-evidence/source", row["definition"], document, modern=False) + aggregate = SOURCE_WITH_DIAGNOSTICS if any(row["status"] == SOURCE_WITH_DIAGNOSTICS for row in rows) else SOURCE_WITNESS + if receipt["status"] != aggregate: + raise AssertionError("Source aggregate diagnostics status does not match its witnesses") + return receipt + + +def require_power(manifest, power_source, power_sha256): + if not re.fullmatch(r"[0-9a-f]{40}", power_source) or not re.fullmatch(r"[0-9A-Fa-f]{64}", power_sha256): + raise ValueError("Explicit final frozen PowerD source and raw release hash required") + power = [jar for jar in read(manifest)["target117"]["jars"] if jar["id"] == "poweradvantage"] + if (len(power) != 1 or power[0].get("sourceRef") != power_source + or power[0]["sha256"].upper() != power_sha256.upper() or power_sha256.upper() == OLD_POWER_C): + raise AssertionError("Destination is not the explicitly approved replacement for PowerC") + + +def run_destination(args): + fresh = source_receipt(args.source_receipt, args.source_receipt_sha256) + manifest = checked(args.target_manifest, args.target_manifest_sha256) + require_power(manifest, args.power_source, args.power_sha256) + target = native117.pinned_profile(manifest, "target117") + protected = [manifest, args.source_receipt, fresh["plan"], target[0], *target[2]] + protected += [row["runtime"] for row in fresh["rows"]] + root = new_root(args.output, protected) + require_lease(args.lease_port) + root.mkdir(parents=True) + report = dict(status="RUNNING", sourceReceipt=str(args.source_receipt.resolve()), + sourceReceiptSha256=args.source_receipt_sha256.upper(), targetManifest=str(manifest), + targetManifestSha256=matrix.digest(manifest), rows=[], ticks=512, + runnerSha256=matrix.digest(Path(__file__)), + scope="ITEM_PRESERVATION_UNDER_TICKS_NOT_RECIPE_THROUGHPUT") + try: + for row in fresh["rows"]: + source, definition = Path(row["runtime"]), row["definition"] + runtime = root / row["sourceKey"] / row["case"] + launcher = native117.boot(runtime, target, args.java8, args.lease_port) + matrix.copy_fixture(source, runtime) + configure(runtime, definition, args.lease_port) + if definition.get("playerBook"): + shutil.copy2(source / "actor-fixture.json", runtime / "actor-fixture.json") + checks, previous = {}, None + try: + for label in ("converted", "restarted", "runtime-restart"): + hold_gate(root) + staged_inputs(runtime, target) + server = DestinationServer(runtime, args.java8, launcher, label) + try: + current = observe(server, runtime, label) + actual = document_items(current, definition, modern=True) + if definition.get("playerBook"): + observe_player(server, runtime, label + "-book", definition, modern=True) + if previous is not None and actual != previous: + raise AssertionError("Frozen machine item/state idempotence changed") + if label == "restarted": + server.operated = True + worked = observe(server, runtime, "operated", ticks=512) + actual = document_items(worked, definition, modern=True, ticks=512, previous=current) + current = worked + server.send("save-all flush") + finally: + server.close() + staged_inputs(runtime, target) + clean_log(runtime / ("console-" + label + ".log"), modern=True) + stopped_evidence(runtime, label, definition, current, modern=True) + previous = actual + checks[label] = dict(status="PASS", items=actual) + matrix.write_json(runtime / "checks.json", checks) + finally: + if matrix.fingerprint(source) != row["runtimeFingerprint"]: + raise AssertionError("Fresh stopped source mutated during destination") + report["rows"].append(dict(sourceKey=row["sourceKey"], case=row["case"], checks=checks)) + matrix.write_json(root / "results.json", report) + report["status"] = "PASS" + except BaseException as error: + report.update(status="FAIL", error=repr(error)) + raise + finally: + matrix.write_json(root / "results.json", report) + return report + + +def audit(root): + """Recompute from sealed source receipts, retained native snapshots and raw chunks.""" + report = read(root / "results.json") + if report.get("status") != "PASS" or report.get("runnerSha256") != matrix.digest(Path(__file__)): + raise AssertionError("Destination run is incomplete/failed or helper changed") + fresh = source_receipt(Path(report["sourceReceipt"]), report["sourceReceiptSha256"]) + manifest = checked(report["targetManifest"], report["targetManifestSha256"]) + target = native117.pinned_profile(manifest, "target117") + if len(report["rows"]) != len(fresh["rows"]): + raise AssertionError("Destination/source coverage differs") + expected = {(row["sourceKey"], row["case"]) for row in fresh["rows"]} + if {(row["sourceKey"], row["case"]) for row in report["rows"]} != expected: + raise AssertionError("Destination source/case coverage differs") + for row in fresh["rows"]: + runtime = root / row["sourceKey"] / row["case"] + definition = row["definition"] + staged_inputs(runtime, target) + previous = None + for label in ("converted", "restarted", "runtime-restart"): + clean_log(runtime / ("console-" + label + ".log"), modern=True) + current = read(runtime / ("connected-" + label + ".json")) + actual = document_items(current, definition, modern=True) + if definition.get("playerBook"): + record = read(runtime / ("actors-" + label + "-book.json")) + if record.get("playerAdapter") is not True or record.get("frozen") is not True: + raise AssertionError("Missing frozen actual native player read") + player_items(matrix.plain(matrix.parse_nbt(record["player"])), definition, modern=True) + if previous is not None and actual != previous: + raise AssertionError("Raw snapshot restart idempotence failed") + if label == "restarted": + worked = read(runtime / "connected-operated.json") + actual = document_items(worked, definition, modern=True, ticks=512, previous=current) + current = worked + commands = ["advconnected snapshot " + label] + if definition.get("playerBook"): + commands.append("advactors read " + label + "-book") + if label == "restarted": + commands.append("advconnected step 512 operated") + commands += ["save-all flush", "stop"] + if read(runtime / ("console-" + label + ".commands.json")) != commands: + raise AssertionError("Destination command custody contains missing/extra/mutating commands") + folder = runtime / "machine-items-evidence" / label + receipt = read(folder / "receipt.json") + if receipt.get("closedBeforeCapture") is not True or receipt.get("status") != "PASS": + raise AssertionError("Unclosed raw evidence") + for name, checksum in receipt["files"].items(): + if Path(name).is_absolute() or ".." in Path(name).parts: + raise ValueError("Raw evidence path escaped retained root") + checked(folder / name, checksum) + if read(folder / "snapshot.json") != current: + raise AssertionError("Retained snapshot differs from raw probe channel") + audit_raw(folder, definition, current, modern=True) + if definition.get("playerBook"): + player_items(matrix.plain(nbtlib.load(folder / "player.dat").unpack()), definition, modern=True) + previous = actual + return dict(status="PASS", routes=len(fresh["rows"]), boots=len(fresh["rows"]) * 3, + sourceFixtures="UNCHANGED", scope="EXACT_MACHINE_ITEMS_NOT_RECIPE_THROUGHPUT") + + +def parser(): + root = argparse.ArgumentParser(description=__doc__) + modes = root.add_subparsers(dest="mode", required=True) + prepare = modes.add_parser("plan", help="Read-only plan; no process or world staging") + prepare.add_argument("--routes", type=Path, required=True) + prepare.add_argument("--routes-sha256", required=True) + prepare.add_argument("--sources", nargs="+", choices=SOURCES, default=["original110", "legacy112"]) + prepare.add_argument("--output", type=Path, required=True) + replay = modes.add_parser("audit", help="Replay completed native evidence without MC") + replay.add_argument("--run", type=Path, required=True) + replay.add_argument("--output", type=Path, required=True) + for name in ("source", "destination"): + command = modes.add_parser(name) + command.add_argument("--output", type=Path, required=True) + command.add_argument("--java8", type=Path, required=True) + command.add_argument("--lease-port", type=int, choices=(25574,), required=True) + if name == "source": + command.add_argument("--plan", type=Path, required=True) + command.add_argument("--plan-sha256", required=True) + else: + command.add_argument("--source-receipt", type=Path, required=True) + command.add_argument("--source-receipt-sha256", required=True) + command.add_argument("--target-manifest", type=Path, required=True) + command.add_argument("--target-manifest-sha256", required=True) + command.add_argument("--power-source", required=True) + command.add_argument("--power-sha256", required=True) + return root + + +def main(argv=None): + args = parser().parse_args(argv) + if args.mode == "plan": + value = plan(args.routes, args.routes_sha256, args.sources) + protected = [args.routes] + [source["route"]["source"] for source in value["sources"]] + destination = new_root(args.output, protected) + matrix.write_json(destination, value) + print("PLAN ONLY: %d old source boots; %d native destination boots; none launched" % ( + value["sourceBoots"], value["destinationBoots"])) + elif args.mode == "audit": + destination = new_root(args.output, [args.run]) + matrix.write_json(destination, audit(args.run.resolve())) + elif args.mode == "source": + run_source(args) + else: + run_destination(args) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_machine_operations.py b/test-rig/migration/native117_machine_operations.py new file mode 100644 index 00000000..08ebe480 --- /dev/null +++ b/test-rig/migration/native117_machine_operations.py @@ -0,0 +1,271 @@ +"""Run independent finite machine jobs on native world ticks, then inspect a cold restart.""" + +import argparse +import copy +from datetime import datetime, timezone +import json +from pathlib import Path +import shutil +import socket +import struct +import traceback + +import nbtlib + +from matrix import digest, write_json +from native117 import RIG, LAUNCHER, output_path, pinned_profile, require_disjoint +from native117_code import Server +from native117_lifecycle import prepare, read_json, require_native_boot, verify_staged + + +REQUIRED = { + "pickup": {"power-drain", "steam-pump", "electric-pump"}, + "power": {"conveyor/down", "conveyor/up", "conveyor/north", "conveyor/south", "conveyor/west", "conveyor/east", + "conveyor/blocked", "conveyor/item-entity", "conveyor/nbt-preserved", "filter/block", "filter/food", "filter/fuel", "filter/ore", + "filter/plant", "filter/smelt", "filter/inventory-0", "filter/inventory-1", "filter/inventory-2", + "filter/inventory-3", "filter/overflow-0", "filter/overflow-1", "filter/overflow-2", "filter/overflow-3", + "drain/minecraft_water", "drain/basepower_crude_oil", "drain/basepower_refined_oil", + "drain/non-source", "tank/filter-true", "tank/filter-false", "tank/full", "fluid-switch/true", + "fluid-switch/false", "discharge/true", "discharge/false", "conveyor/locked-source", "conveyor/locked-destination", + "still/production", "still/no-fuel", "still/invalid", "still/full", "still/one-space", "still/redstone"}, + "steam": {"coal-production", "coal-invalid-fuel", "coal-dry", "coal-redstone", "oil-production", "oil-dry", + "oil-water-not-fuel", "oil-small-charge", "geothermal-cold", "geothermal-dry", + "electric-production", "electric-no-power", "electric-dry", "electric-insufficient", "geothermal-production", + "furnace-three-lanes", "furnace-no-fuel", "furnace-full-outputs", "furnace-invalid-input", "furnace-without-steam", + "crusher-and-tank-discharge", "crusher-no-power", "crusher-full-outputs", "crusher-invalid-input", + "tank-redstone-stop", "tank-backup-no-cycling", "still-production", "still-no-power", "still-full-output", + "still-one-output-space", "still-incomplete-input", "still-invalid-fluid", + "pump-3x3-intake-lift", "pump-no-power", "pump-insufficient-steam", "pump-full-output", + "elevator-up", "elevator-down", "elevator-no-power", "elevator-scarce", + "musket-last-cartridge", "musket-no-ammo", "musket-powderless", "musket-infinity", + "extra-priority-backup-eligible", "extra-priority-consumer-first", "extra-recoil-0", "extra-recoil-1", + "extra-recoil-5", "extra-explosive-0", "extra-explosive-1", "extra-musket-solid-wall"}, + "electric": {"furnace", "crusher", "oven", "parallel-inputs", "fabricator-recursive", "refinery", "still", + "still-faced-input", "pump", "growth-chain", "growth-scarcity-full", "led-rate", "led-exhaustion", + "hydro-rate", "solar-daylight-rate", "solar-blocklight-rate", "steam-generator-rate", "steam-generator-supply", + "battery-priority", "battery-redstone", "turret-hit", "turret-no-power", "turret-scarce", "turret-occluded", + "turret-owner-credit", "battery-chemistry-lead_acid", "battery-chemistry-nickel_hydride", + "battery-chemistry-alkaline", "battery-chemistry-lithium"}, +} + + +def require_case_set(catalog, group): + rows = catalog.get("cases", []) + ids = {row["id"] for row in rows} + if not ids or len(rows) != len(ids): raise AssertionError("Empty/duplicate operating catalog") + groups = REQUIRED if group == "all" else {group: REQUIRED[group]} + for prefix, names in groups.items(): + missing = {prefix + "/" + name for name in names} - ids + if missing: raise AssertionError("Missing required operating families: " + str(sorted(missing))) + if group != "all" and any(not key.startswith(group + "/") for key in ids): + raise AssertionError("Operating result includes the wrong group") + return ids + + +def validate_operation(catalog, document, group): + expected = require_case_set(catalog, group) + rows = document.get("cases", []) + if len(rows) != len(expected) or {row["id"] for row in rows} != expected: + raise AssertionError("Incomplete operating result; a passing subset is forbidden") + if document.get("held") is not True: raise AssertionError("Final measurement is not quiescent") + failures = [] + for row in rows: + assertions = row.get("assertions", []) + measured = [check for check in assertions if check.get("phase") == "verify"] + if (row.get("status") != "PASS" or row.get("verifiedAssertions", 0) < 1 or not measured + or any(check.get("status") != "PASS" for check in assertions) + or row.get("finishTick", 0) - row.get("startTick", 0) < row.get("requiredTicks", 1) + or not row.get("before") or not row.get("after")): + failures.append({"id": row["id"], "failure": row.get("failure", "Missing/failed measured natural-tick outcome")}) + return dict(status="FAIL" if failures or document.get("status") != "PASS" else "PASS", cases=len(rows), failures=failures) + + +def normalized_rows(rows): + result = {} + for row in rows: + key = (row["x"], row["y"], row["z"]) + if key in result: raise AssertionError("Duplicate persisted position") + value = {name: row[name] for name in ("block", "state")} + if "nbt" in row: value["nbt"] = saved_tile_nbt(nbtlib.parse_nbt(row["nbt"])) + result[key] = value + return result + + +def typed_nbt(value): + if isinstance(value, nbtlib.Compound): return {key: typed_nbt(tag) for key, tag in value.items()} + if isinstance(value, nbtlib.List): return ["List", value.subtype.tag_id, [typed_nbt(tag) for tag in value]] + plain = value.unpack() + if isinstance(value, nbtlib.Float): + # SNBT uses a shortest decimal; the saved Float tag stores IEEE binary32. + plain = struct.unpack('>f', struct.pack('>f', plain))[0] + if hasattr(plain, "tolist"): plain = plain.tolist() + return [type(value).__name__, plain] + + +def saved_tile_nbt(value): + result = typed_nbt(value) + for key in ("Tank", "InputTank", "TankIn", "TankOut", "FuelTank", "WaterTank"): + tank = result.get(key) + if (isinstance(tank, dict) and set(tank) == {"Amount", "FluidName"} + and tank["Amount"] == ["Int", 0] and tank["FluidName"][0] == "String" and tank["FluidName"][1]): + # FluidTank.readFromNBT drops a zero-volume fluid identity. No material or filter is stored here. + result[key] = {"Empty": ["Byte", 1]} + return result + + +def saved_actor_nbt(value): + value = copy.deepcopy(value) + # AttributeMap serializes a HashMap. Attribute names, not iteration order, + # define identity; retain exact typed values and every other list's order. + def visit(entity): + if "Attributes" in entity: + attributes = entity["Attributes"] + if not isinstance(attributes, nbtlib.List) or any( + not isinstance(row, nbtlib.Compound) or not isinstance(row.get("Name"), nbtlib.String) + for row in attributes): + raise AssertionError("Malformed native actor attributes") + names = [str(row["Name"]) for row in attributes] + if len(names) != len(set(names)): raise AssertionError("Duplicate native actor attribute identity") + entity["Attributes"] = nbtlib.List[nbtlib.Compound](sorted(attributes, key=lambda row: str(row["Name"]))) + for passenger in entity.get("Passengers", []): visit(passenger) + visit(value) + return typed_nbt(value) + + +def validate_restart(fixture, inspected): + before = fixture.get("cases", []) + after = inspected.get("cases", []) + if (not before or len(after) != len(before) or {r["id"] for r in before} != {r["id"] for r in after} + or len({r["id"] for r in after}) != len(after) or inspected.get("held") is not True + or inspected.get("constructionCallsThisProcess") != 0): + raise AssertionError("Missing/rebuilt/unfrozen full-process restart evidence") + actual = {row["id"]: normalized_rows(row["positions"]) for row in after} + failures = [] + for row in before: + expected = normalized_rows(row["positions"]) + if actual[row["id"]] != expected: + changed = [dict(pos=key, expected=value, actual=actual[row["id"]].get(key)) for key, value in expected.items() + if actual[row["id"]].get(key) != value] + extra = set(actual[row["id"]]) - set(expected) + failures.append(dict(id=row["id"], differences=changed, extraPositions=sorted(extra))) + actual_case = next(case for case in after if case["id"] == row["id"]) + def actors(case): + rows = case.get("entities", []) + result = {actor["uuid"]: saved_actor_nbt(nbtlib.parse_nbt(actor["nbt"])) if "nbt" in actor else actor for actor in rows} + if len(result) != len(rows): raise AssertionError("Duplicate persisted actor UUID") + return result + if actors(row) != actors(actual_case): failures.append(dict(id=row["id"], difference="Saved test actors changed across restart")) + return dict(status="FAIL" if failures else "PASS", cases=len(before), failures=failures, + comparison="Full block/state/typed-NBT equality including target-only keys; only exact zero-volume named tanks and unique actor-attribute ordering canonicalized; no expected-state writes") + + +def pin(args): + base = read_json(args.base) + destination = args.output.resolve() + if destination.exists(): raise FileExistsError(destination) + destination.mkdir(parents=True) + replacements = { + "basepower": RIG.parent / "build/libs/basePower-2.4.9.117011.jar", + "basesteam": RIG.parent.parent / "SteamAdvantage/SteamAdvantage/build/libs/baseSteam-2.2.10.117011.jar", + "baseelectric": RIG.parent.parent / "ElectricAdvantage/ElectricAdvantage/build/libs/baseElectric-2.2.10.117011.jar", + "advantageworkstest": RIG / "build/gradle/libs/AdvantageWorksTestHarness-1.1.0.117011.jar", + } + target = copy.deepcopy(base["target117"]) + for jar in target["jars"]: + source = replacements.get(jar["id"], Path(jar["path"])) + checksum = digest(source) + folder = destination / "artifacts" / checksum + folder.mkdir(parents=True, exist_ok=True) + path = folder / source.name + if path.exists(): raise FileExistsError(path) + shutil.copy2(source, path) + if digest(path) != checksum: raise AssertionError("Archive copy differs") + jar.update(path=str(path), originPath=str(source), sha256=checksum) + if jar["id"] in replacements: jar["sourceRef"] = "LOCAL_OPERATING_CANDIDATE_" + args.label + target["qualificationStatus"] = "UNQUALIFIED_OPERATING_CANDIDATE" + target["scope"] = args.label + path = destination / "inputs.json" + write_json(path, {"target117": target, "baseManifest": str(args.base.resolve()), "baseSha256": digest(args.base)}) + pinned_profile(path, "target117") + print(str(path), flush=True) + return 0 + + +def run(args): + output = output_path(args.output) + target = pinned_profile(args.manifest.resolve(), "target117") + for protected in (args.manifest, target[0], *target[2]): require_disjoint(output, protected) + with socket.socket() as listener: listener.bind(("127.0.0.1", args.port)) + launcher = prepare(output, target, [], args.port) + for marker in ("advantage-machine-operation.allow", "migration-operate.allow"): (output / marker).touch() + report = dict(status="RUNNING", group=args.group, manifest=str(args.manifest.resolve()), manifestSha256=digest(args.manifest), + startedAtUtc=datetime.now(timezone.utc).isoformat(), phases={}, + scope="Finite natural-tick machine jobs, negative controls, then frozen strict full-process restart; no rebuilding") + helpers = [Path(__file__), Path(__file__).with_name("native117_lifecycle.py"), + Path(__file__).with_name("native117_code.py"), Path(__file__).with_name("native117.py")] + report["protocolSha256"] = {str(path): digest(path) for path in helpers} + for path in helpers: + destination = output / "protocol-sources" / path.name + destination.parent.mkdir(exist_ok=True) + shutil.copy2(path, destination) + server = None + try: + server = Server(output, args.java8, launcher, "operate") + server.send("gamerule doMobSpawning false") + server.send("gamerule randomTickSpeed 0") + server.send("gamerule doWeatherCycle false") + server.send("weather clear") + server.send("advmachines run " + args.group + " measured") + server.wait("ADVMACHINES COMPLETE measured", 1500) + catalog = read_json(output / "advantage-works-results/machine-catalog-measured.json") + result = read_json(output / "advantage-works-results/machine-operation-measured.json") + report["phases"]["operation"] = validate_operation(catalog, result, args.group) + server.send("save-all flush") + server.close(); server = None + require_native_boot(output / "console-operate.log") + fixture = read_json(output / "machine-operation-fixture.json") + report["fixtureSha256"] = digest(output / "machine-operation-fixture.json") + from native117_machine_saved import audit as audit_saved + report["phases"]["saved-before-restart"] = audit_saved(output, fixture, output / "raw-machine-save-before-restart") + write_json(output / "native117-machine-operation-results.json", report) + server = Server(output, args.java8, launcher, "restart") + server.send("advmachines inspect restarted") + server.wait("ADVMACHINES INSPECTED restarted", 360) + observed = read_json(output / "advantage-works-results/machine-inspection-restarted.json") + report["phases"]["restart"] = validate_restart(fixture, observed) + server.send("save-all flush") + server.close(); server = None + require_native_boot(output / "console-restart.log") + report["phases"]["saved-after-restart"] = audit_saved(output, observed, output / "raw-machine-save-after-restart") + if digest(output / "machine-operation-fixture.json") != report["fixtureSha256"]: raise AssertionError("Fixture authority changed across restart") + verify_staged(output, target) + report["status"] = "PASS" if all(row["status"] == "PASS" for row in report["phases"].values()) else "FAIL" + except BaseException as error: + report.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + finally: + if server: + try: server.close() + except BaseException as error: report.update(status="FAIL", shutdownError=str(error)) + report["protocolUnchanged"] = {str(path): digest(path) for path in helpers} == report["protocolSha256"] + if not report["protocolUnchanged"]: report.update(status="FAIL", protocolError="Operating protocol changed during the run") + report["completedAtUtc"] = datetime.now(timezone.utc).isoformat() + write_json(output / "native117-machine-operation-results.json", report) + print(report["status"] + ": " + str(output), flush=True) + return int(report["status"] != "PASS") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + p = commands.add_parser("pin") + p.add_argument("--base", type=Path, required=True); p.add_argument("--output", type=Path, required=True) + p.add_argument("--label", required=True) + r = commands.add_parser("run") + r.add_argument("--manifest", type=Path, required=True); r.add_argument("--output", type=Path, required=True) + r.add_argument("--java8", type=Path, required=True); r.add_argument("--port", type=int, required=True) + r.add_argument("--group", choices=("all", *REQUIRED), default="all") + args = parser.parse_args() + return pin(args) if args.command == "pin" else run(args) + + +if __name__ == "__main__": raise SystemExit(main()) diff --git a/test-rig/migration/native117_machine_saved.py b/test-rig/migration/native117_machine_saved.py new file mode 100644 index 00000000..9551bb4e --- /dev/null +++ b/test-rig/migration/native117_machine_saved.py @@ -0,0 +1,120 @@ +"""Corroborate frozen machine snapshots against stopped native terrain files.""" + +import gzip +import io +import json +from pathlib import Path +import re +import zlib + +import nbtlib + +from matrix import WORLD, digest, write_json +from native117_format import decode_palette +from transformer_order import read_region + + +def chunk(payload): + if len(payload) < 6 or int.from_bytes(payload[:4], 'big') != len(payload) - 4: + raise ValueError('Invalid terrain payload length') + decoders = {1: gzip.decompress, 2: zlib.decompress, 3: lambda value: value} + if payload[4] not in decoders: + raise ValueError('Unsupported terrain compression') + root = nbtlib.File.parse(io.BytesIO(decoders[payload[4]](payload[5:]))) + if int(root.get('DataVersion', -1)) != 2730: + raise ValueError('Machine save must be native 1.17.1 terrain') + return root['Level'] + + +def state(row): + match = re.fullmatch(r'Block\{([^}]+)\}(?:\[([^]]*)\])?', row['state']) + if match is None or match[1] != row['block']: + raise ValueError('Malformed machine state snapshot') + properties = {} + for part in match[2].split(',') if match[2] else (): + key, value = part.split('=', 1) + if key in properties: + raise ValueError('Duplicate state property') + properties[key] = value + return dict(Name=match[1], Properties=properties) + + +def compare_chunk(rows, data): + from native117_machine_operations import saved_tile_nbt + sections = {} + for section in data['Sections']: + y = int(section['Y']) + if y in sections: + raise ValueError('Duplicate saved terrain section') + sections[y] = decode_palette(section['Palette'].unpack(), section.get('BlockStates', []).tolist() + if isinstance(section.get('BlockStates'), nbtlib.LongArray) else []) if 'Palette' in section else None + tiles = {} + for tile in data['TileEntities']: + pos = tuple(int(tile[key]) for key in ('x', 'y', 'z')) + if pos in tiles: + raise ValueError('Duplicate saved tile position') + tiles[pos] = tile + failures = [] + for row in rows: + x, y, z = pos = tuple(row[key] for key in ('x', 'y', 'z')) + palette = sections.get(y // 16) + actual = palette[((y & 15) << 8) | ((z & 15) << 4) | (x & 15)] if palette else {'Name': 'minecraft:air'} + actual = dict(Name=actual['Name'], Properties=actual.get('Properties', {})) + expected = state(row) + before = saved_tile_nbt(nbtlib.parse_nbt(row['nbt'])) if 'nbt' in row else None + saved = tiles.get(pos) + if saved is not None: + # LevelChunk.getBlockEntityNbtForSaving adds this storage-only flag + # after BlockEntity.save. Packed or wrongly typed values are not equivalent. + if not isinstance(saved.get('keepPacked'), nbtlib.Byte) or int(saved['keepPacked']) != 0: + raise ValueError('Expected the native unpacked-tile storage marker') + saved = nbtlib.Compound({key: value for key, value in saved.items() if key != 'keepPacked'}) + after = saved_tile_nbt(saved) if saved is not None else None + if actual != expected or before != after: + failures.append(dict(pos=pos, expectedState=expected, savedState=actual, + expectedNbt=before, savedNbt=after)) + return failures + + +def audit(runtime, fixture, evidence): + """Read only after shutdown; retain source payloads even when comparison fails.""" + runtime, evidence = Path(runtime).resolve(), Path(evidence).resolve() + world = runtime / WORLD + if evidence.is_relative_to(world) or world.is_relative_to(evidence): + raise ValueError('Evidence overlaps the world') + evidence.mkdir(parents=True, exist_ok=False) + report = dict(status='FAIL', cases=len(fixture['cases']), files={}, failures=[], + scope='Stopped raw terrain state and typed machine NBT; no expected-state writes') + write_json(evidence / 'snapshot.json', fixture) + regions, chunks = {}, {} + try: + for case in fixture['cases']: + grouped = {} + for row in case['positions']: + key = row['x'] // 16, row['z'] // 16 + grouped.setdefault(key, []).append(row) + for (cx, cz), rows in grouped.items(): + region = cx // 32, cz // 32 + if region not in regions: + path = world / 'region' / ('r.%d.%d.mca' % region) + checksum = digest(path) + regions[region] = read_region(path)[1] + report['files'][str(path)] = checksum + if (cx, cz) not in chunks: + payload = regions[region][(cx & 31) + (cz & 31) * 32] + (evidence / ('%d.%d.chunk' % (cx, cz))).write_bytes(payload) + chunks[cx, cz] = chunk(payload) + if (int(chunks[cx, cz]['xPos']), int(chunks[cx, cz]['zPos'])) != (cx, cz): + raise AssertionError('Saved chunk coordinate differs from its region slot') + failures = compare_chunk(rows, chunks[cx, cz]) + if failures: + report['failures'].append(dict(id=case['id'], differences=failures)) + if any(digest(Path(path)) != checksum for path, checksum in report['files'].items()): + raise AssertionError('Terrain changed during stopped audit') + report['status'] = 'FAIL' if report['failures'] else 'PASS' + except BaseException as error: + report['error'] = type(error).__name__ + ': ' + str(error) + raise + finally: + write_json(evidence / 'receipt.json', report) + return report diff --git a/test-rig/migration/native117_mod_inventory.py b/test-rig/migration/native117_mod_inventory.py new file mode 100644 index 00000000..97e0f3b9 --- /dev/null +++ b/test-rig/migration/native117_mod_inventory.py @@ -0,0 +1,206 @@ +"""Read-only six-mod stage inventory validation, separate from frozen auditors. + +The caller must supply an independently pinned manifest SHA256 and keep the +runtime closed and unchanged. Rechecks detect observed changes; this is NOT an +atomic filesystem snapshot, server-exit proof, or gameplay qualification. +""" + +import hashlib +import json +import ntpath +import os +from pathlib import Path +import re +import stat + + +EXPECTED_IDS = frozenset(("advantageworkstest", "basepower", "basesteam", + "baseelectric", "basemetals", "orespawn")) +_REPARSE = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400) + + +class InventoryError(ValueError): + pass + + +def _sha(value): + if not isinstance(value, str) or re.fullmatch(r"[a-fA-F0-9]{64}", value) is None: + raise InventoryError("Explicit SHA256 pin required") + return value.upper() + + +def _path(value, parent=None): + raw = os.fspath(value) + if not isinstance(raw, str) or not raw or "\0" in raw or ".." in raw.replace("\\", "/").split("/"): + raise InventoryError("Unsafe inventory path") + path = Path(raw) + drive, tail = ntpath.splitdrive(raw) + if drive and not path.is_absolute(): + raise InventoryError("Drive-relative inventory path") + if ":" in tail: + raise InventoryError("Alternate stream/device path") + if not path.is_absolute(): + if parent is None: + raise InventoryError("Absolute inventory path required") + path = parent / path + return Path(os.path.abspath(path)) + + +def _plain_path(path, directory=False): + for ancestor in (*reversed(path.parents), path): + metadata = ancestor.lstat() + if stat.S_ISLNK(metadata.st_mode) or getattr(metadata, "st_file_attributes", 0) & _REPARSE: + raise InventoryError("Link/reparse inventory path: " + str(ancestor)) + metadata = path.lstat() + valid = stat.S_ISDIR(metadata.st_mode) if directory else stat.S_ISREG(metadata.st_mode) + if not valid: + raise InventoryError("Wrong inventory entry type: " + str(path)) + return metadata + + +def _stamp(metadata): + return (metadata.st_dev, metadata.st_ino, metadata.st_size, + metadata.st_mtime_ns, metadata.st_ctime_ns) + + +def _path_stamp(metadata): + # Windows path and handle stat can disagree on ctime; compare it handle-to-handle. + return (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns) + + +def _read(path, retain=False): + before = _plain_path(path) + digest, chunks = hashlib.sha256(), [] + with path.open("rb") as stream: + opened = os.fstat(stream.fileno()) + if _path_stamp(before) != _path_stamp(opened): + raise InventoryError("Inventory file changed before read: " + str(path)) + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + if retain: + chunks.append(chunk) + after = os.fstat(stream.fileno()) + if _stamp(opened) != _stamp(after) or _path_stamp(opened) != _path_stamp(_plain_path(path)): + raise InventoryError("Inventory file changed during read: " + str(path)) + return digest.hexdigest().upper(), b"".join(chunks), before.st_size + + +def _object(pairs): + value = {} + for key, child in pairs: + if key in value: + raise InventoryError("Duplicate JSON key: " + key) + value[key] = child + return value + + +def _constant(value): + raise InventoryError("Non-finite JSON constant: " + value) + + +def _json(raw): + try: + return json.loads(raw.decode("utf-8-sig"), object_pairs_hook=_object, parse_constant=_constant) + except (UnicodeError, json.JSONDecodeError) as error: + raise InventoryError("Invalid inventory JSON") from error + + +def _name(value): + if (not isinstance(value, str) or not value or value != value.strip() + or any(ord(c) < 32 or c in '/\\:<>"|?*' for c in value) + or not value.endswith(".jar") or value.startswith(".") + or value.split(".", 1)[0].upper() in {"CON", "PRN", "AUX", "NUL", "CLOCK$"} + or re.fullmatch(r"(?:COM|LPT)[1-9]", value.split(".", 1)[0], re.I)): + raise InventoryError("Expected a flat safe jar filename: " + repr(value)) + return value + + +def _expected(manifest, parent): + if not isinstance(manifest, dict) or not isinstance(manifest.get("target117"), dict): + raise InventoryError("Missing sealed target117 manifest") + rows = manifest["target117"].get("jars") + if not isinstance(rows, list) or len(rows) != len(EXPECTED_IDS): + raise InventoryError("Require the complete six-mod native117 target") + expected, ids, folded = {}, set(), set() + required = {"id", "version", "path", "sha256"} + for row in rows: + if (not isinstance(row, dict) or not required <= set(row) + or set(row) - required - {"originPath", "sourceRef"}): + raise InventoryError("Malformed sealed jar record") + identity = row["id"] + if not isinstance(identity, str) or identity not in EXPECTED_IDS or identity in ids: + raise InventoryError("Unknown or duplicate sealed mod identity") + if not isinstance(row["version"], str) or not row["version"].endswith(".117011"): + raise InventoryError("Non-native sealed mod version") + source = _path(row["path"], parent) + name = _name(source.name) + if name.casefold() in folded: + raise InventoryError("Duplicate/case-colliding sealed jar filename") + ids.add(identity) + folded.add(name.casefold()) + expected[name] = dict(id=identity, source=source, sha256=_sha(row["sha256"])) + if ids != EXPECTED_IDS: + raise InventoryError("Incomplete sealed six-mod identity set") + return expected + + +def _physical(mods): + _plain_path(mods, directory=True) + names, folded = set(), set() + for entry in mods.iterdir(): + name = _name(entry.name) + _plain_path(entry) + if name.casefold() in folded: + raise InventoryError("Duplicate/case-colliding physical mod filename") + names.add(name) + folded.add(name.casefold()) + return names + + +def _validate(runtime, manifest_path, manifest_sha256): + runtime, manifest_path = _path(runtime), _path(manifest_path) + _plain_path(runtime, directory=True) + manifest_hash, raw_manifest, _ = _read(manifest_path, retain=True) + if manifest_hash != _sha(manifest_sha256): + raise InventoryError("Sealed manifest SHA256 mismatch") + expected = _expected(_json(raw_manifest), manifest_path.parent) + ledger_path, mods = runtime / "input-jars.json", runtime / "mods" + ledger_hash, raw_ledger, _ = _read(ledger_path, retain=True) + rows = _json(raw_ledger) + if not isinstance(rows, list) or len(rows) != len(expected): + raise InventoryError("Ledger does not contain exactly the sealed six jars") + ledger, folded = {}, set() + for row in rows: + if not isinstance(row, dict) or set(row) != {"file", "source", "sha256"}: + raise InventoryError("Malformed native stage ledger row") + name = _name(row["file"]) + if name.casefold() in folded: + raise InventoryError("Duplicate/case-colliding ledger jar") + folded.add(name.casefold()) + if name not in expected or _sha(row["sha256"]) != expected[name]["sha256"]: + raise InventoryError("Ledger jar/hash differs from sealed manifest: " + name) + if _path(row["source"]) != expected[name]["source"]: + raise InventoryError("Ledger source differs from sealed manifest: " + name) + ledger[name] = row + if set(ledger) != set(expected) or _physical(mods) != set(expected): + raise InventoryError("Physical mods, ledger and sealed manifest jar sets differ") + observations = [] + for name, pin in sorted(expected.items()): + source_hash, _, _ = _read(pin["source"]) + actual, _, size = _read(mods / name) + if source_hash != pin["sha256"] or actual != pin["sha256"]: + raise InventoryError("Actual jar bytes differ from sealed SHA256: " + name) + observations.append(dict(file=name, id=pin["id"], sha256=actual, bytes=size)) + if (_physical(mods) != set(expected) or _read(manifest_path)[0] != manifest_hash + or _read(ledger_path)[0] != ledger_hash): + raise InventoryError("Inventory inputs changed during validation") + return dict(status="PASS", scope="EXACT_SIX_MOD_INVENTORY_ONLY", atomicSnapshot=False, + manifestSha256=manifest_hash, ledgerSha256=ledger_hash, jars=observations) + + +def validate_mod_inventory(runtime, manifest_path, manifest_sha256): + """Require sealed sources, ledger and real mods to match; never writes inputs.""" + try: + return _validate(runtime, manifest_path, manifest_sha256) + except (OSError, TypeError) as error: + raise InventoryError("Unreadable or malformed native inventory input") from error diff --git a/test-rig/migration/native117_observation_plan.py b/test-rig/migration/native117_observation_plan.py new file mode 100644 index 00000000..6ecc2e2c --- /dev/null +++ b/test-rig/migration/native117_observation_plan.py @@ -0,0 +1,227 @@ +"""Local, worldless CoreMods observation plans; never edits shared seeds or runners. + +Derivation is from a pinned original inventory and a pinned validated bridge build, +not from arbitrary current runtime bytes. Parent integration calls seal_runtime +after its existing observer copy and before its unchanged CodeCustody/Server gate. +""" + +import argparse +import copy +import json +import os +from pathlib import Path +import re +import shutil +import zipfile + +from matrix import digest, write_json +from native117_code import INVENTORY, code_files, verify_code +from transformer_order import validate_patch + + +COREMODS = "libraries/net/minecraftforge/coremods/5.0.1/coremods-5.0.1.jar" +CUSTODY = "observation-custody" +BRIDGE_SOURCE = Path(__file__).parent / "native117-transformer-order-probe/TransformerOrderBridge.java" + + +def read(path): + return json.loads(Path(path).read_text(encoding="utf-8")) + + +def pinned(path, sha256): + path = Path(path).resolve() + if not re.fullmatch(r"[0-9A-Fa-f]{64}", sha256) or digest(path) != sha256.upper(): + raise AssertionError("Changed pinned observation input: " + str(path)) + return path + + +def disjoint(output, protected): + output, protected = Path(output).resolve(), Path(protected).resolve() + if output.is_relative_to(protected) or protected.is_relative_to(output): + raise ValueError("Observation output overlaps a protected input: " + str(protected)) + + +def validate_inventory(value): + if (set(value) != {"files", "format", "minecraft", "forge"} or type(value["format"]) is not int + or value["format"] != 1 or value["minecraft"] != "1.17.1" or value["forge"] != "37.1.1" + or not isinstance(value["files"], dict) or not value["files"]): + raise ValueError("Expected exact nonempty code inventory schema") + folded = set() + for name, metadata in value["files"].items(): + path = Path(name) + if (path.is_absolute() or "\\" in name or ":" in name or any(part in ("", ".", "..") for part in name.split("/")) + or name.lower() in folded + or not (name.startswith("libraries/") or (len(path.parts) == 1 and name.endswith(".jar")))): + raise ValueError("Unsafe, duplicate or non-code inventory path: " + name) + folded.add(name.lower()) + if (set(metadata) != {"sha256", "size"} or not re.fullmatch(r"[0-9A-F]{64}", metadata["sha256"]) + or type(metadata["size"]) is not int or metadata["size"] <= 0): + raise ValueError("Invalid code inventory metadata: " + name) + if COREMODS not in value["files"]: + raise ValueError("Pinned original CoreMods entry missing") + + +def make_plan(seed, inventory_sha256, observer, observer_sha256, build_proof, build_proof_sha256, + bridge_source=BRIDGE_SOURCE): + seed = Path(seed).resolve() + receipt = pinned(seed / INVENTORY, inventory_sha256) + baseline = read(receipt) + validate_inventory(baseline) + verify_code(seed, baseline) + observer = pinned(observer, observer_sha256) + proof_path = pinned(build_proof, build_proof_sha256) + proof = read(proof_path) + bridge_source = pinned(bridge_source, proof["sourceSha256"]) + for jar in (seed / COREMODS, observer): + with zipfile.ZipFile(jar) as archive: + names = archive.namelist() + if len(names) != len(set(names)): + raise ValueError("Duplicate ZIP entry in observation input") + patch = validate_patch(seed / COREMODS, observer) + if any(proof.get(key) != value for key, value in patch.items()): + raise AssertionError("Pinned bridge receipt differs from actual exact-entry validation") + if not isinstance(proof.get("dependencies"), dict) or not proof["dependencies"]: + raise ValueError("Bridge build dependency provenance missing") + dependencies = {} + for name, checksum in proof["dependencies"].items(): + path = pinned(name, checksum) + relative = path.relative_to(seed).as_posix() + if relative not in baseline["files"] or baseline["files"][relative]["sha256"] != checksum: + raise AssertionError("Bridge dependency is not in the original sealed code inventory") + dependencies[relative] = checksum + derived = copy.deepcopy(baseline) + derived["files"][COREMODS] = dict(sha256=digest(observer), size=observer.stat().st_size) + changed = [name for name in baseline["files"] if baseline["files"][name] != derived["files"][name]] + if changed != [COREMODS] or set(derived["files"]) != set(baseline["files"]): + raise AssertionError("Observation inventory must replace exactly the original CoreMods entry") + verify_code(seed, baseline) + pinned(receipt, inventory_sha256) + return dict(format=1, kind="DERIVED_COREMODS_OBSERVATION_ONLY", seed=str(seed), + baselineReceiptSha256=inventory_sha256.upper(), baselineInventory=baseline, + observerJar=str(observer), observerJarSha256=observer_sha256.upper(), + buildProof=str(proof_path), buildProofSha256=build_proof_sha256.upper(), patch=patch, + bridgeSource=str(bridge_source), bridgeSourceSha256=digest(bridge_source), dependencies=dependencies, + derivedInventory=derived, codeFileCount=len(baseline["files"]), changedPaths=changed, + helperSha256=digest(Path(__file__)), scope="No worlds, candidate mods, server process or gameplay qualification") + + +def load_plan(path, sha256): + path = pinned(path, sha256) + plan = read(path) + actual = make_plan(plan["seed"], plan["baselineReceiptSha256"], plan["observerJar"], + plan["observerJarSha256"], plan["buildProof"], plan["buildProofSha256"], plan["bridgeSource"]) + if plan != actual: + raise AssertionError("Observation plan differs from its pinned derivation") + return path, plan + + +def seal_runtime(runtime, plan_path, plan_sha256): + """Seal only a stopped/disposable staged baseline with the one approved replacement.""" + plan_path, plan = load_plan(plan_path, plan_sha256) + runtime = Path(runtime).resolve() + seed = Path(plan["seed"]) + for protected in (seed, plan_path, plan["observerJar"], plan["buildProof"], plan["bridgeSource"]): + disjoint(runtime, protected) + current_receipt = runtime / INVENTORY + if current_receipt.is_symlink() or not current_receipt.resolve().is_relative_to(runtime): + raise ValueError("Runtime inventory must be a local regular file") + if os.path.samefile(current_receipt, seed / INVENTORY): + raise ValueError("Runtime inventory aliases the protected baseline receipt") + original_bytes = current_receipt.read_bytes() + if digest(current_receipt) != plan["baselineReceiptSha256"] or read(current_receipt) != plan["baselineInventory"]: + raise AssertionError("Runtime must still carry the exact original baseline receipt") + if code_files(runtime) != plan["derivedInventory"]["files"]: + raise AssertionError("Runtime code is not exactly baseline plus the approved CoreMods replacement") + try: + verify_code(runtime) + except AssertionError as error: + original_rejection = str(error) + else: + raise AssertionError("Original baseline gate unexpectedly accepted the observer replacement") + custody = runtime / CUSTODY + custody.mkdir(exist_ok=False) + (custody / "baseline-inventory.json").write_bytes(original_bytes) + shutil.copy2(plan_path, custody / "observation-plan.json") + temporary = custody / "derived-inventory.tmp.json" + write_json(temporary, plan["derivedInventory"]) + os.replace(temporary, current_receipt) + verify_code(runtime, plan["derivedInventory"]) + verify_code(seed, plan["baselineInventory"]) + pinned(seed / INVENTORY, plan["baselineReceiptSha256"]) + proof = dict(status="PASS_CODE_CUSTODY_ONLY_NO_MC", planSha256=plan_sha256.upper(), + baselineReceiptSha256=plan["baselineReceiptSha256"], derivedReceiptSha256=digest(current_receipt), + baselineGateBeforeReseal="REJECTED_AS_EXPECTED", baselineRejection=original_rejection, + codeFileCount=plan["codeFileCount"], changedPaths=[COREMODS], observerJarSha256=plan["observerJarSha256"]) + write_json(custody / "receipt.json", proof) + return audit_runtime(runtime, plan_path, plan_sha256) + + +def audit_runtime(runtime, plan_path, plan_sha256): + _, plan = load_plan(plan_path, plan_sha256) + runtime = Path(runtime).resolve() + custody = runtime / CUSTODY + proof = read(custody / "receipt.json") + pinned(custody / "baseline-inventory.json", plan["baselineReceiptSha256"]) + pinned(custody / "observation-plan.json", plan_sha256) + pinned(runtime / INVENTORY, proof["derivedReceiptSha256"]) + if read(runtime / INVENTORY) != plan["derivedInventory"]: + raise AssertionError("Derived inventory differs from the observation plan") + if (proof.get("status") != "PASS_CODE_CUSTODY_ONLY_NO_MC" or proof.get("changedPaths") != [COREMODS] + or proof.get("codeFileCount") != plan["codeFileCount"] or proof.get("planSha256") != plan_sha256.upper() + or proof.get("baselineReceiptSha256") != plan["baselineReceiptSha256"] + or proof.get("observerJarSha256") != plan["observerJarSha256"]): + raise AssertionError("Observation custody receipt changed") + verify_code(runtime, plan["derivedInventory"]) + return proof + + +def stage_probe(output, plan_path, plan_sha256): + """Create a NEW code-only copy and exercise the unchanged verifier without booting.""" + plan_path, plan = load_plan(plan_path, plan_sha256) + output = Path(output).resolve() + for protected in (plan["seed"], plan_path, plan["observerJar"], plan["buildProof"], plan["bridgeSource"]): + disjoint(output, protected) + output.mkdir(parents=True, exist_ok=False) + for name in plan["baselineInventory"]["files"]: + destination = output / name + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(Path(plan["seed"]) / name, destination) + shutil.copy2(Path(plan["seed"]) / INVENTORY, output / INVENTORY) + verify_code(output, plan["baselineInventory"]) + shutil.copy2(plan["observerJar"], output / COREMODS) + return seal_runtime(output, plan_path, plan_sha256) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + modes = parser.add_subparsers(dest="mode", required=True) + derive = modes.add_parser("derive") + for name in ("seed", "observer", "build-proof", "bridge-source", "output"): + derive.add_argument("--" + name, type=Path, required=True) + for name in ("inventory-sha256", "observer-sha256", "build-proof-sha256"): + derive.add_argument("--" + name, required=True) + for name in ("stage-probe", "seal-runtime", "audit"): + command = modes.add_parser(name) + command.add_argument("--plan", type=Path, required=True) + command.add_argument("--plan-sha256", required=True) + command.add_argument("--output" if name == "stage-probe" else "--runtime", type=Path, required=True) + args = parser.parse_args(argv) + if args.mode == "derive": + plan = make_plan(args.seed, args.inventory_sha256, args.observer, args.observer_sha256, + args.build_proof, args.build_proof_sha256, args.bridge_source) + output = args.output.resolve() + for protected in (args.seed, args.observer, args.build_proof, args.bridge_source): + disjoint(output, protected) + if output.exists(): + raise FileExistsError(output) + write_json(output, plan) + print("DERIVED PLAN ONLY: " + str(plan["codeFileCount"]) + " code files; one replacement; no MC") + else: + operation = {"stage-probe": stage_probe, "seal-runtime": seal_runtime, "audit": audit_runtime}[args.mode] + proof = operation(args.output if args.mode == "stage-probe" else args.runtime, args.plan, args.plan_sha256) + print(json.dumps(proof, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_oil.py b/test-rig/migration/native117_oil.py new file mode 100644 index 00000000..a7cf8ece --- /dev/null +++ b/test-rig/migration/native117_oil.py @@ -0,0 +1,368 @@ +"""Qualify native117 bucket/drain/pipe transport in a disposable pinned runtime. + +The original oil_transfer geometry and quantity/identity oracles are unchanged. +Only the initial empty construction and two native bucket placements may mutate +the fixture; subsequent boots must retain exactly 1000, then 2000 mB per lane. +""" + +import argparse +from copy import deepcopy +from datetime import datetime, timezone +import hashlib +import json +from pathlib import Path +import re +import socket +import time +import traceback + +from lifecycle import LifecycleRun, at, parse +from matrix import digest, fingerprint, stage, write_json +from native117 import stage +from native117_code import Server +from native117 import (LAUNCHER, RIG, output_path, pinned_profile, + require_clean_log, require_disjoint, jar_identity) +import oil_transfer + + +MINERALOGY_VERSION = "6.1.4.117011" +MINERALOGY_SOURCE_COMMIT = "1d7d53450da710701189802f180c1d560ded3c2a" +BOOT_LABELS = ("oil-first-fill", "oil-restart-refill", "oil-second-restart") +CONFIG_NOTICE = "[ne.mi.co.ForgeConfig/CORE]: Forge config just got changed on the file system!" + + +def read_json(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def fixtures(pins): + 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") + 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) + for template in definition[:2]: + lane = deepcopy(template) + for pos in [lane["min"], lane["max"], lane["source"], lane["drain"], *lane["pipes"], lane["output"]]: + pos[0] += 256 + lane["witnesses"] = [lane["source"], lane["drain"], *lane["pipes"], lane["output"]] + lane.update(id="OIL-mineralogy-crude_oil-" + lane["tank"].split(":")[1], + fluid="mineralogy:crude_oil", bucket="mineralogy:crude_oil_bucket") + definition.append(lane) + require_bounds(definition) + return definition + + +def require_bounds(definition): + ids = set() + for lane in definition: + key, low, high = lane["id"], lane["min"], lane["max"] + if key in ids or not re.fullmatch(r"[A-Za-z0-9_-]+", key): + raise AssertionError("Invalid or duplicate oil lane ID") + ids.add(key) + if (len(low) != 3 or len(high) != 3 or low[0] < 1024 or low[2] < 1024 + or low[1] < 8 or high[1] > 240 + or any(not 0 <= b - a <= 31 for a, b in zip(low, high))): + raise AssertionError("Oil lane is outside original remote fixture bounds") + route = [lane["source"], lane["drain"], *lane["pipes"], lane["output"]] + if lane["witnesses"] != route or len({tuple(pos) for pos in route}) != len(route): + raise AssertionError("Oil lane witness geometry changed") + x, y, z = lane["source"] + walls = [[x + dx, y, z + dz] for dx, dz in ((-1, 0), (1, 0), (0, -1), (0, 1))] + for pos in [*route, *walls]: + if len(pos) != 3 or not all(a <= value <= b for a, value, b in zip(low, pos, high)): + raise AssertionError("Oil witness or containment wall outside fixture bounds") + if y >= high[1] or any(sum(abs(a - b) for a, b in zip(p, q)) != 1 + for p, q in zip(route, route[1:])): + raise AssertionError("Oil route disconnected or missing bucket headroom") + for other in definition: + if other is not lane and all(a <= d and c <= b for a, b, c, d in + zip(low, high, other["min"], other["max"])): + raise AssertionError("Oil lane bounds overlap") + + +def require_document(snapshot, definition): + cases = snapshot["cases"] + if set(cases) != {lane["id"] for lane in definition}: + raise AssertionError("Oil snapshot lane IDs/count changed") + for lane in definition: + positions = [tuple(entry["pos"]) for entry in cases[lane["id"]]] + if len(set(positions)) != len(positions) or set(positions) != {tuple(p) for p in lane["witnesses"]}: + raise AssertionError("Oil snapshot has missing, duplicate or moved witnesses") + + +def require_native_boot(path): + require_clean_log(path) + text = path.read_text(encoding="utf-8", errors="replace") + for marker in ("Starting minecraft server version 1.17.1", + "Forge mod loading, version 37.1.1, for MC 1.17.1 with MCP 20210706.113038"): + if marker not in text: + raise AssertionError("Missing genuine native117 boot proof: " + marker) + failures = [line for line in text.splitlines() if any(token in line for token in ( + "Lifecycle probe failed", "ADVLIFECYCLE FAIL", "Unknown or incomplete command", + "Incorrect argument for command", "An unexpected error occurred trying to execute that command"))] + if failures: + raise AssertionError("Native oil runtime failure: " + repr(failures[:5])) + + +def verify_staged(runtime, pins, seed_before): + expected = sorted((Path(jar["path"]).name, jar["sha256"].upper()) for jar in pins["jars"]) + actual = sorted((jar.name, digest(jar)) for jar in (runtime / "mods").glob("*.jar")) + recorded = sorted((jar["file"], jar["sha256"].upper()) for jar in read_json(runtime / "input-jars.json")) + if len({name for name, _ in expected}) != len(expected) or actual != expected or recorded != expected: + raise AssertionError("Staged native jars differ from captured manifest pins") + if digest(runtime / LAUNCHER) != pins["launcher_sha256"].upper(): + raise AssertionError("Staged native launcher differs from captured manifest pin") + # The installed seed's complete libraries and root launch jars, not its + # worlds/config/mods, are the only seed content matrix.stage may copy. + expected_libraries = {str(Path(name).relative_to("libraries")): value for name, value in seed_before.items() + if Path(name).parts[0] == "libraries"} + if not expected_libraries or fingerprint(runtime / "libraries") != expected_libraries: + raise AssertionError("Staged native libraries differ from seed fingerprint") + expected_roots = {name: value for name, value in seed_before.items() + if len(Path(name).parts) == 1 and name.endswith(".jar") and "installer" not in name} + if {jar.name: digest(jar) for jar in runtime.glob("*.jar")} != expected_roots: + raise AssertionError("Staged root jars differ from seed fingerprint") + + +def prepare(runtime, profile, definition, port, pins, seed_before): + if profile[1] != LAUNCHER: + raise ValueError("Refusing a non-native117 oil runtime") + launcher = stage(runtime, profile) + properties = runtime / "server.properties" + lines = [line for line in properties.read_text().splitlines() + if not line.startswith(("server-port=", "generator-settings="))] + properties.write_text("\n".join(lines) + f"\nserver-port={port}\ngenerator-settings=\n", encoding="utf-8") + for marker in ("migration-operate.allow", "advantage-works.allow", "advantage-lifecycle.allow"): + (runtime / marker).touch() + write_json(runtime / "lifecycle-fixture.json", definition) + verify_staged(runtime, pins, seed_before) + return launcher + + +def construction_commands(definition): + commands = [] + for lane in definition: + commands.append("fill " + " ".join(map(str, lane["min"] + lane["max"])) + " minecraft:air replace") + blocks = [(p, "basepower:fluid_pipe") for p in lane["pipes"]] + blocks += [(lane["drain"], "basepower:fluid_drain"), (lane["output"], lane["tank"])] + x, y, z = lane["source"] + blocks += [([x + dx, y, z + dz], "minecraft:glass") for dx, dz in ((-1, 0), (1, 0), (0, -1), (0, 1))] + commands.extend("setblock " + " ".join(map(str, pos)) + " " + block for pos, block in blocks) + return commands + + +class NativeOilRun(LifecycleRun): + def __init__(self, runtime, java, launcher, definition, pins, seed_before): + super().__init__(runtime, java, launcher, definition) + self.pins, self.seed_before = pins, seed_before + self.boots = [] + self.pending_construction = construction_commands(definition) + self.construction_sealed = False + self.bucket_permission = False + 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)]): + 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.boots.append(label) + require_native_boot(self.runtime / f"console-{label}.log") + + def seal_construction(self): + if self.pending_construction or self.construction_sealed or len(self.boots) != 1: + raise AssertionError("Incomplete or repeated initial oil construction") + self.construction_sealed = True + + def permit_buckets(self): + if not self.construction_sealed or self.bucket_placements != len(self.boots) - 1 or len(self.boots) > 2: + raise AssertionError("Only one bucket placement after each preservation gate is permitted") + self.bucket_permission = True + + def command(self, command): + if self.server is None: + raise AssertionError("No native oil process is running") + if self.pending_construction and command == self.pending_construction[0]: + if len(self.boots) != 1 or self.construction_sealed: + raise AssertionError("Oil construction after initial baseline is forbidden") + self.pending_construction.pop(0) + elif command == "save-all flush": + pass + else: + match = re.fullmatch(r"advlifecycle (load|snapshot|place-buckets) [A-Za-z0-9_-]+", command) + if not match: + raise AssertionError("Forbidden native oil mutation: " + command) + if match[1] == "place-buckets": + if not self.bucket_permission or self.bucket_placements >= 2: + raise AssertionError("Bucket placement requires the empty/restored preservation gate") + self.bucket_permission = False + self.bucket_placements += 1 + super().command(command) + + +def exercise(runner, seconds, progress): + definition = runner.definition + phases = [] + + def record(phase, snapshot, expected): + require_document(snapshot, definition) + phases.append(dict(phase=phase, cases=oil_transfer.inspect(snapshot, definition, expected))) + progress(dict(phases=phases, full_process_restarts=len(runner.boots) - 1, + bucket_placements=runner.bucket_placements)) + + runner.boot(BOOT_LABELS[0]) + runner.probe("load", "oil-loaded") + for command in construction_commands(definition): + runner.command(command) + runner.seal_construction() + empty = runner.probe("snapshot", "oil-empty") + require_document(empty, definition) + for lane in definition: + for pos in (lane["drain"], lane["output"]): + if parse(at(empty, lane["id"], pos)).get("Tank", {}).get("Amount", 0) != 0: + raise AssertionError("Fixture was not empty: " + lane["id"]) + oil_transfer.require_same_machines(empty, empty, definition) + runner.permit_buckets() + runner.probe("place-buckets", "oil-first-buckets") + time.sleep(seconds) + first = runner.probe("snapshot", "oil-first-fill") + record("first-fill", first, 1000) + oil_transfer.require_same_machines(empty, first, definition) + runner.command("save-all flush") + runner.close() + + runner.boot(BOOT_LABELS[1]) + runner.probe("load", "oil-restart-loaded") + time.sleep(seconds) + restored = runner.probe("snapshot", "oil-restored") + record("restored", restored, 1000) + runner.permit_buckets() + runner.probe("place-buckets", "oil-refill-buckets") + time.sleep(seconds) + refilled = runner.probe("snapshot", "oil-refilled") + record("refilled", refilled, 2000) + oil_transfer.require_same_machines(restored, refilled, definition) + runner.command("save-all flush") + runner.close() + + runner.boot(BOOT_LABELS[2]) + runner.probe("load", "oil-second-restart-loaded") + time.sleep(seconds) + record("retained", runner.probe("snapshot", "oil-retained"), 2000) + runner.command("save-all flush") + runner.close() + if runner.boots != list(BOOT_LABELS) or runner.bucket_placements != 2: + raise AssertionError("Incomplete native oil restart/refill protocol") + + +def run(args): + if not 1 <= args.port <= 65535 or not 2 <= args.seconds <= 120 or not args.java8.is_file(): + raise ValueError("Supply Java 8, a TCP port, and 2..120 seconds per observation interval") + output = output_path(args.output) + if not output.name.startswith("native117-oil-"): + raise ValueError("Use a new native117-oil-* output under test-rig/build") + manifest = args.target_manifest.resolve() + require_disjoint(output, manifest) + manifest_bytes = manifest.read_bytes() + manifest_hash = hashlib.sha256(manifest_bytes).hexdigest().upper() + pins = json.loads(manifest_bytes)["target117"] + profile = pinned_profile(manifest, "target117") + if digest(manifest) != manifest_hash: + raise AssertionError("Target manifest changed during preflight") + for protected in (profile[0], *profile[2]): + require_disjoint(output, protected) + definition = fixtures(pins) + with socket.socket() as listener: + listener.bind(("127.0.0.1", args.port)) + seed_before = fingerprint(profile[0]) + helpers = [Path(oil_transfer.__file__), Path(__file__), Path(__file__).with_name("lifecycle.py")] + helpers_before = {path.name: digest(path) for path in helpers} + output.mkdir(parents=True, exist_ok=False) + (output / "target-manifest.json").write_bytes(manifest_bytes) + report_path = output / "native117-oil-results.json" + runtime = output / "target117" + report = dict(status="RUNNING", started_at_utc=datetime.now(timezone.utc).isoformat(), + scope="native117-buckets-descending-pipes-two-full-restarts", + protocol="Initial empty construction only; native bucket sources twice; no tile.read() restores/API offers/manual tank refills/resets", + targetManifest=str(manifest), targetManifestSha256=manifest_hash, targetProfile=pins, + seed=str(profile[0]), seedFingerprintBefore=seed_before, helperSha256=helpers_before, + port=args.port, seconds=args.seconds, laneCount=len(definition), phases=[], full_process_restarts=0, + mineralogy="INCLUDED" if len(definition) == 6 else "ABSENT_FROM_MANIFEST") + write_json(output / "oil-fixture.json", definition) + write_json(report_path, report) + runner = None + try: + launcher = prepare(runtime, profile, definition, args.port, pins, seed_before) + report["stagedInputs"] = read_json(runtime / "input-jars.json") + runner = NativeOilRun(runtime, args.java8, launcher, definition, pins, seed_before) + + def progress(value): + report.update(value) + write_json(report_path, report) + print("Oil " + value["phases"][-1]["phase"] + ": PASS, " + str(len(definition)) + " lanes", flush=True) + + exercise(runner, args.seconds, progress) + report["status"] = "PASS" + except Exception as error: + report.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + except BaseException as error: + report.update(status="FAIL", failure=repr(error), traceback=traceback.format_exc()) + raise + finally: + if runner is not None: + try: + runner.close() + except Exception as error: + report.update(status="FAIL", shutdown_failure=str(error)) + try: + verify_staged(runtime, pins, seed_before) + logs = sorted(runtime.glob("console-*.log")) + for log in logs: + require_native_boot(log) + report["nativeBoots"] = [log.name for log in logs] + report["forge_config_reload_notices"] = { + log.name: log.read_text(encoding="utf-8", errors="replace").count(CONFIG_NOTICE) for log in logs} + if report["status"] == "PASS" and len(logs) != 3: + raise AssertionError("Three complete native117 boot logs are required") + except Exception as error: + report.update(status="FAIL", runtime_audit_failure=str(error)) + try: + after = fingerprint(profile[0]) + report["seedFingerprintAfter"] = after + report["seed_unchanged"] = after == seed_before + report["helpers_unchanged"] = {path.name: digest(path) for path in helpers} == helpers_before + report["manifest_unchanged"] = digest(manifest) == manifest_hash + if not all(report[key] for key in ("seed_unchanged", "helpers_unchanged", "manifest_unchanged")): + raise AssertionError("Protected seed, helper or manifest changed") + except Exception as error: + report.update(status="FAIL", preservation_failure=str(error)) + report["completed_at_utc"] = datetime.now(timezone.utc).isoformat() + write_json(report_path, report) + print(report["status"] + ": " + str(report_path), flush=True) + return int(report["status"] != "PASS") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, default=25576) + parser.add_argument("--seconds", type=int, default=8) + return run(parser.parse_args()) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_operating_import.py b/test-rig/migration/native117_operating_import.py new file mode 100644 index 00000000..96878b15 --- /dev/null +++ b/test-rig/migration/native117_operating_import.py @@ -0,0 +1,122 @@ +"""Cold-import an accepted operating fixture without reconstructing its machines.""" + +import argparse +from datetime import datetime, timezone +import json +from pathlib import Path +import shutil +import traceback + +from matrix import WORLD, digest, fingerprint, write_json +import native116 +from native116_lifecycle import verify_staged as verify_staged_116 +from native117 import output_path, pinned_profile, require_disjoint +from native117_code import Server +from native117_lifecycle import prepare, verify_staged +from native117_machine_operations import validate_operation, validate_restart + + +def read(path): + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def verify_source_profile(manifest, source): + entry = read(manifest)["target116"] + required = {"basepower", "basesteam", "baseelectric", "basemetals", "orespawn", "advantageworkstest"} + ids = [row["id"] for row in entry["jars"]] + if set(ids) != required or len(ids) != len(required): + raise ValueError("Expected the complete released-identity 1.16.5 profile") + seed, launcher, jars = pinned_profile(manifest, "target116") + if launcher != native116.LAUNCHER or digest(seed / launcher) != entry["launcher_sha256"].upper(): + raise AssertionError("Source Forge 1.16.5 launcher changed") + native116.verify_code(seed, entry["serverCode"]) + if read(seed / native116.INVENTORY) != entry["serverCode"]: + raise AssertionError("Source Forge 1.16.5 inventory receipt changed") + for row, jar in zip(entry["jars"], jars): + if not row["version"].endswith(".116051"): + raise ValueError("Expected a native 1.16.5 mod version") + native116.jar_identity(jar, row["id"], row["version"]) + verify_staged_116(source, (seed, launcher, jars)) + + +def approved_source(source, report_sha256): + report_path = source / "native116-machine-operation-results.json" + if digest(report_path) != report_sha256.upper(): raise AssertionError("Source operating report changed") + report = read(report_path) + if (report.get("status") != "PASS" or report.get("group") != "all" + or report.get("protocolUnchanged") is not True + or set(report.get("phases", {})) != {"operation", "restart"} + or any(row.get("status") != "PASS" or row.get("cases") != 132 for row in report["phases"].values())): + raise AssertionError("Import requires the complete accepted 132-case source, not a passing subset") + manifest = Path(report["manifest"]) + if digest(manifest) != report["manifestSha256"]: raise AssertionError("Source mod manifest changed") + verify_source_profile(manifest, source) + fixture_path = source / "machine-operation-fixture.json" + if digest(fixture_path) != report["fixtureSha256"]: raise AssertionError("Source fixture authority changed") + fixture = read(fixture_path) + if len(fixture.get("cases", [])) != 132: raise AssertionError("Incomplete source fixture") + catalog = read(source / "advantage-works-results/machine-catalog-measured.json") + actual = read(source / "advantage-works-results/machine-operation-measured.json") + if validate_operation(catalog, actual, "all")["status"] != "PASS": + raise AssertionError("Source raw operating assertions did not pass") + inspected = read(source / "advantage-works-results/machine-inspection-restarted.json") + if validate_restart(fixture, inspected)["status"] != "PASS": + raise AssertionError("Source raw cold inspection differs from its fixture") + if not (source / WORLD / "level.dat").is_file(): raise FileNotFoundError("Missing source world") + return fixture, report_path + + +def run(args): + source = args.source.resolve() + output = output_path(args.output) + for protected in (source, args.target_manifest): require_disjoint(output, protected) + fixture, source_report = approved_source(source, args.source_report_sha256) + target = pinned_profile(args.target_manifest.resolve(), "target117") + for protected in (target[0], *target[2]): require_disjoint(output, protected) + before = fingerprint(source) + launcher = prepare(output, target, [], args.port) + shutil.copytree(source / WORLD, output / WORLD, ignore=shutil.ignore_patterns("session.lock")) + shutil.copy2(source / "machine-operation-fixture.json", output / "machine-operation-fixture.json") + (output / "advantage-machine-operation.allow").touch() + (output / "migration-operate.allow").touch() + report = dict(status="RUNNING", scope="132 accepted native116 operating setups, frozen import and full restart only; no construction or operation claimed", + source=str(source), sourceReportSha256=digest(source_report), sourceFingerprintBefore=before, + targetManifest=str(args.target_manifest.resolve()), targetManifestSha256=digest(args.target_manifest), + startedAtUtc=datetime.now(timezone.utc).isoformat(), phases={}) + server = None + try: + for label in ("converted", "restarted"): + server = Server(output, args.java16, launcher, label) + server.send("advmachines inspect " + label) + server.wait("ADVMACHINES INSPECTED " + label, 360) + inspected = read(output / f"advantage-works-results/machine-inspection-{label}.json") + report["phases"][label] = validate_restart(fixture, inspected) + server.send("save-all flush") + server.close(); server = None + write_json(output / "native117-operating-import-results.json", report) + verify_staged(output, target) + if digest(output / "machine-operation-fixture.json") != digest(source / "machine-operation-fixture.json"): + raise AssertionError("Fixture authority changed during import") + report["status"] = "PASS" if all(row["status"] == "PASS" for row in report["phases"].values()) else "FAIL" + except BaseException as error: + report.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + finally: + if server: + try: server.close() + except BaseException as error: report.update(status="FAIL", shutdownError=str(error)) + report["sourceUnchanged"] = fingerprint(source) == before + if not report["sourceUnchanged"]: report.update(status="FAIL", sourceError="Protected source changed") + report["completedAtUtc"] = datetime.now(timezone.utc).isoformat() + write_json(output / "native117-operating-import-results.json", report) + return int(report["status"] != "PASS") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--source-report-sha256", required=True) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java16", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, required=True) + raise SystemExit(run(parser.parse_args())) diff --git a/test-rig/migration/native117_partial.py b/test-rig/migration/native117_partial.py new file mode 100644 index 00000000..d80f0d38 --- /dev/null +++ b/test-rig/migration/native117_partial.py @@ -0,0 +1,362 @@ +"""Native 1.16 transport for unchanged partial-network and pipe-bridge oracles.""" + +import argparse +from datetime import datetime, timezone +import json +from pathlib import Path +import shutil +import socket +import time +import traceback + +from audit_pipe_only import bridge_result +import extended_lifecycle as extended +import pipe_only_lifecycle as bridge +from lifecycle import at, counts, parse +from matrix import digest, fingerprint, write_json +from native117 import output_path, pinned_profile, require_disjoint +from native117_lifecycle import (NativeLifecycleRun, authorize_lifecycle, frozen_import, + prepare, read_json, require_native_boot, verify_source, + verify_staged) + + +def selected_chunks(definition, group=None): + if group is None: + return {(x, z) for f in definition + for x in range(f["min"][0] >> 4, (f["max"][0] >> 4) + 1) + for z in range(f["min"][2] >> 4, (f["max"][2] >> 4) + 1)} + return {tuple(p) for f in definition for p in f.get("groups", {}).get(group, [])} + + +def subset_load_issues(before, current, selected): + old = {tuple(c["pos"]): c for c in before["chunks"]} + chunks = {tuple(c["pos"]): c for c in current["chunks"]} + events = {tuple(e["pos"]) for e in current.get("chunkEvents", []) if e["operation"] == "load"} + errors = [] + for pos in selected: + a, b = old.get(pos), chunks.get(pos) + if a is None or b is None or not b["loaded"]: + errors.append("selected chunk not loaded: " + str(pos)) + elif pos not in events or b["loads"] <= 0 or (not a["loaded"] and b["loads"] <= a["loads"]): + errors.append("missing actual Chunk.Load: " + str(pos)) + for rows in current["cases"].values(): + for row in rows: + if (row["pos"][0] >> 4, row["pos"][2] >> 4) in selected and not row.get("loaded"): + errors.append("selected witness not loaded: " + str(row["pos"])) + return errors + + +def subset_unload_pending(before, current, selected): + old = {tuple(c["pos"]): c for c in before["chunks"]} + chunks = {tuple(c["pos"]): c for c in current["chunks"]} + events = {tuple(e["pos"]) for e in current.get("chunkEvents", []) if e["operation"] == "unload"} + absent = all(pos in chunks and not chunks[pos]["loaded"] for pos in selected) + pending = [str(pos) for pos in selected if pos not in old or pos not in chunks + or pos not in events or chunks[pos]["unloads"] <= old[pos]["unloads"]] + return absent, pending + + +class NativePartialRun(NativeLifecycleRun): + """Select actual snapshots by chunk callbacks/ticks, never by machine results.""" + + def __init__(self, *args): + super().__init__(*args) + self._released_groups = {} + + def _canonical(self, selected, label): + root = self.runtime / "advantage-works-results" + destination = root / ("lifecycle-" + label + ".json") + if destination.exists(): + raise FileExistsError(destination) + shutil.copy2(root / ("lifecycle-" + selected + ".json"), destination) + + def _load_ready(self, operation, label): + selected = selected_chunks(self.definition, None if operation == "load" else operation[5:]) + if not selected: + raise ValueError("Unknown fixture chunk group: " + operation) + before = self._raw_probe("snapshot", label + "-native-before") + current_label = label + "-native-initial" + current = self._raw_probe(operation, current_label) + initial_tick, loaded_tick, started = current["worldTime"], None, time.monotonic() + record = dict(label=label, phase="native-subset-load-readiness", operation=operation, + selected_chunks=sorted(selected), samples=[], max_seconds=20, max_ticks=160) + self.observations.append(record) + try: + for attempt in range(200): + errors = subset_load_issues(before, current, selected) + tick = current["worldTime"] + if tick < initial_tick: + raise AssertionError("Native worldTime moved backwards") + loaded_tick = None if errors else (tick if loaded_tick is None else loaded_tick) + record["samples"].append(dict(label=current_label, world_time=tick, issues=errors, + loaded_since_world_time=loaded_tick)) + if loaded_tick is not None and tick - loaded_tick >= 1: + self._canonical(current_label, label) + record.update(status="PASS", selected_label=current_label, canonical_label=label) + self._record() + return current + if time.monotonic() - started >= 20 or tick - initial_tick >= 160 or attempt == 199: + raise TimeoutError("Native selected chunks did not load and advance a real tick: " + label) + self._record() + time.sleep(0.01) + current_label = label + f"-native-pending-{attempt + 1:03}" + current = self._raw_probe("snapshot", current_label) + except BaseException as error: + record.update(status="FAIL", failure=str(error)) + self._record() + raise + + def _snapshot_ready(self, operation, label): + current_label = label + "-native-initial" + current = self._raw_probe(operation, current_label) + record = dict(label=label, phase="native-subset-unload-callback-readiness", samples=[]) + self.observations.append(record) + for attempt in range(45): + pending_groups = [] + for group, context in self._released_groups.items(): + absent, pending = subset_unload_pending(context["before"], current, context["selected"]) + if absent and pending and time.monotonic() < context["deadline"]: + pending_groups.append(group) + record["samples"].append(dict(label=current_label, world_time=current["worldTime"], + pending_groups=pending_groups)) + if not pending_groups or attempt == 44: + break + self._record() + deadline = min(self._released_groups[g]["deadline"] for g in pending_groups) + time.sleep(min(1, max(0, deadline - time.monotonic()))) + current_label = label + f"-native-pending-{attempt + 1:02}" + current = self._raw_probe("snapshot", current_label) + self._canonical(current_label, label) + record.update(status="OBSERVED", selected_label=current_label, canonical_label=label) + self._record() + return current + + def probe(self, operation, label): + if operation == "load" or operation.startswith("load-"): + result = self._load_ready(operation, label) + if operation == "load": + self._released_groups.clear() + else: + self._released_groups.pop(operation[5:], None) + return result + if operation.startswith("release-"): + group = operation[8:] + selected = selected_chunks(self.definition, group) + if not selected: + raise ValueError("Unknown fixture chunk group: " + group) + before = self._raw_probe("snapshot", label + "-native-before") + result = self._raw_probe(operation, label) + self._released_groups[group] = dict(before=before, selected=selected, + deadline=time.monotonic() + 45) + return result + if self._released_groups and operation in ("snapshot", "isolated"): + return self._snapshot_ready(operation, label) + return super().probe(operation, label) + + def boot(self, label): + super().boot(label) + require_native_boot(self.runtime / f"console-{label}.log") + if label == "blocked-restart": + self.probe("load", "blocked-restart-native-restored") + + +class NativeExtendedRun(extended.ExtendedRun, NativePartialRun): + pass + + +def orders(runner, process=False): + results = [] + for order in (("machine", "supply"), ("supply", "machine")): + label = "order-" + "-".join(order) + before = runner.probe("snapshot", label + "-before") + runner.probe("release-supply", label + "-release-supply") + runner.probe("release-machine", label + "-release-machine") + time.sleep(5) + absent = runner.probe("snapshot", label + "-absent") + first = runner.probe("load-" + order[0], label + "-first") + time.sleep(2) + settled = runner.probe("snapshot", label + "-settled") + loaded = runner.probe("load-" + order[1], label + "-loaded") + results.append(dict(label=label, order=list(order), cases=extended.order_results( + runner.definition, before, absent, first, settled, loaded, order))) + if process: + runner.replenish() + time.sleep(50) + runner.processing(label + "-resumed", 5000) + return results + + +def exercise_partial(runner, progress): + runner.boot("partial-trace") + runner.build() + time.sleep(50) + runner.processing("commissioned", 1000) + runner.probe("trace-loads", "trace-enabled") + result = dict(status="RUNNING", partial_networks=[], reload_orders=[]) + for cycle, group in enumerate(("machine", "supply"), 1): + result["partial_networks"].append(runner.partial(group, cycle)) + progress(result) + result["reload_orders"] = orders(runner) + result["status"] = "PASS" if all(c["status"] == "PASS" for r in + result["partial_networks"] + result["reload_orders"] for c in r["cases"].values()) else "FAIL" + runner.command("save-all flush") + runner.close() + progress(result) + return result + + +def exercise_bridge(runner, progress): + runner.boot("pipe-only") + runner.probe("load", "loaded") + for f in runner.definition: + runner.command("fill " + " ".join(map(str, f["min"] + f["max"])) + " minecraft:air replace") + fluid, steam = f["id"] == "C-FLUID", f["id"] == "C-STEAM" + pipe = "basepower:fluid_pipe" if fluid else "basesteam:steam_pipe" if steam else "baseelectric:electric_conduit" + runner.block(f["witnesses"][0], pipe if fluid else "basepower:infinite_" + ("steam" if steam else "electricity")) + for pos in f["pipes"]: + runner.block(pos, pipe) + runner.block(f["witnesses"][1], "basepower:fluid_metal_tank" if fluid else + "basesteam:steam_furnace" if steam else "baseelectric:electric_furnace") + if steam: + runner.inventory(f["witnesses"][1], 0, "minecraft:coal", 64) + runner.probe("trace-loads", "trace-enabled") + time.sleep(2) + runner.probe("snapshot", "commissioned") + result = dict(status="RUNNING", cycles=[]) + for cycle in range(3): + before = runner.probe("snapshot", f"bridge-{cycle}-before") + runner.command("save-all flush") + runner.probe("release-bridge", f"bridge-{cycle}-released") + time.sleep(3) + absent = runner.probe("isolated", f"bridge-{cycle}-isolated") + runner.probe("offer-blocked", f"bridge-{cycle}-blocked-offer") + time.sleep(3) + confirmed = runner.probe("snapshot", f"bridge-{cycle}-confirmed") + loaded = runner.probe("load-bridge", f"bridge-{cycle}-loaded") + runner.probe("offer", f"bridge-{cycle}-reconnected-offer") + for f in runner.definition: + if f["id"] == "C-FLUID": + continue + machine, steam = f["witnesses"][1], f["id"] == "C-STEAM" + for slot in range(1 if steam else 0, 7 if steam else 10): + runner.inventory(machine, slot, "minecraft:air", 1) + runner.inventory(machine, 1 if steam else 0, "minecraft:iron_ore", 4) + time.sleep(50) + resumed = runner.probe("snapshot", f"bridge-{cycle}-resumed") + verdict = bridge_result(runner.definition, before, absent, confirmed, loaded, resumed, cycle) + result["cycles"].append(dict(cycle=cycle, **verdict)) + progress(result) + print(f"{runner.runtime.name}: bridge cycle {cycle + 1}: {verdict['status']}", flush=True) + result["status"] = "PASS" if all(c["status"] == "PASS" for c in result["cycles"]) else "FAIL" + runner.command("save-all flush") + runner.close() + progress(result) + return result + + +def common_parser(description): + parser = argparse.ArgumentParser(description=description) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--source-manifest", type=Path, required=True) + parser.add_argument("--source-key", default="target113") + parser.add_argument("--source-hashes", type=Path, required=True) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, required=True, choices=(25576, 25580, 25581, 25582, 25583)) + return parser + + +def campaign(args, name, definition, exercise, runner_type=NativeExtendedRun): + output = output_path(args.output) + for protected in (args.source, args.source_manifest, args.source_hashes, args.target_manifest): + require_disjoint(output, protected) + if not args.java8.is_file(): + raise FileNotFoundError(args.java8) + source_before, history = verify_source(args) + target = pinned_profile(args.target_manifest.resolve(), "target117") + for protected in (target[0], *target[2]): + require_disjoint(output, protected) + with socket.socket() as listener: + listener.bind(("127.0.0.1", args.port)) + output.mkdir(parents=True) + report_path = output / ("native117-" + name + "-results.json") + helpers = [Path(__file__), Path(extended.__file__), Path(bridge.__file__), + Path(__file__).with_name("lifecycle.py"), Path(__file__).with_name("native117_lifecycle.py"), + Path(__file__).with_name("native117_extended.py")] + hashes = {path.name: digest(path) for path in helpers if path.is_file()} + report = dict(status="RUNNING", campaign=name, started_at_utc=datetime.now(timezone.utc).isoformat(), + scope="native117 fresh and hash-protected retained13 copy; unchanged historical semantic oracles", + source=str(args.source.resolve()), sourceRuntimeBefore=source_before, sourceStateGates=history, + sourceManifestSha256=digest(args.source_manifest), sourceHashesSha256=digest(args.source_hashes), + targetManifest=str(args.target_manifest.resolve()), targetManifestSha256=digest(args.target_manifest), + targetProfile=read_json(args.target_manifest)["target117"], helpersBefore=hashes, port=args.port, profiles={}) + for path in helpers: + if path.is_file(): + archive = output / "protocol-sources" / path.name + archive.parent.mkdir(exist_ok=True) + shutil.copy2(path, archive) + write_json(output / "lifecycle-fixture.json", definition) + write_json(report_path, report) + try: + for label, source in (("fresh117", None), ("original110-chained113-native117", args.source)): + runtime, runner = output / label, None + result = report["profiles"][label] = dict(status="RUNNING") + write_json(report_path, report) + try: + launcher = prepare(runtime, target, definition, args.port, source) + runner = runner_type(runtime, args.java8, launcher, definition) + if source is not None: + result["frozen_import"] = frozen_import(runner, source) + authorize_lifecycle(runtime) + def progress(value): + result.update(value) + write_json(report_path, report) + exercise(runner, progress) + except BaseException as error: + result.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + print(label + ": FAIL: " + str(error), flush=True) + finally: + if runner is not None: + try: + runner.close() + except BaseException as error: + result.update(status="FAIL", shutdown_error=str(error)) + if (runtime / "input-jars.json").is_file(): + try: + verify_staged(runtime, target) + logs = sorted(runtime.glob("console-*.log")) + for log in logs: + require_native_boot(log) + text = log.read_text(encoding="utf-8", errors="replace") + if "Stopping server" not in text: + raise AssertionError("Missing graceful shutdown: " + log.name) + result["native_boots"] = [log.name for log in logs] + result["keep_up_warnings"] = {log.name: log.read_text(encoding="utf-8").count("Can't keep up!") for log in logs} + except BaseException as error: + result.update(status="FAIL", runtime_audit_error=str(error)) + write_json(report_path, report) + report["status"] = "PASS" if all(p["status"] == "PASS" for p in report["profiles"].values()) else "FAIL" + finally: + report["sourceRuntimeAfter"] = fingerprint(args.source.resolve()) + report["source_fixture_unchanged"] = report["sourceRuntimeAfter"] == source_before + report["helpersAfter"] = {path.name: digest(path) for path in helpers if path.is_file()} + report["helpers_unchanged"] = hashes == report["helpersAfter"] + if not report["source_fixture_unchanged"] or not report["helpers_unchanged"]: + report["status"] = "FAIL" + report["completed_at_utc"] = datetime.now(timezone.utc).isoformat() + write_json(report_path, report) + print(report["status"] + ": " + str(report_path), flush=True) + return int(report["status"] != "PASS") + + +def main(): + parser = common_parser(__doc__) + parser.add_argument("--campaign", choices=("partial", "bridge"), required=True) + args = parser.parse_args() + definition = [f for f in extended.fixtures() if "groups" in f] if args.campaign == "partial" else bridge.fixtures() + return campaign(args, args.campaign, definition, exercise_partial if args.campaign == "partial" else exercise_bridge) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_partial_diagnostic.py b/test-rig/migration/native117_partial_diagnostic.py new file mode 100644 index 00000000..c2ca2d16 --- /dev/null +++ b/test-rig/migration/native117_partial_diagnostic.py @@ -0,0 +1,30 @@ +"""Empty-bay control for native partial-unload fixture ticket interference.""" + +import time + +import extended_lifecycle +from native117_partial import campaign, common_parser, orders + + +def exercise(runner, progress): + runner.boot("partial-empty-control") + runner.probe("load", "loaded") + for f in runner.definition: + runner.command("fill " + " ".join(map(str, f["min"] + f["max"])) + " minecraft:air replace") + runner.probe("trace-loads", "trace-enabled") + result = dict(status="RUNNING", purpose="empty bays; no Advantage networks or machines constructed", partial_networks=[]) + for cycle, group in enumerate(("machine", "supply"), 1): + result["partial_networks"].append(runner.partial(group, cycle)) + progress(result) + result["reload_orders"] = orders(runner) + result["status"] = "PASS" if all(c["status"] == "PASS" for r in + result["partial_networks"] + result["reload_orders"] for c in r["cases"].values()) else "FAIL" + runner.command("save-all flush") + runner.close() + progress(result) + return result + + +if __name__ == "__main__": + args = common_parser(__doc__).parse_args() + raise SystemExit(campaign(args, "empty-control", [f for f in extended_lifecycle.fixtures() if "groups" in f], exercise)) diff --git a/test-rig/migration/native117_player_layout.py b/test-rig/migration/native117_player_layout.py new file mode 100644 index 00000000..47f1970d --- /dev/null +++ b/test-rig/migration/native117_player_layout.py @@ -0,0 +1,129 @@ +"""Native destinations for the historical player-only bag/Ender layout oracle.""" + +import argparse +import json +from pathlib import Path +import shutil +import socket + +from native117 import boot as native117_boot +import actors +from native117_actor_io import capture_actors, capture_dimensions +from dimensions import exact_item_differences +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_actors import baseline, command, raw, ready_entities +from native117_readiness import require_ready +from player_layout import player_verdict + + +ROUTES = ("original110", "maintained110", "maintained112", "original110-chained-113", + "maintained110-direct-113", "maintained112-direct-113") + + +def verdict(expected, observed): + result = player_verdict(expected, observed) + extra = exact_item_differences({key: expected[key] for key in ("file", "adapter")}, observed) + result["differences"].extend(extra) + if extra: + result["status"] = "FAIL" + return result + + +def audit(root): + summary = json.loads((root / "summary.json").read_text()) + findings = [] + manifest = Path(summary["targetManifest"]) + if digest(manifest) != summary["targetManifestSha256"]: + findings.append("Target manifest changed") + pinned_profile(manifest, "target117") + for route in summary["results"]: + runtime = root / route + provenance = json.loads((runtime / "source-provenance.json").read_text()) + source = Path(provenance["source"]) + if fingerprint(source) != provenance["fingerprint"]: + findings.append(route + ": protected source changed") + expected, _ = baseline(source, "actors") + for jar in json.loads((runtime / "input-jars.json").read_text()): + if digest(runtime / "mods" / jar["file"]) != jar["sha256"]: + findings.append(route + ": staged jar changed") + for label in ("converted", "restarted"): + state = capture_actors(runtime, label, runtime / "actor-evidence" / label) + current = {"player": verdict(expected, state), "frozen": actors.freeze_result(raw(runtime, "actors", label))} + ready_label = "entities-layout-" + label + current["readiness"] = require_ready(json.loads((runtime / "advantage-works-results" / + ("native-readiness-" + ready_label + ".json")).read_text()), (6, -2, 13, 2), ready_label) + if current != summary["results"][route][label] or any(value["status"] != "PASS" for value in current.values()): + findings.append(route + "/" + label + ": raw player/freeze replay differs or fails") + require_clean_log(runtime / ("console-" + label + ".log")) + return {"status": "FAIL" if findings else "PASS", "routes": len(summary["results"]), "findings": findings, + "scope": "Player storage only; no actor release or turbine operation assertion"} + + +def run(args): + root = output_path(args.output) + require_disjoint(root, args.fixtures) + 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) + results = {} + for route in ROUTES: + source = args.fixtures / route + before = fingerprint(source) + approved = source / "fixture-hashes.json" + if approved.is_file() and fingerprint(source / WORLD) != json.loads(approved.read_text()): + raise AssertionError("Protected layout fixture changed") + expected, source_label = baseline(source, "actors") + runtime = root / route + launcher = native117_boot(runtime, target, args.java8, args.port) + (runtime / "advantage-works.allow").touch() + copy_fixture(source, runtime) + shutil.copy2(source / "actor-fixture.json", runtime / "actor-fixture.json") + write_json(runtime / "source-provenance.json", {"source": str(source.resolve()), "fingerprint": before, "checkpoint": source_label}) + checks = {} + try: + for label in ("converted", "restarted"): + server = Server(runtime, args.java8, launcher, label) + try: + readiness = ready_entities(server, runtime, "actors", "layout-" + label) + state = command(server, runtime, "actors", "read", label) + checks[label] = {"player": verdict(expected, state), + "frozen": actors.freeze_result(raw(runtime, "actors", label)), "readiness": readiness} + finally: + server.close() + require_clean_log(runtime / ("console-" + label + ".log")) + finally: + write_json(runtime / "player-comparison.json", checks) + if fingerprint(source) != before: + raise AssertionError("Protected player-layout source changed") + results[route] = checks + write_json(root / "results.json", results) + print(route + ": " + ", ".join(label + "=" + check["player"]["status"] for label, check in checks.items()), flush=True) + status = "FAIL" if any(value["status"] != "PASS" for checks in results.values() for phase in checks.values() for value in phase.values()) else "PASS" + write_json(root / "summary.json", {"status": status, "results": results, "sourceFixtures": "UNCHANGED", + "targetManifest": str(args.target_manifest.resolve()), "targetManifestSha256": digest(args.target_manifest), + "scope": "Unchanged historical player-only oracle plus exact item NBT and genuinely frozen actor guard"}) + return int(status != "PASS") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--audit", type=Path) + parser.add_argument("--fixtures", type=Path) + parser.add_argument("--target-manifest", type=Path) + parser.add_argument("--java8", type=Path) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, choices=(25574, *range(25584, 25588))) + args = parser.parse_args() + if args.audit: + if args.output.exists(): + raise FileExistsError(args.output) + result = audit(args.audit.resolve()) + write_json(args.output, result) + print(result["status"] + ": " + str(result["routes"]) + " routes; " + str(len(result["findings"])) + " integrity findings") + raise SystemExit(int(result["status"] != "PASS")) + if any(value is None for value in (args.fixtures, args.target_manifest, args.java8, args.port)): + parser.error("Running requires fixtures, target manifest, Java 8 and port") + raise SystemExit(run(args)) diff --git a/test-rig/migration/native117_readiness.py b/test-rig/migration/native117_readiness.py new file mode 100644 index 00000000..95546501 --- /dev/null +++ b/test-rig/migration/native117_readiness.py @@ -0,0 +1,92 @@ +"""Read-only native chunk/entity readiness gate shared by disposable runtime tests.""" + +import json +from pathlib import Path +import re + +PROTOCOL = "native117-chunk-readiness-v1" +MAX_CHUNKS = 256 +MAX_CHECKS = 1200 +MAX_WAIT_MILLIS = 120000 + + +def chunk_set(bounds): + if len(bounds) != 4 or any(type(value) is not int for value in bounds): + raise AssertionError("Readiness bounds must contain four integer chunk coordinates") + min_x, min_z, max_x, max_z = bounds + width, depth = max_x - min_x + 1, max_z - min_z + 1 + if (min(min_x, min_z) < -1875000 or max(max_x, max_z) > 1874999 or + not 1 <= width <= 64 or not 1 <= depth <= 64 or width * depth > MAX_CHUNKS): + raise AssertionError("Readiness bounds exceed the native command limits") + 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"): + 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("dimension") != dimension): + raise AssertionError("Missing or failed native readiness receipt") + if document.get("bounds") != dict(zip(("minX", "minZ", "maxX", "maxZ"), bounds)): + raise AssertionError("Native readiness receipt covers the wrong bounds") + for field in ("checks", "elapsedMillis", "requestedWorldTime", "completedWorldTime"): + if type(document.get(field)) is not int or document[field] < 0: + raise AssertionError("Invalid native readiness clock/check evidence") + if (not 1 <= document["checks"] <= MAX_CHECKS or document["elapsedMillis"] > MAX_WAIT_MILLIS + or document.get("maximumChecks") != MAX_CHECKS or document.get("maximumWaitMillis") != MAX_WAIT_MILLIS + or document["completedWorldTime"] < document["requestedWorldTime"]): + raise AssertionError("Native readiness time bounds were exceeded") + + def states(rows): + if not isinstance(rows, list) or not rows: + raise AssertionError("Missing native readiness chunk states") + result = {} + for row in rows: + if (type(row.get("x")) is not int or type(row.get("z")) is not int + or type(row.get("blockAndEntitiesLoaded")) is not bool + or type(row.get("entityTicking")) is not bool): + raise AssertionError("Invalid native readiness state") + key = row["x"], row["z"] + if key not in expected or key in result: + raise AssertionError("Duplicate or out-of-bounds native readiness chunk") + result[key] = row["blockAndEntitiesLoaded"], row["entityTicking"] + return result + + final = states(document.get("chunks")) + if set(final) != expected or not all(value == (True, True) for value in final.values()): + raise AssertionError("Not every native chunk and entity section is ready") + transitions = document.get("transitions") + if not isinstance(transitions, list) or not transitions: + raise AssertionError("Missing native readiness transitions") + replay, previous_check, previous_time = {}, 0, document["requestedWorldTime"] + for frame in transitions: + check, time = frame.get("check"), frame.get("worldTime") + if (type(check) is not int or not previous_check < check <= document["checks"] + or type(time) is not int or not previous_time <= time <= document["completedWorldTime"]): + raise AssertionError("Invalid native readiness transition order") + changes = states(frame.get("changes")) + if not replay and (check != 1 or set(changes) != expected): + raise AssertionError("Incomplete initial native readiness observation") + if any(replay.get(key) == value for key, value in changes.items()): + raise AssertionError("Native readiness transition did not change state") + replay.update(changes) + if all(value == (True, True) for value in replay.values()) and check != document["checks"]: + raise AssertionError("Native readiness completed before its reported final check") + previous_check, previous_time = check, time + if replay != final or previous_check != document["checks"]: + raise AssertionError("Native readiness transition replay differs from completion") + return document + + +def await_ready(server, runtime, bounds, label, dimension="minecraft:overworld"): + chunk_set(bounds) + if not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", label): + raise AssertionError("Unsafe native readiness label") + path = Path(runtime) / "advantage-works-results" / ("native-readiness-" + label + ".json") + if path.exists(): raise FileExistsError(path) + 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) + server.wait("ADVREADINESS COMPLETE " + label, 10) + return document diff --git a/test-rig/migration/native117_recipes.py b/test-rig/migration/native117_recipes.py new file mode 100644 index 00000000..f9eca6cd --- /dev/null +++ b/test-rig/migration/native117_recipes.py @@ -0,0 +1,360 @@ +"""Native 1.17 recipe exports and progression on new hash-pinned disposable servers. + +Historical campaign modules supply assertion data only. No legacy profile, seed, +world, artifact fallback or historical result is used as native runtime evidence. +""" + +import argparse +from collections import Counter +from datetime import datetime, timezone +import json +from pathlib import Path +import re +import socket +import zipfile + +import nbtlib + +from matrix import WORLD, digest, fingerprint, stage, write_json +from native117_code import Server +from native117 import TEST_FLAT, stage +from native117 import RIG, output_path, pinned_profile, require_clean_log, require_disjoint, runtime_error_lines +from native117_identity import fixture_ids +from native117_readiness import await_ready +from progression import fixture as progression_fixture, validate_tags +from recipe_unlocks import MATERIALS + + +MOD_COUNTS = {"basepower": 35, "basesteam": 24, "baseelectric": 50} +RECIPE_TYPES = {"minecraft:crafting_shaped", "minecraft:crafting_shapeless", "minecraft:smelting"} +UNLOCK_BOOTS = ( + ("NORMAL", "normal", ("sweep", "persist")), + ("NORMAL", "normal-restart", ("restore",)), + ("TECH_PROGRESSION", "tech", ("restore", "sweep", "persist")), + ("APOCALYPTIC", "apocalyptic", ("restore", "sweep", "persist")), + ("NORMAL", "normal-return", ("restore",)), +) +MODES = ("NORMAL", "TECH_PROGRESSION", "APOCALYPTIC") + + +def read_json(path): + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def recipe_fixture(jars): + recipes = [] + sources = {} + for path in jars: + with zipfile.ZipFile(path) as jar: + for mod in MOD_COUNTS: + prefix = f"data/{mod}/recipes/" + names = sorted(name for name in jar.namelist() if name.startswith(prefix) + and name.endswith(".json") and "/" not in name[len(prefix):]) + for name in names: + recipe = json.loads(jar.read(name)) + if recipe["type"] not in RECIPE_TYPES: + continue + stem = Path(name).stem + advancement_name = f"data/{mod}/advancements/recipes/{stem}.json" + advancement = json.loads(jar.read(advancement_name)) + predicates = {} + for key, criterion in advancement["criteria"].items(): + if criterion["trigger"] != "minecraft:inventory_changed": + continue + items = criterion["conditions"]["items"] + if len(items) != 1 or set(items[0]) not in ({"items"}, {"tag"}): + raise AssertionError(f"Unsupported inventory predicate: {advancement_name}/{key}") + if "items" in items[0]: + choices = items[0]["items"] + if (not isinstance(choices, list) or len(choices) != 1 + or not isinstance(choices[0], str) or not choices[0]): + raise AssertionError(f"Expected one explicit native item: {advancement_name}/{key}") + predicates[key] = {"item": choices[0]} + else: + predicates[key] = items[0] + if not predicates: + raise AssertionError(f"No ingredient-discovery predicates: {advancement_name}") + identity = f"{mod}:{stem}" + if identity in sources: + raise AssertionError(f"Duplicate packaged recipe: {identity}") + sources[identity] = {"jar": str(path), "recipe": name, "advancement": advancement_name} + recipes.append({"id": identity, "ingredients": predicates}) + counts = Counter(row["id"].split(":")[0] for row in recipes) + if counts != MOD_COUNTS: + raise AssertionError(f"Packaged recipe inventory changed: {dict(counts)}; expected {MOD_COUNTS}") + return {"recipes": recipes, "materials": fixture_ids(MATERIALS)}, sources + + +def native_target(manifest): + entry = read_json(manifest)["target117"] + required = {"advantageworkstest", "basepower", "basesteam", "baseelectric", "basemetals", "orespawn"} + if {jar["id"] for jar in entry["jars"]} != required: + raise ValueError("This campaign requires exactly the native harness and five packaged mods") + target = pinned_profile(manifest, "target117") + if any("deobf" in path.name.lower() for path in target[2]): + raise ValueError("Use packaged native jars, not development candidates") + return target + + +def require_free_port(port): + with socket.socket() as check: + check.bind(("127.0.0.1", port)) + + +def stage_native(runtime, target, port, campaign): + require_free_port(port) + launcher = stage(runtime, target) + # Replace only the disposable stage's legacy generator and port defaults. + props = dict(line.split("=", 1) for line in (runtime / "server.properties").read_text().splitlines() if "=" in line) + props.update({"server-port": str(port), "generator-settings": "", "level-type": TEST_FLAT}) + (runtime / "server.properties").write_text("".join(f"{key}={value}\n" for key, value in props.items()), encoding="utf-8") + marker = "advantage-recipes.allow" if campaign == "unlock" else "advantage-progression.allow" + (runtime / marker).touch() + if campaign == "progression": + (runtime / "advantage-works.allow").touch() + if digest(runtime / launcher) != digest(target[0] / launcher): + raise AssertionError("Staged launcher differs from native input") + for jar in target[2]: + if digest(runtime / "mods" / jar.name) != digest(jar): + raise AssertionError(f"Staged mod differs from pinned input: {jar}") + if (runtime / WORLD).exists(): + raise AssertionError("Staging must not import an existing world") + return launcher + + +def configure(runtime, mode, rubber=True): + config = runtime / "config/basepower-common.toml" + config.parent.mkdir(exist_ok=True) + config.write_text(f'[options]\nrecipeMode = "{mode}"\nplasticEqualsRubber = {str(rubber).lower()}\n', encoding="utf-8") + + +def validate_unlock(result, definition, operation): + expected = {row["id"] for row in definition["recipes"]} + rows = result["cases"] + if (result["status"] != "PASS" or len(rows) != len(expected) + or {row["id"] for row in rows} != expected or result["potatoCraftingCases"] != 59): + raise AssertionError("Incomplete native unlock/potato-crafting report") + if not result["bookRecipes"]: + raise AssertionError("Native server exported no recipe-book recipes") + if operation == "persist" and result["additionalMaterials"]: + raise AssertionError("Human materials checklist has gaps: " + str(result["additionalMaterials"])) + if operation == "restore" and result.get("nativeForgeLoginEvent") is not True: + raise AssertionError("Saved-book restoration did not dispatch the real native Forge login event") + allowed = {"PASS", "SKIP_NO_INGREDIENT_PROVIDER"} + if any(row["status"] not in allowed for row in rows): + raise AssertionError("Failed native discovery case") + return {"cases": len(rows), "active": sum(row["recipeActive"] for row in rows), + "passed": sum(row["status"] == "PASS" for row in rows), + "skipped": sum(row["status"] != "PASS" for row in rows), "potatoCraftingCases": 59} + + +def validate_progression(result, mode, rubber): + 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") + if result["status"] != "PASS" or result["blocked"] != 0 or result["blockedRecipes"]: + raise AssertionError("Native manufacture blocked: " + str(result.get("blockedRecipes")) + "; " + + str([row["id"] for row in result["targets"] if row["status"] == "BLOCKED"])) + if not result["targets"] or not result["reachable"] or not result["executedRecipes"] or not result["processWitnesses"]: + raise AssertionError("Empty native manufacture evidence") + if not result["lootDataPackOverrideVerified"]: + raise AssertionError("Player loot override was not preserved") + + +def verify_level(runtime, entry): + 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") + 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"]): + 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} + + +def audit_unlock_log(path, previous_recipes, current_recipes): + """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")): + 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: + raise AssertionError(f"Unexpected native recipe runtime error: {original}") + observed.add(match[1]) + events.append(original) + if observed != removed: + raise AssertionError(f"Saved-book pruning did not match the native packet-set difference: expected {sorted(removed)}, saw {sorted(observed)}") + return {"expected_removed_ids": sorted(removed), "observed_removed_ids": sorted(observed), "events": events} + + +class RecipeServer(Server): + def __init__(self, runtime, java, launcher, label, previous): + self.previous = set(previous) + self.current = None + self.pruning = {} + super().__init__(runtime, java, launcher, label) + + 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) + + +def unlock_campaign(root, target, entry, java, port, definition, report): + runtime = root / "unlock" + launcher = stage_native(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) + try: + for operation in operations: + name = f"{label}-{operation}" + server.send(f"advrecipes {operation} {name}") + server.wait(f"ADVRECIPES COMPLETE {operation} {name}", 120) + result = read_json(runtime / f"recipes-{name}.json") + counts = validate_unlock(result, definition, operation) + packet = runtime / f"recipes-{name}.bin" + if not packet.is_file() or packet.stat().st_size == 0: + raise AssertionError("Missing native server recipe packet") + row["operations"].append({"operation": operation, "report": f"recipes-{name}.json", + "packet_sha256": digest(packet), **counts}) + server.current = {recipe["id"] for recipe in result["bookRecipes"]} + server.send("save-all flush") + finally: + server.close() + current_recipes = {recipe["id"] for recipe in result["bookRecipes"]} + if set(server.pruning) != {f"{kind}-{label}.log" for kind in ("console", "latest", "debug")}: + 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) + 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), + "files": [{"file": path.name, "sha256": digest(path)} for path in artifacts]} + + +def prepare_progression(server, runtime, bounds, label, reload): + if reload: + server.send("reload") + server.wait("Reloading!", 60) + server.wait(" advancements", 120) + low, high = bounds["min"], bounds["max"] + 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) + return runtime / "advantage-works-results" / f"native-readiness-{readiness}.json" + + +def progression_campaign(root, target, entry, java, port, report): + runtime = root / "progression" + launcher = stage_native(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 / "data/minecraft/loot_tables/chests/simple_dungeon.json", { + "pools": [{"name": "test_override", "rolls": 1, "entries": [{"type": "item", "name": "minecraft:dirt"}]}]}) + for mode in MODES: + for rubber in (True, False): + 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) + 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) + 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) + 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) + + +def run(args): + manifest = args.target_manifest.resolve() + target = native_target(manifest) + entry = read_json(manifest)["target117"] + root = output_path(args.output) + for protected in (manifest, target[0], *target[2]): + require_disjoint(root, protected) + java = args.java8.resolve() + if not java.is_file() or not re.search(r'(?m)^JAVA_VERSION="16(?:\.|\+|")', (java.parent.parent / "release").read_text()): + raise ValueError("Supply an existing Java 16 bin/java.exe") + require_free_port(args.port) + definition, sources = recipe_fixture(target[2]) + before = fingerprint(target[0]) + root.mkdir(parents=True, exist_ok=False) + report = {"status": "RUNNING", "minecraft": "1.17.1", "forge": "37.1.1", "campaign": args.campaign, + "target_manifest": str(manifest), "target_manifest_sha256": digest(manifest), + "target_jars": entry["jars"], "java": str(java), "java_sha256": digest(java), + "unlock_boots": [], "progression_cases": [], "recipe_inventory": dict(MOD_COUNTS), + "scope": "packaged native server assertions and recipe exports; not graphical/manual acceptance"} + write_json(root / "packaged-recipe-sources.json", sources) + write_json(root / "seed-fingerprint-before.json", before) + try: + if args.campaign in ("all", "unlock"): + unlock_campaign(root, target, entry, java, args.port, definition, report) + if args.campaign in ("all", "progression"): + progression_campaign(root, target, entry, java, args.port, report) + if args.campaign in ("all", "unlock") and len(report["unlock_boots"]) != 5: + raise AssertionError("The native unlock campaign did not complete five boots") + if args.campaign in ("all", "progression") and len(report["progression_cases"]) != 18: + raise AssertionError("The native progression campaign did not complete eighteen checks") + native_target(manifest) + if digest(manifest) != report["target_manifest_sha256"]: + raise AssertionError("Pinned manifest changed during the campaign") + report["status"] = "PASS" + except BaseException as failure: + report.update(status="FAIL", error=f"{type(failure).__name__}: {failure}") + raise + finally: + after = fingerprint(target[0]) + write_json(root / "seed-fingerprint-after.json", after) + report["seed_unchanged"] = before == after + if before != after: + report.update(status="FAIL", seed_error="Protected installed server seed changed") + write_json(root / "native117-recipes-results.json", report) + if before != after: + raise AssertionError("Protected installed server seed changed") + print(f"Native117 recipe campaign PASS: {root}", flush=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java16", "--java8", dest="java8", type=Path, required=True) + parser.add_argument("--output", type=Path, default=RIG / "build" / ("native117-recipes-" + datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S"))) + parser.add_argument("--port", type=int, default=25577) + parser.add_argument("--campaign", choices=("all", "unlock", "progression"), default="all") + run(parser.parse_args()) + + +if __name__ == "__main__": + main() diff --git a/test-rig/migration/native117_redstone_reload.py b/test-rig/migration/native117_redstone_reload.py new file mode 100644 index 00000000..16c8e526 --- /dev/null +++ b/test-rig/migration/native117_redstone_reload.py @@ -0,0 +1,392 @@ +"""Finite mid-obsidian work, frozen holds, then per-tick operation after a real process restart. + +Separate protocol: never changes historical lifecycle helpers or reconstructs a restarted world. +Pin preserves every base production jar unless --electric-jar is explicitly supplied. +""" + +import argparse +import copy +from datetime import datetime, timezone +import json +import math +from pathlib import Path +import shutil +import socket +import struct +import traceback + +import nbtlib + +from matrix import WORLD, digest, fingerprint, write_json +from native117 import output_path, pinned_profile, require_disjoint +from native117_code import Server +from native117_lifecycle import prepare, read_json, require_native_boot, verify_staged +from native117_readiness import await_ready + + +OBSERVATION_TICKS = 16 +READINESS_TICKS = 80 +TARGET_KEYS = {"targetX", "targetY", "targetZ", "TargetStates"} +MUTABLE_KEYS = TARGET_KEYS | {"progress", "Energy"} + + +def require(condition, message): + if not condition: + raise AssertionError(message) + + +def definitions(): + result = [] + for i in range(9): + offset = i if i < 8 else 0 + result.append(dict(id="held-" + str(i) if i < 8 else "unheld", held=i < 8, offset=offset, + pos=[60000 + i * 64 + 14 + ((offset >> 1) & 1), + 80 + (offset & 1), 60000 + ((offset >> 2) & 1)])) + return result + + +def typed(value): + if isinstance(value, nbtlib.Compound): + return {key: typed(tag) for key, tag in value.items()} + if isinstance(value, nbtlib.List): + return ["List", value.subtype.tag_id, [typed(tag) for tag in value]] + raw = value.unpack() + if hasattr(raw, "tolist"): + raw = raw.tolist() + return [type(value).__name__, raw] + + +def parsed(row): + try: + return nbtlib.parse_nbt(row["nbt"]) + except Exception as error: + raise AssertionError("Invalid raw tile NBT: " + str(error)) from error + + +def rows_by_id(rows, holds): + expected = definitions() + require(isinstance(rows, list) and len(rows) == 9, "Missing nine independent witnesses") + require(len({row["id"] for row in rows}) == 9, "Duplicate witness") + actual = {row["id"]: row for row in rows} + require(set(actual) == {case["id"] for case in expected}, "Wrong witness IDs") + for case in expected: + row = actual[case["id"]] + key = case["id"] + require(all(row.get(name) == value for name, value in case.items()), key + ": changed position/phase/declaration") + x, y, z = row["pos"] + require(((z & 1) << 2 | (x & 1) << 1 | (y & 1)) == row["offset"], key + ": false scheduler phase") + signal = holds and case["held"] + require(row.get("signal") is signal, key + ": actual neighbor signal differs") + require(row.get("holdBlock") == ("minecraft:redstone_block" if signal else "minecraft:air"), key + ": wrong hold block") + require(row.get("block") == "baseelectric:electric_drill", key + ": stationary drill changed") + require(type(row.get("active")) is bool, key + ": missing actual ACTIVE") + state = "Block{baseelectric:electric_drill}[active=" + str(row["active"]).lower() + ",facing=north]" + require(row.get("state") == state, key + ": raw block state disagrees with ACTIVE/facing") + require(isinstance(row.get("energy"), (int, float)) and math.isfinite(row["energy"]) + and 0 <= row["energy"] <= 500, key + ": invalid finite reserve") + require(type(row.get("progress")) is int and 0 <= row["progress"] < 14400, key + ": invalid mid-obsidian progress") + targets = row.get("targets", []) + expected_targets = {(x + dx, y + dy, z - 1): "Block{minecraft:obsidian}" + for dx in range(-1, 2) for dy in range(-1, 2)} + require(len(targets) == 9 and {tuple(t["pos"]): t["state"] for t in targets} == expected_targets, + key + ": real 3x3 obsidian target changed") + tag = parsed(row) + require(set(tag).issubset({"id", "x", "y", "z", "Items"} | MUTABLE_KEYS), key + ": unexpected saved state") + require(str(tag.get("id")) == row["block"] and [int(tag.get(axis, -1)) for axis in ("x", "y", "z")] == row["pos"], + key + ": serialized position/identity changed") + energy = tag.get("Energy") + require(isinstance(energy, nbtlib.IntArray) and len(energy) == 1, key + ": wrong energy storage") + value = struct.unpack(">f", struct.pack(">i", int(energy[0])))[0] + require(value == row["energy"], key + ": raw energy and measurement disagree") + require(isinstance(tag.get("progress"), nbtlib.Short) and int(tag["progress"]) == row["progress"], + key + ": raw progress and measurement disagree") + expected_items = nbtlib.parse_nbt('{Items:[{Slot:0b,id:"minecraft:dirt",Count:7b,tag:{AdvWorksWitness:"' + key + + '"}},{Slot:1b,id:"minecraft:apple",Count:13b}]}')["Items"] + require("Items" in tag and typed(tag["Items"]) == typed(expected_items), key + ": inventory/witness changed") + present = set(tag) & TARGET_KEYS + require(not present or present == TARGET_KEYS, key + ": partial target serialization") + if present: + require([int(tag[axis]) for axis in ("targetX", "targetY", "targetZ")] == [x, y, z - 1], key + ": target moved") + states = tag["TargetStates"] + require(isinstance(states, nbtlib.List) and len(states) == 9 + and all(typed(s) == {"Name": ["String", "minecraft:obsidian"]} for s in states), key + ": target oracle changed") + return actual + + +def same_saved(left, right): + a, b = dict(left), dict(right) + a["nbt"], b["nbt"] = typed(parsed(left)), typed(parsed(right)) + return a == b + + +def clock(frame, previous_time, index): + require(frame.get("index") == index, "Missing/reordered real tick index") + require(frame.get("tickStartWorldTime") == previous_time and frame.get("worldTime") == previous_time + 1, + "Missing, repeated or gapped real world tick") + + +def validate_readiness(document, phase, release_time): + samples = document.get("readiness", []) + require(1 <= len(samples) <= READINESS_TICKS, "Missing/beyond-budget native readiness observations") + expected = {case["id"]: case["pos"] for case in definitions()} + fields = ("blockTicking", "tickableListed", "blockAndEntitiesLoaded", "entityTicking") + previous_time = None + for index, sample in enumerate(samples): + require(sample.get("phase") == phase, "Wrong readiness phase") + now = sample.get("worldTime") + require(type(now) is int and (previous_time is None or now == previous_time + 1), + "Readiness did not observe consecutive paused real ticks") + rows = sample.get("cases", []) + require(len(rows) == 9 and len({row["id"] for row in rows}) == 9 + and {row["id"]: row["pos"] for row in rows} == expected, "Incomplete readiness witness set") + require(all(type(row.get(key)) is bool for row in rows for key in fields), + "Missing native block/list/entity readiness measurements") + ready = all(row[key] for row in rows for key in fields) + require(ready is (index == len(samples) - 1), "Release was not the first all-native-ready sample") + previous_time = now + require(previous_time == release_time, "Operated before/after the independently selected readiness boundary") + + +def validate_seed(fixture): + require(fixture.get("schema") == 1 and fixture.get("constructionCallsThisProcess") == 9, + "Not nine fresh-only constructed seeds") + require(fixture.get("initialEnergy") == 500 and fixture.get("processToken"), "Missing finite seed authority") + initial = fixture["initial"] + validate_readiness(fixture, "seed", initial["worldTime"]) + previous = rows_by_id(initial["cases"], False) + for row in previous.values(): + require(row["energy"] == 500 and row["progress"] == 0 and not row["active"], "Seed did not begin empty of work") + require(not (set(parsed(row)) & TARGET_KEYS), "Initial setup injected a mining target") + previous_time = initial["worldTime"] + trace = fixture.get("seedTrace", []) + require(1 <= len(trace) <= 16, "No bounded natural seed trace") + for index, frame in enumerate(trace, 1): + clock(frame, previous_time, index) + current = rows_by_id(frame["cases"], False) + for key, row in current.items(): + old = previous[key] + advance = row["progress"] - old["progress"] + require(advance in (0, 1) and old["energy"] - row["energy"] == 24 * advance, + key + ": seed work was not earned with real debit") + had_target, has_target = bool(set(parsed(old)) & TARGET_KEYS), bool(set(parsed(row)) & TARGET_KEYS) + if not had_target: + require(advance == 0, key + ": mining before target acquisition") + require(not has_target or (frame["worldTime"] + row["offset"]) % 8 == 0, + key + ": target appeared outside a native power-update slot") + else: + require(has_target and advance == 1, key + ": seed stopped operating") + previous, previous_time = current, frame["worldTime"] + before = rows_by_id(fixture["preHold"], False) + frozen = rows_by_id(fixture["cases"], True) + require(fixture.get("worldTime") == previous_time, "Freeze did not follow natural work immediately") + for key, row in before.items(): + require(same_saved(row, previous[key]), key + ": seed changed before the hold") + require(row["progress"] > 0 and row["energy"] > 24 and row["active"] + and set(parsed(row)) & TARGET_KEYS == TARGET_KEYS, key + ": not powered mid-obsidian") + expected = dict(row, signal=row["held"], holdBlock="minecraft:redstone_block" if row["held"] else "minecraft:air") + require(same_saved(expected, frozen[key]), key + ": adding hold altered saved work/resources") + return dict(status="PASS", cases=9, naturalSeedTicks=len(trace), phases=list(range(8))) + + +def validate_reload(fixture, report): + validate_seed(fixture) + require(report.get("schema") == 1 and report.get("constructionCallsThisProcess") == 0, + "Restart reconstructed the world") + require(report.get("processToken") and report["processToken"] != fixture["processToken"], "Not a full-process restart") + require(report.get("held") is True and report.get("observationTicks") == OBSERVATION_TICKS, + "Incomplete or nonquiescent final measurement") + frozen = rows_by_id(fixture["cases"], True) + restored = report["restored"] + validate_readiness(report, "restart", restored["worldTime"]) + previous = rows_by_id(restored["cases"], True) + require(restored["worldTime"] >= fixture["worldTime"], "World time moved backwards across restart") + for key in frozen: + require(same_saved(frozen[key], previous[key]), key + ": complete frozen block/NBT changed during restart") + ticks = report.get("ticks", []) + require(len(ticks) == OBSERVATION_TICKS, "Missing per-real-tick observations") + previous_time = restored["worldTime"] + for index, frame in enumerate(ticks, 1): + clock(frame, previous_time, index) + current = rows_by_id(frame["cases"], True) + for key, row in current.items(): + old = previous[key] + before_tag, after_tag = typed(parsed(old)), typed(parsed(row)) + require({k: v for k, v in before_tag.items() if k not in MUTABLE_KEYS} + == {k: v for k, v in after_tag.items() if k not in MUTABLE_KEYS}, key + ": saved resources changed") + if row["held"]: + require(row["energy"] == frozen[key]["energy"], key + f": held energy debit at tick {index}") + require(row["progress"] <= old["progress"], key + f": held work advanced at tick {index}") + require(row["active"] is False, key + f": held ACTIVE immediately true at tick {index}") + else: + powered = old["energy"] > 24 + require(set(parsed(row)) & TARGET_KEYS == TARGET_KEYS, "Unheld control lost its real mining target") + delta = 1 if powered else (-1 if old["progress"] else 0) + require(row["progress"] == old["progress"] + delta + and row["energy"] == old["energy"] - (24 if powered else 0), + f"Unheld control failed real progress/debit at tick {index}") + require(row["active"] is (row["energy"] > 24), "Unheld ACTIVE disagrees with finite reserve") + previous, previous_time = current, frame["worldTime"] + require(previous["unheld"]["energy"] < frozen["unheld"]["energy"], "Unheld control never consumed finite power") + require(any(frame["cases"] and next(row for row in frame["cases"] if row["id"] == "unheld")["progress"] + > frozen["unheld"]["progress"] for frame in ticks), "Unheld control never advanced work") + return dict(status="PASS", heldWitnesses=8, unheldControls=1, consecutiveRealTicks=OBSERVATION_TICKS, + heldDebit=0, firstTickInactive=True, unheldDebit=frozen["unheld"]["energy"] - previous["unheld"]["energy"]) + + +def pin(args): + base = args.base.resolve() + pinned_profile(base, "target117") + document = read_json(base) + destination = args.output.resolve() + require_disjoint(destination, base) + if destination.exists(): + raise FileExistsError(destination) + replacements = {"advantageworkstest": args.harness.resolve()} + if args.electric_jar: + replacements["baseelectric"] = args.electric_jar.resolve() + target = copy.deepcopy(document["target117"]) + require(set(replacements).issubset({jar["id"] for jar in target["jars"]}), "Missing replaced mod ID in base") + sources = [(jar, replacements.get(jar["id"], (base.parent / jar["path"]).resolve())) for jar in target["jars"]] + for _, source in sources: + require_disjoint(destination, source) + destination.mkdir(parents=True) + for jar, source in sources: + checksum = digest(source) + path = destination / "artifacts" / checksum / source.name + path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, path) + require(digest(path) == checksum, "Archive copy differs") + jar.update(path=str(path), originPath=str(source), sha256=checksum) + if jar["id"] in replacements: + jar["sourceRef"] = "LOCAL_REDSTONE_RELOAD_" + args.label + target.update(seed=str((base.parent / target["seed"]).resolve()), qualificationStatus="UNQUALIFIED_REDSTONE_RELOAD", + scope=args.label) + manifest = destination / "inputs.json" + write_json(manifest, dict(target117=target, baseManifest=str(base), baseSha256=digest(base))) + pinned_profile(manifest, "target117") + print(str(manifest), flush=True) + return 0 + + +def prepare_seed_terrain(server, runtime): + # Load empty terrain only. The Java probe still owns all placement and + # checks that the real block-entity tickers are ready before releasing them. + areas = [] + for row in definitions(): + x, _, z = row["pos"] + low_x, low_z, high_x, high_z = x - 1, z - 1, x + 1, z + 1 + server.send(f"forceload add {low_x} {low_z} {high_x} {high_z}") + chunks = tuple(value // 16 for value in (low_x, low_z, high_x, high_z)) + areas.append(("redstone-seed-" + row["id"], chunks)) + return [await_ready(server, runtime, chunks, label) for label, chunks in areas] + + +def run(args): + output = output_path(args.output) + manifest = args.manifest.resolve() + target = pinned_profile(manifest, "target117") + for protected in (manifest, target[0], *target[2]): + require_disjoint(output, protected) + require(args.java8.is_file() and 1024 <= args.port <= 65535, "Supply existing Java 8 and available nonprivileged port") + with socket.socket() as listener: + listener.bind(("127.0.0.1", args.port)) + launcher = prepare(output, target, [], args.port) + (output / "advantage-redstone-reload.allow").touch() + (output / "migration-operate.allow").touch() + (output / "advantage-works.allow").touch() + helpers = [Path(__file__), Path(__file__).with_name("test_native117_redstone_reload.py")] + helpers += [Path(__file__).with_name(name) for name in ( + "native117.py", "native117_code.py", "native117_lifecycle.py", "native117_readiness.py", + "lifecycle.py", "matrix.py")] + java_root = Path(__file__).parents[1] / "src/main/java/zone/moddev/mc/advantageworks" + helpers += [java_root / "RedstoneReloadProbe.java", java_root / "AdvantageWorksMod.java"] + hashes = {str(path): digest(path) for path in helpers} + report = dict(status="RUNNING", manifest=str(manifest), manifestSha256=digest(manifest), protocolSha256=hashes, + startedAtUtc=datetime.now(timezone.utc).isoformat(), phases={}, + scope="Nine finite naturally earned mid-obsidian seeds; same frozen world, two server processes, 16 real ticks") + for path in helpers: + destination = output / "protocol-sources" / path.name + destination.parent.mkdir(exist_ok=True) + shutil.copy2(path, destination) + result_path = output / "native117-redstone-reload-results.json" + write_json(result_path, report) + server = None + try: + server = Server(output, args.java8, launcher, "redstone-seed") + for command in ("gamerule doMobSpawning false", "gamerule randomTickSpeed 0", "gamerule doWeatherCycle false", "weather clear"): + server.send(command) + report["seedTerrainReadiness"] = prepare_seed_terrain(server, output) + server.send("advredstonereload seed seed") + server.wait("ADVREDSTONE COMPLETE seed", 180) + failure = output / "advantage-works-results/redstone-reload-failure-seed.json" + require(not failure.exists(), "Seed probe failed; retained " + str(failure)) + fixture_path = output / "redstone-reload-fixture.json" + fixture = read_json(fixture_path) + report["phases"]["seed"] = validate_seed(fixture) + report["fixtureSha256"] = digest(fixture_path) + server.send("save-all flush") + server.close() + server = None + require_native_boot(output / "console-redstone-seed.log") + frozen = output / "frozen-save" / WORLD + shutil.copytree(output / WORLD, frozen) + report["frozenSaveSha256"] = fingerprint(frozen) + write_json(result_path, report) + server = Server(output, args.java8, launcher, "redstone-restart") + server.send("advredstonereload resume restarted") + server.wait("ADVREDSTONE COMPLETE restarted", 180) + failure = output / "advantage-works-results/redstone-reload-failure-restarted.json" + require(not failure.exists(), "Restart probe failed; retained " + str(failure)) + measured = read_json(output / "advantage-works-results/redstone-reload-restarted.json") + # Store a failed semantic verdict before shutdown; never resample until green. + try: + report["phases"]["reload"] = validate_reload(fixture, measured) + except AssertionError as error: + report["phases"]["reload"] = dict(status="FAIL", failure=str(error)) + server.send("save-all flush") + server.close() + server = None + require_native_boot(output / "console-redstone-restart.log") + require(digest(fixture_path) == report["fixtureSha256"], "Frozen fixture authority changed") + require(fingerprint(frozen) == report["frozenSaveSha256"], "Archived pre-restart save changed") + verify_staged(output, target) + report["status"] = "PASS" if all(row["status"] == "PASS" for row in report["phases"].values()) else "FAIL" + except BaseException as error: + report.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + finally: + if server: + try: + server.close() + except BaseException as error: + report.update(status="FAIL", shutdownError=str(error)) + report["protocolUnchanged"] = hashes == {str(path): digest(path) for path in helpers} + if not report["protocolUnchanged"]: + report.update(status="FAIL", protocolError="Protocol changed during execution") + if digest(manifest) != report["manifestSha256"]: + report.update(status="FAIL", manifestError="Input manifest changed during execution") + report["completedAtUtc"] = datetime.now(timezone.utc).isoformat() + write_json(result_path, report) + print(report["status"] + ": " + str(output), flush=True) + return int(report["status"] != "PASS") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + p = commands.add_parser("pin") + p.add_argument("--base", type=Path, required=True) + p.add_argument("--harness", type=Path, required=True) + p.add_argument("--electric-jar", type=Path) + p.add_argument("--output", type=Path, required=True) + p.add_argument("--label", required=True) + r = commands.add_parser("run") + r.add_argument("--manifest", type=Path, required=True) + r.add_argument("--output", type=Path, required=True) + r.add_argument("--java8", type=Path, required=True) + r.add_argument("--port", type=int, required=True) + args = parser.parse_args() + return pin(args) if args.command == "pin" else run(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_refresh.py b/test-rig/migration/native117_refresh.py new file mode 100644 index 00000000..bab8c65d --- /dev/null +++ b/test-rig/migration/native117_refresh.py @@ -0,0 +1,110 @@ +"""Rerun archived frozen/connected fixture inputs against a new pinned candidate. + +Previous results select immutable inputs only. Every destination is new and all +state/operating verdicts are recomputed by the existing native runners. +""" + +import argparse +from argparse import Namespace +from pathlib import Path +import socket +import traceback +from unittest.mock import patch + +import native117 +import native117_connected +from matrix import digest, write_json +from native117_lifecycle import read_json + + +def snapshot_name(source, expected): + matches = sorted(p.name for p in source.glob("migration-*.json") if digest(p) == expected) + if not matches: + raise AssertionError("No retained snapshot matches its archived digest") + # Frozen conversion/restart snapshots can be byte-for-byte identical. + # Identity remains the exact archived SHA, never a preferred passing state. + return "migration-restarted.json" if "migration-restarted.json" in matches else matches[0] + + +def run(args): + output = native117.output_path(args.output) + for protected in (args.previous, args.target_manifest): + native117.require_disjoint(output, protected) + native117.pinned_profile(args.target_manifest.resolve(), "target117") + previous = read_json(args.previous) + if previous.get("status") != "PASS": + raise AssertionError("Select a completed archived campaign as fixture provenance") + output.mkdir(parents=True, exist_ok=False) + report = dict(status="RUNNING", scope=args.kind, previous=str(args.previous.resolve()), + previousSha256=digest(args.previous), targetManifest=str(args.target_manifest.resolve()), + targetManifestSha256=digest(args.target_manifest), routes=[]) + rows = previous["routes"] if args.kind == "frozen" else previous["sources"] + for index, row in enumerate(rows): + outcome = dict(index=index, status="RUNNING") + report["routes"].append(outcome) + try: + with socket.socket() as listener: + listener.bind(("127.0.0.1", args.port)) + if args.kind == "frozen": + old = Path(row["runtime"]) / (args.previous_target + "-provenance.json") + if digest(old) != row["provenanceSha256"]: + raise AssertionError("Archived fixture provenance changed") + provenance = read_json(old) + source = Path(provenance["source"]) + hashes = output / f"source-{index:02}-approved-hashes.json" + write_json(hashes, provenance["sourceHashes"]) + selected = snapshot_name(source, provenance["sourceSnapshotSha256"]) + settings = Namespace(source=source, source_manifest=Path(provenance["sourceManifest"]), + source_key=provenance["sourceKey"], source_hashes=hashes, + source_snapshot=selected, target_manifest=args.target_manifest, + java8=args.java8, output=output / f"route-{index:02}") + stage = native117.stage + def native_stage(runtime, profile): + launcher = stage(runtime, profile) + properties = runtime / "server.properties" + lines = [line for line in properties.read_text().splitlines() + if not line.startswith(("server-port=", "generator-settings="))] + properties.write_text("\n".join(lines) + f"\nserver-port={args.port}\ngenerator-settings=\n", encoding="utf-8") + return launcher + with patch.object(native117, "stage", native_stage): + code = native117.run(settings) + outcome.update(source=str(source), sourceSnapshot=selected, runtime=str(settings.output), + comparison=read_json(settings.output / "state-comparison.json")) + else: + old = Path(row["report"]) + if digest(old) != row["sha256"]: + raise AssertionError("Archived connected provenance changed") + provenance = read_json(old) + source = Path(provenance["source"]) + hashes = Path(provenance["sourceHashes"]) + if digest(hashes) != provenance["sourceHashesSha256"]: + raise AssertionError("Archived source fingerprint changed") + settings = Namespace(source=source, source_manifest=Path(provenance["sourceManifest"]), + source_key=provenance["sourceKey"], source_hashes=hashes, + source_checkpoint=provenance["sourceCheckpoint"], target_manifest=args.target_manifest, + java8=args.java8, output=output / f"route-{index:02}", + ticks=512, port=args.port) + code = native117_connected.run(settings) + outcome.update(source=str(source), runtime=str(settings.output), + result=str(settings.output / "native117-connected-results.json")) + outcome["status"] = "PASS" if code == 0 else "FAIL" + except Exception as error: + outcome.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + write_json(output / "native117-refresh-results.json", report) + print(f"{args.kind}/{index + 1}/{len(rows)}: {outcome['status']}", flush=True) + report["status"] = "PASS" if len(rows) == (13 if args.kind == "frozen" else 5) and all( + row["status"] == "PASS" for row in report["routes"]) else "FAIL" + write_json(output / "native117-refresh-results.json", report) + return int(report["status"] != "PASS") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("kind", choices=("frozen", "connected")) + parser.add_argument("--previous", type=Path, required=True) + parser.add_argument("--previous-target", choices=("native114", "native117"), default="native114") + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, default=25595) + raise SystemExit(run(parser.parse_args())) diff --git a/test-rig/migration/native117_resource_audit.py b/test-rig/migration/native117_resource_audit.py new file mode 100644 index 00000000..bcb9a6b2 --- /dev/null +++ b/test-rig/migration/native117_resource_audit.py @@ -0,0 +1,287 @@ +"""Read-only resource equality for frozen migration/restart snapshots, not operation. + +compare_resources accepts JSON paths, migration witness arrays, or connected +documents with a snapshot array. It does not launch anything or write evidence. +Root metadata is ignored; item identity, quantity, slot and item NBT are exact. +Pass each world's enchantment-id map when comparing legacy numeric enchantments. +""" + +import argparse +import base64 +import gzip +import io +import json +import math +from pathlib import Path +import struct + +import nbtlib + +from matrix import WORLD, plain +from native117 import native_normalized, source_normalized +from native117_identity import target_id +from native117_legacy_snbt import parse_legacy_snbt + + +# Counts are container sizes, not highest valid slot numbers. Unknown containers +# still receive structural/quantity checks; callers can supply additional limits. +SLOT_LIMITS = { + "minecraft:chest": 27, "minecraft:trapped_chest": 27, "minecraft:barrel": 27, + "minecraft:hopper": 5, "minecraft:furnace": 3, "minecraft:blast_furnace": 3, + "minecraft:smoker": 3, "minecraft:dispenser": 9, "minecraft:dropper": 9, + "baseelectric:electric_furnace": 10, "baseelectric:electric_crusher": 9, + "baseelectric:electric_oven": 2, "baseelectric:electric_fabricator": 15, + "baseelectric:electric_battery_array": 8, "baseelectric:electric_drill": 6, + "baseelectric:growth_chamber": 9, "baseelectric:growth_chamber_controller": 1, + "baseelectric:plastic_refinery": 1, "baseelectric:electric_pump": 0, + "baseelectric:electric_still": 0, "baseelectric:photovoltaic_generator": 0, + "baseelectric:hydroelectric_generator": 0, "baseelectric:steam_powered_generator": 0, + "baseelectric:laser_turret": 0, "baseelectric:laser_turret_evil": 0, + "basesteam:steam_furnace": 7, "basesteam:steam_crusher": 6, + "basesteam:steam_drill": 5, "basesteam:steam_boiler_coal": 1, + "basepower:coal_fired_still": 1, + "basepower:still": 1, "basepower:fluid_discharge": 0, "basepower:fluid_drain": 0, + "basepower:fluid_metal_tank": 1, "basepower:fluid_storage_tank": 0, + "basepower:item_conveyor": 1, "basepower:item_filter_inventory": 10, + "basepower:item_filter_block": 1, "basepower:item_filter_food": 1, + "basepower:item_filter_fuel": 1, "basepower:item_filter_ore": 1, + "basepower:item_filter_overflow": 1, "basepower:item_filter_plant": 1, + "basepower:item_filter_smelt": 1, "basesteam:steam_boiler_electric": 0, + "basesteam:steam_boiler_geothermal": 0, "basesteam:steam_boiler_oil": 0, + "basesteam:steam_pump": 0, "basesteam:steam_still": 0, "basesteam:steam_tank": 0, + "basesteam:steam_elevator": 0, +} +BATTERIES = { + "baseelectric:lead_acid_battery": 1000, + "baseelectric:nickel_hydride_battery": 2000, + "baseelectric:alkaline_battery": 4000, + "baseelectric:lithium_battery": 8000, +} +SCALARS = frozenset(("Electricity", "energy", "steam", "soil", "Temperature", + "BurnTime", "burnTime", "Fuel", "fuel", "FuelTime", "fuelTime")) +TANKS = frozenset(("Tank", "InputTank", "OutputTank", "TankIn", "TankOut", "WaterTank", "FuelTank")) + + +def _integer(value, label, minimum=0): + if isinstance(value, bool) or not isinstance(value, int) or value < minimum: + raise ValueError(label + ": invalid integer") + return value + + +def _quantity(value, label): + if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0: + raise ValueError(label + ": invalid/nonfinite/negative quantity") + return value + + +def _rows(document): + if isinstance(document, (str, Path)): + document = json.loads(Path(document).read_text(encoding="utf-8")) + if isinstance(document, dict): + document = document.get("snapshot") + if not isinstance(document, list) or not document: + raise ValueError("Expected a nonempty migration witness array or connected snapshot") + return document + + +def _nbt(row): + values = [] + binary = None + if "nbtBase64" in row: + payload = gzip.decompress(base64.b64decode(row["nbtBase64"], validate=True)) + binary = nbtlib.File.parse(io.BytesIO(payload)) + values.append(plain(binary.unpack())) + if "nbt" in row: + # SNBT Float parsing keeps Python decimal precision until serialization. + # Round through its declared NBT type, not a quantity tolerance. + stream = io.BytesIO() + nbtlib.File(parse_legacy_snbt(row["nbt"], binary)).write(stream) + stream.seek(0) + values.append(plain(nbtlib.File.parse(stream).unpack())) + if not values: + return {} + if any(not isinstance(value, dict) for value in values) or any(value != values[0] for value in values[1:]): + raise ValueError("Conflicting or non-compound raw NBT encodings") + return values[0] + + +def _energy(value, label): + if not isinstance(value, list): + raise ValueError(label + ": expected float-bit int array") + result = {} + for channel, bits in enumerate(value): + if isinstance(bits, bool) or not isinstance(bits, int) or not -(1 << 31) <= bits < (1 << 31): + raise ValueError(label + ": invalid signed float-bit integer") + amount = _quantity(struct.unpack(">f", struct.pack(">I", bits & 0xffffffff))[0], label) + if amount != 0: + result[str(channel)] = amount + return result + + +def _tank(value, label): + if not isinstance(value, dict): + raise ValueError(label + ": expected tank compound") + amount = value.get("Amount") + if amount is not None: + _integer(amount, label + ".Amount") + if "Empty" in value: + # native_normalized retains the established presence-based Empty convention. + if amount not in (None, 0): + raise ValueError(label + ": contradictory empty marker and positive fluid") + return None + if amount is None: + raise ValueError(label + ": missing Amount/Empty evidence") + fluid = value.get("FluidName", value.get("FluidID")) + if not isinstance(fluid, str) or not fluid: + raise ValueError(label + ": missing fluid identity") + if "FluidID" in value and "FluidName" in value and value["FluidID"] != value["FluidName"]: + raise ValueError(label + ": conflicting fluid identities") + # A named zero-mB tank and native Empty are the same zero resource budget. + if amount == 0: + return None + result = {"fluid": fluid, "amount": amount} + if value.get("Tag"): + result["tag"] = value["Tag"] + return result + + +def _items(value, label, limit): + if not isinstance(value, list): + raise ValueError(label + ": expected inventory list") + result = {} + for entry in value: + if not isinstance(entry, dict): + raise ValueError(label + ": invalid stack compound") + slot = _integer(entry.get("Slot"), label + ".Slot") + if limit is not None and slot >= limit: + raise ValueError(label + ": slot outside container size " + str(limit)) + if str(slot) in result: + raise ValueError(label + ": duplicate inventory slot " + str(slot)) + count = _integer(entry.get("Count"), label + ".Count", 1) + item = entry.get("id") + if not isinstance(item, str) or not item or item == "minecraft:air" or count > 64: + raise ValueError(label + ": invalid item identity/count") + stack = {key: val for key, val in entry.items() if key != "Slot"} + if item in BATTERIES: + tag = stack.get("tag", {}) + if not isinstance(tag, dict): + raise ValueError(label + ": invalid battery tag") + # BatteryItem's established default is a fully charged new battery. + charge = _quantity(tag.get("energy", BATTERIES[item]), label + ".battery.energy") + if charge > BATTERIES[item]: + raise ValueError(label + ": battery charge exceeds chemistry capacity") + stack["tag"] = dict(tag, energy=charge) + if stack.get("tag") == {}: + stack.pop("tag") + result[str(slot)] = stack + return result + + +def _project(data, block, slot_limits): + result = {} + + def visit(node, prefix=""): + for key, value in node.items(): + path = prefix + key + if key == "Energy": + channels = _energy(value, path) + for channel, amount in channels.items(): result[path + "[" + channel + "]"] = amount + elif key in SCALARS: + amount = _quantity(value, path) + if amount != 0: result[path] = amount + elif key == "Items": + inventory = _items(value, path, slot_limits.get(block) if not prefix else None) + for slot, item in inventory.items(): result[path + "[" + slot + "]"] = item + elif key in TANKS or key.endswith("Tank"): + tank = _tank(value, path) + if tank is not None: result[path] = tank + elif isinstance(value, dict): + # Catch known resource fields inside capability compounds too, while + # ignoring ordinary ForgeData/witness/custom-name metadata. + visit(value, path + ".") + + visit(data) + return result + + +def canonical_resources(document, *, enchantments=None, slot_limits=None, legacy_source=False): + """Return per-position exact budgets; malformed evidence raises ValueError. + + No operation is permitted between the input snapshots. Zero optional reserves + and empty inventories/tanks may be omitted by native serialization. Channel, + tank, slot, fluid and item identities remain separate; station totals cannot + conceal a transfer, duplication or loss. Unknown root metadata is ignored. + """ + limits = dict(SLOT_LIMITS) + if slot_limits: + for name, limit in slot_limits.items(): + limits[name] = _integer(limit, "slot limit") + result = {} + for row in _rows(document): + if not isinstance(row, dict): raise ValueError("Invalid witness row") + pos = tuple(_integer(row.get(key), key, -(1 << 31)) for key in ("x", "y", "z")) + if pos in result: raise ValueError("Duplicate witness position " + str(pos)) + identity = row.get("actualName", row.get("block")) + if not isinstance(identity, str) or not identity: raise ValueError("Missing observed block identity") + block = native_normalized({"id": identity})["id"] + if legacy_source: + block = target_id(block) + if block in limits and not ("nbtBase64" in row or "nbt" in row): + raise ValueError("Missing NBT for known resource container " + str(pos)) + normalize = source_normalized if legacy_source else native_normalized + data = normalize(_nbt(row), enchantments) + result[pos] = {"block": block, "resources": _project(data, block, limits)} + return result + + +def compare_resources(before, after, *, before_enchantments=None, after_enchantments=None, slot_limits=None, + legacy_source=False): + """JSON-safe PASS/FAIL result. Does not mutate inputs or write any files.""" + frames, failures, differences = [], [], [] + for phase, document, enchantments in (("before", before, before_enchantments), ("after", after, after_enchantments)): + try: + frames.append(canonical_resources(document, enchantments=enchantments, slot_limits=slot_limits, + legacy_source=legacy_source and phase == "before")) + except Exception as error: + # Evidence/normalization errors must fail closed, not become an empty budget. + frames.append(None) + failures.append(phase + ": " + type(error).__name__ + ": " + str(error)) + if not failures: + old, new = frames + for pos in sorted(old.keys() | new.keys()): + if pos not in old or pos not in new: + differences.append(dict(position=list(pos), resource="witness", before=old.get(pos), after=new.get(pos))) + continue + if old[pos]["block"] != new[pos]["block"]: + differences.append(dict(position=list(pos), resource="block", before=old[pos]["block"], after=new[pos]["block"])) + a, b = old[pos]["resources"], new[pos]["resources"] + for resource in sorted(a.keys() | b.keys()): + if a.get(resource) != b.get(resource): + differences.append(dict(position=list(pos), resource=resource, before=a.get(resource), after=b.get(resource))) + return dict(status="FAIL" if failures or differences else "PASS", failures=failures, differences=differences, + positions=len(frames[0]) if frames[0] is not None else 0, + scope="Exact per-position resource equality across a frozen migration/restart boundary; not operation") + + +def compare_snapshot_resources(before, after, *, legacy_source=False): + def enchantments(snapshot): + level = nbtlib.load(Path(snapshot).parent / WORLD / "level.dat") + entries = level.get("FML", {}).get("Registries", {}).get("minecraft:enchantments", {}).get("ids", []) + return {int(entry["V"]): str(entry["K"]) for entry in entries} + return compare_resources(before, after, before_enchantments=enchantments(before), + after_enchantments=enchantments(after), legacy_source=legacy_source) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("before", type=Path) + parser.add_argument("after", type=Path) + parser.add_argument("--legacy-source", action="store_true") + args = parser.parse_args() + result = compare_resources(args.before, args.after, legacy_source=args.legacy_source) + print(json.dumps(result, indent=2, sort_keys=True)) + return int(result["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_respawn.py b/test-rig/migration/native117_respawn.py new file mode 100644 index 00000000..a1c184f7 --- /dev/null +++ b/test-rig/migration/native117_respawn.py @@ -0,0 +1,259 @@ +"""Retained dimension/player fixtures through native 1.17 death and respawn.""" + +import argparse +import copy +import itertools +import json +from pathlib import Path +import re +import shutil +import socket + +import nbtlib +from actors import enchantments +from native117_actor_io import semantic, bind, parsed_actor +from dimensions import PLAYER_STATE, exact_item_differences +from native117_actors import ROUTES, baseline +from matrix import WORLD, copy_fixture, differences, digest, fingerprint, parse_nbt, write_json +from native117_code import Server +from native117 import output_path, pinned_profile, require_clean_log, require_disjoint +from respawn import respawn_verdict, spawn_reference + +respawn_verdict = bind(respawn_verdict, semantic=semantic, parse_nbt=parsed_actor) +REQUEST_PROTOCOL = "native117-respawn-request-v1" +REQUEST_INDEX = "respawn-requests.json" +CASE_LABELS = tuple(boot + "-" + keep + "-" + mode + for boot in ("respawn", "respawn-restart") for keep in ("keep", "drop") + for mode in ("forced", "bed", "missing")) +REQUEST_FIELDS = {"protocol", "id", "baseFixtureSha256", "keepInventory", "bedMode"} + + +def read_json(path): + def unique(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError("Duplicate JSON key: " + key) + result[key] = value + return result + return json.loads(Path(path).read_bytes(), object_pairs_hook=unique) + + +def case_options(label): + if not isinstance(label, str) or label not in CASE_LABELS: + raise ValueError("Unsafe or unknown respawn request label") + _, keep, mode = label.rsplit("-", 2) + return keep == "keep", mode + + +def request_file(runtime, label): + case_options(label) + runtime = Path(runtime).resolve() + folder = runtime / "respawn-requests" + if folder.is_symlink() or (folder.exists() and folder.resolve() != folder): + raise ValueError("Respawn request directory must not redirect outside the runtime") + return folder / (label + ".json") + + +def read_request(runtime, label): + path = request_file(runtime, label) + base = Path(runtime) / "dimension-fixture.json" + if path.is_symlink() or not path.is_file() or base.is_symlink() or not base.is_file(): + raise ValueError("Respawn request and base fixture must be regular files") + if path.stat().st_size > 4096: + raise ValueError("Respawn request exceeds the explicit size limit") + value = read_json(path) + keep, mode = case_options(label) + if (not isinstance(value, dict) or set(value) != REQUEST_FIELDS + or value.get("protocol") != REQUEST_PROTOCOL or value.get("id") != label + or type(value.get("keepInventory")) is not bool or value["keepInventory"] is not keep + or value.get("bedMode") != mode + or not isinstance(value.get("baseFixtureSha256"), str) + or not re.fullmatch(r"[A-F0-9]{64}", value["baseFixtureSha256"]) + or digest(base) != value["baseFixtureSha256"]): + raise ValueError("Respawn request schema, label options or immutable base hash differ") + return dict(value, path="respawn-requests/" + label + ".json", sha256=digest(path)) + + +def publish_request(runtime, label, keep, mode): + expected_keep, expected_mode = case_options(label) + if type(keep) is not bool or keep is not expected_keep or mode != expected_mode: + raise ValueError("Respawn request options do not match the exact case label") + path = request_file(runtime, label) + base = Path(runtime) / "dimension-fixture.json" + if base.is_symlink() or not base.is_file(): + raise ValueError("Immutable base fixture is not a regular file") + value = dict(protocol=REQUEST_PROTOCOL, id=label, baseFixtureSha256=digest(base), + keepInventory=keep, bedMode=mode) + path.parent.mkdir(exist_ok=True) + # Java reads only after the command is sent; exclusive creation closes first. + with path.open("xb") as stream: + stream.write((json.dumps(value, indent=2, sort_keys=True) + "\n").encode("utf-8")) + return read_request(runtime, label) + + +def verify_request(runtime, label, record, expected): + actual = read_request(runtime, label) + canonical = lambda value: json.dumps(value, sort_keys=True, separators=(",", ":")) + if canonical(actual) != canonical(expected) or canonical(record.get("respawnRequest")) != canonical(actual): + raise AssertionError("Native respawn request acknowledgement or published index differs") + definition = read_json(Path(runtime) / "dimension-fixture.json") + options = definition.setdefault("respawn", {}) + if not isinstance(options, dict): + raise ValueError("Invalid base respawn options") + options.update(keepInventory=actual["keepInventory"], bedMode=actual["bedMode"]) + native = record.get("respawn", {}) + if (record.get("operation") != "respawn" or canonical(record.get("definition")) != canonical(definition) + or type(native.get("keepInventory")) is not bool + or native["keepInventory"] is not actual["keepInventory"] or native.get("bedMode") != actual["bedMode"]): + raise AssertionError("Native respawn changed the root definition or ignored its validated options") + return actual + + +def verify_request_files(runtime, labels): + labels = tuple(labels) + if not labels or len(set(labels)) != len(labels): + raise ValueError("Require a nonempty unique respawn case set") + for label in labels: + case_options(label) + folder = Path(runtime) / "respawn-requests" + actual = set(folder.iterdir()) if folder.is_dir() else set() + expected = {request_file(runtime, label) for label in labels} + if actual != expected: + raise AssertionError("Respawn request directory has missing or unexpected case files") + return {label: read_request(runtime, label) for label in labels} + + +def native_respawn_verdict(record, registry, world_spawn): + # Only translate the exact renamed platform class. Unknown handlers still fail. + value = copy.deepcopy(record) + if value.get("respawn", {}).get("handler") == "ServerPlayNetHandler.PERFORM_RESPAWN": + value["respawn"]["handler"] = "NetHandlerPlayServer.PERFORM_RESPAWN" + return respawn_verdict(value, registry, world_spawn) + + +def run(args): + from native117 import boot + root = output_path(args.output) + require_disjoint(root, args.fixtures) + 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) + results = {} + sources = {} + requests = {"protocol": REQUEST_PROTOCOL, "cases": {}} + write_json(root / REQUEST_INDEX, requests) + for route in args.routes: + source = args.fixtures / route + before = fingerprint(source) + 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) + copy_fixture(source, runtime) + shutil.copy2(source / "dimension-fixture.json", runtime / "dimension-fixture.json") + props = runtime / "server.properties" + props.write_text(props.read_text().replace("allow-nether=false", "allow-nether=true")) + definition = read_json(runtime / "dimension-fixture.json") + base_sha = sources[route]["baseFixtureSha256"] + if digest(runtime / "dimension-fixture.json") != base_sha: + raise AssertionError("Copied immutable dimension fixture differs from the source") + player_file = runtime / WORLD / "playerdata" / (definition["playerUuid"] + ".dat") + expected_player = baseline(source, "dimensions")[0]["file"] + expected_items = {key: expected_player[key] for key in ("Inventory", "EnderItems")} + migrated_player = None + try: + for boot_label in ("respawn", "respawn-restart"): + server = Server(runtime, args.java8, launcher, boot_label) + try: + if migrated_player is None: + # Let the real pre-datafix world hook migrate the copy once. + # Independent deaths restore that native player, not raw old bytes + # after the hook has already completed for this process. + migrated_player = player_file.read_bytes() + bootstrap = semantic(nbtlib.load(player_file).unpack(), PLAYER_STATE, enchantments(runtime)) + bootstrap_issues = differences(expected_items, bootstrap, "initial-migration") + bootstrap_issues.extend(exact_item_differences(expected_items, bootstrap, "initial-migration")) + if bootstrap_issues: + write_json(runtime / "bootstrap-failure.json", bootstrap_issues) + raise AssertionError("Native player bootstrap lost or changed source inventory") + folder = runtime / "bootstrap-evidence" + folder.mkdir() + shutil.copy2(player_file, folder / "player.dat") + shutil.copy2(runtime / WORLD / "level.dat", folder / "level.dat") + for keep, mode in itertools.product((True, False), ("forced", "bed", "missing")): + label = boot_label + "-" + ("keep" if keep else "drop") + "-" + mode + player_file.write_bytes(migrated_player) + if digest(runtime / "dimension-fixture.json") != base_sha: + raise AssertionError("Immutable dimension fixture changed before respawn") + binding = publish_request(runtime, label, keep, mode) + case = route + "/" + label + requests["cases"][case] = {"request": binding, "recordSha256": None} + write_json(root / REQUEST_INDEX, requests) + server.send("advdimensions respawn " + label) + server.wait("ADVDIMENSIONS COMPLETE " + label, 120) + server.send("save-all flush") + server.wait("Saved the", 45) + record_path = runtime / f"dimensions-{label}.json" + record = read_json(record_path) + verify_request(runtime, label, record, binding) + requests["cases"][case]["recordSha256"] = digest(record_path) + write_json(root / REQUEST_INDEX, requests) + registry = enchantments(runtime) + verdict = native_respawn_verdict(record, registry, spawn_reference(runtime / WORLD / "level.dat")) + observed_before = semantic(parse_nbt(record["respawn"]["before"]), PLAYER_STATE, registry) + source_issues = differences(expected_items, observed_before, "source-inventory") + source_issues.extend(exact_item_differences(expected_items, observed_before, "source-inventory")) + verdict["differences"].extend(source_issues) + if source_issues: + verdict["status"] = "FAIL" + saved = semantic(nbtlib.load(player_file).unpack(), PLAYER_STATE, registry) + observed = semantic(parse_nbt(record["respawn"]["after"]), PLAYER_STATE, registry) + issues = differences(observed, saved, "saved-player") + exact_item_differences(observed, saved, "saved-player") + verdict["differences"].extend(issues) + if issues: + verdict["status"] = "FAIL" + folder = runtime / "respawn-evidence" / label + folder.mkdir(parents=True) + shutil.copy2(player_file, folder / "player.dat") + shutil.copy2(runtime / WORLD / "level.dat", folder / "level.dat") + shutil.copy2(runtime / binding["path"], folder / "request.json") + if digest(folder / "request.json") != binding["sha256"]: + raise AssertionError("Archived respawn request differs") + results[route + "/" + label] = verdict + write_json(root / "results.json", results) + print(route + "/" + label + "=" + verdict["status"], flush=True) + finally: + server.close() + require_clean_log(runtime / ("console-" + boot_label + ".log")) + verify_request_files(runtime, CASE_LABELS) + finally: + unchanged = fingerprint(source) == before + base_unchanged = digest(runtime / "dimension-fixture.json") == base_sha + write_json(runtime / "source-integrity.json", {"unchanged": unchanged, + "baseFixtureUnchanged": base_unchanged, "baseFixtureSha256": base_sha}) + if not unchanged: + raise AssertionError("Protected respawn source changed") + if not base_unchanged: + raise AssertionError("Immutable dimension fixture changed during respawn") + status = "FAIL" if any(result["status"] != "PASS" for result in results.values()) else "PASS" + write_json(root / "summary.json", {"status": status, "cases": len(results), "results": results, + "targetManifest": str(args.target_manifest.resolve()), "targetManifestSha256": digest(args.target_manifest), + "sourceFixtures": "UNCHANGED", "realClientLoginAndDeathScreen": "NOT_TESTED", + "requestProtocol": REQUEST_PROTOCOL, "requestIndexSha256": digest(root / REQUEST_INDEX), + "oracleAdapter": "Exact ServerPlayNetHandler to NetHandlerPlayServer class rename only"}) + return int(status != "PASS") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--fixtures", type=Path, required=True) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, choices=(25574, *range(25584, 25588)), required=True) + parser.add_argument("--routes", nargs="+", choices=ROUTES, + default=["original110", "maintained112", "original110-chained-113"]) + raise SystemExit(run(parser.parse_args())) diff --git a/test-rig/migration/native117_save_io.py b/test-rig/migration/native117_save_io.py new file mode 100644 index 00000000..ef493592 --- /dev/null +++ b/test-rig/migration/native117_save_io.py @@ -0,0 +1,113 @@ +"""Retain and replay native saved-format evidence from disposable destinations only.""" + +import gzip +import io +import json +from pathlib import Path +import re +import shutil +import zlib + +import nbtlib + +from matrix import WORLD, digest, plain, write_json +from native117_format import decode_palette, worldgen_settings +from transformer_order import read_region + + +def chunk_data(payload): + if len(payload) < 5 or int.from_bytes(payload[:4], 'big') != len(payload) - 4: + raise ValueError('Malformed retained chunk length') + decoders = {1: gzip.decompress, 2: zlib.decompress, 3: lambda value: value} + if payload[4] not in decoders: + raise ValueError('Unsupported native chunk compression') + data = plain(nbtlib.File.parse(io.BytesIO(decoders[payload[4]](payload[5:]))).unpack()) + if data.get('DataVersion') != 2730: + raise ValueError('Saved chunk is not native Minecraft 1.17.1 DataVersion 2730') + return data['Level'] + + +def sections(chunk): + result = {} + for section in chunk['Sections']: + y = section['Y'] + if y in result: + raise ValueError('Duplicate saved section') + if 'Blocks' in section or 'Data' in section or 'Add' in section: + raise ValueError('Legacy numeric chunk section is not native117 evidence') + if 'Palette' in section: + result[y] = decode_palette(section['Palette'], section.get('BlockStates', [])) + elif 'BlockStates' in section: + raise ValueError('BlockStates without a palette') + else: + result[y] = None + return result + + +def audit_save(runtime, receipt): + root = (runtime / receipt['root']).resolve() + if not root.is_relative_to(runtime.resolve() / 'native117-save-format'): + raise ValueError('Saved-format evidence escaped its disposable destination') + for name, checksum in receipt['files'].items(): + path = (root / name).resolve() + if not path.is_relative_to(root) or digest(path) != checksum: + raise AssertionError('Retained native save evidence changed: ' + name) + actual_settings = worldgen_settings(plain(nbtlib.load(root / 'level.dat')['Data'].unpack())) + if actual_settings != receipt['worldGenSettings']: + raise AssertionError('Retained WorldGenSettings differs from raw level.dat') + rows = json.loads((root / 'snapshot.json').read_text(encoding='utf-8')) + cache = {} + for row in rows: + x, y, z = (int(row[key]) for key in ('x', 'y', 'z')) + key = (x // 16, z // 16) + if key not in cache: + chunk = chunk_data((root / 'chunks' / ('%d_%d.chunk' % key)).read_bytes()) + if (chunk['xPos'], chunk['zPos']) != key: + raise AssertionError('Saved chunk coordinate differs from witness') + cache[key] = sections(chunk) + blocks = cache[key].get(y // 16) + state = blocks[((y & 15) << 8) | ((z & 15) << 4) | (x & 15)] if blocks else {'Name': 'minecraft:air'} + if (state['Name'], state.get('Properties', {})) != (row['actualName'], row['properties']): + raise AssertionError('Saved block differs from native snapshot: ' + str((x, y, z))) + padded = sum(blocks is not None for chunk in cache.values() for blocks in chunk.values()) + if receipt['witnesses'] != len(rows) or not rows or receipt['paddedSections'] != padded: + raise AssertionError('Saved-format witness/section count differs') + if receipt['status'] != 'PASS': + raise AssertionError('Saved-format receipt did not pass') + return receipt + + +def capture_save(runtime, label, snapshot=None): + if not re.fullmatch(r'[A-Za-z0-9_-]+', label): + raise ValueError('Unsafe saved-format label') + root = runtime / 'native117-save-format' / label + root.mkdir(parents=True, exist_ok=False) + world = runtime / WORLD + snapshot = snapshot or runtime / ('migration-' + label + '.json') + shutil.copy2(world / 'level.dat', root / 'level.dat') + shutil.copy2(snapshot, root / 'snapshot.json') + rows = json.loads((root / 'snapshot.json').read_text(encoding='utf-8')) + region_cache, chunk_cache = {}, {} + (root / 'chunks').mkdir() + for row in rows: + cx, cz = int(row['x']) // 16, int(row['z']) // 16 + if (cx, cz) in chunk_cache: + continue + region = (cx // 32, cz // 32) + if region not in region_cache: + _, region_cache[region] = read_region(world / 'region' / ('r.%d.%d.mca' % region)) + payload = region_cache[region][(cx & 31) + (cz & 31) * 32] + (root / 'chunks' / ('%d_%d.chunk' % (cx, cz))).write_bytes(payload) + chunk_cache[(cx, cz)] = sections(chunk_data(payload)) + receipt = dict(status='PASS', root=str(root.relative_to(runtime)), + worldGenSettings=worldgen_settings(plain(nbtlib.load(root / 'level.dat')['Data'].unpack())), + witnesses=len(rows), paddedSections=sum(blocks is not None for chunk in chunk_cache.values() for blocks in chunk.values()), + files={str(path.relative_to(root)): digest(path) for path in sorted(root.rglob('*')) if path.is_file()}) + audit_save(runtime, receipt) + write_json(root / 'receipt.json', receipt) + return receipt + + +def require_same_settings(first, second): + if first['worldGenSettings'] != second['worldGenSettings']: + raise AssertionError('Full native WorldGenSettings changed across restart') diff --git a/test-rig/migration/native117_sources.py b/test-rig/migration/native117_sources.py new file mode 100644 index 00000000..02b1c218 --- /dev/null +++ b/test-rig/migration/native117_sources.py @@ -0,0 +1,266 @@ +"""Explicit 1.15 source receipts; source approval is not 1.16 qualification.""" + +from argparse import Namespace +import json +from pathlib import Path +import types +from unittest.mock import patch + +import connected +import matrix +import native115 +import native115_connected +import native117 +from matrix import WORLD, digest, fingerprint + + +ACCEPTED_POWER115 = "5b8e73a78391960643420a09798a1ef8eb06f518" +HISTORICAL = ("early110", "legacy112", "maintained110", "maintained112", "original110") +SOURCE115 = {"frozen": {"native115/full"}, + "connected": {"native115/" + name for name in HISTORICAL}} +ARCHIVED115_F_SHA256 = "A9BCD5A442F6332010F9D467BD00A685AA953339C39545812B32B825B96E64AA" +ARCHIVED115_ROUTES_SHA256 = "A5241F5F41426DC57DD85A15B62B211415BE8B69D004154F8FC47DDF239B35C2" +CONNECTED_CASES = {"C-CONVEYOR", "C-CRUSHER", "C-EDRILL", "C-ELECTRIC", "C-SDRILL", "C-STEAM", "battery"} + + +def pending_routes(): + return [dict(id=identity, kind=kind, sourceStatus="REQUIRES_APPROVED_DISPOSABLE_115_SOURCE") + for kind, identities in SOURCE115.items() for identity in sorted(identities)] + + +def require_source115_receipt(row): + if not row["id"].startswith("native115/"): + return + import json + if row.get("sourceStatus") != "GENERATED_DISPOSABLE_115_VERIFIED": + raise AssertionError("Native115 requires an explicit stopped-source receipt; prior destination PASS is not approval") + path = Path(row["source115Receipt"]) + if digest(path) != row["source115ReceiptSha256"].upper(): + raise AssertionError("Native115 source receipt changed") + receipt = json.loads(path.read_text()) + source = Path(row["source"]) + if (receipt.get("status") != "PASS" or receipt.get("scope") != "DISPOSABLE_NATIVE115_SOURCE_ONLY" + or receipt.get("acceptedPower115") != ACCEPTED_POWER115 or receipt.get("stoppedCleanly") is not True + or receipt.get("sourceRuntime") != str(source.resolve()) + or receipt.get("runtimeFingerprint") != fingerprint(source) + or receipt.get("worldFingerprint") != fingerprint(source / WORLD)): + raise AssertionError("Native115 stopped-source identity/fingerprint changed or was never approved") + manifest = Path(receipt["target115Manifest"]) + if digest(manifest) != receipt["target115ManifestSha256"].upper(): + raise AssertionError("Native115 source manifest changed") + native115.pinned_profile(manifest, "target115") + if row["kind"] == "frozen": + if (receipt.get("blocks"), receipt.get("tiles")) != (355, 332): + raise AssertionError("Native115 comprehensive source must retain all 355 blocks and 332 tiles") + elif (receipt.get("preservationStatus") != "PASS" + or receipt.get("requestedTicks") != [512, 512] + or receipt.get("advancedTicks") != [512, 512]): + raise AssertionError("Native115 connected source requires both exact 512-tick boots and preservation") + + +def _read(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def _approved(path, checksum, description): + path = Path(path).resolve() + if digest(path) != checksum.upper(): + raise AssertionError("Changed " + description + ": " + str(path)) + return path + + +def _seed_code(seed): + result = {str(Path("libraries") / name): checksum for name, checksum in fingerprint(seed / "libraries").items()} + result.update({path.name: digest(path) for path in seed.glob("*.jar") if "installer" not in path.name}) + return result + + +def source115_settings(args, recipe, output): + return Namespace(source=Path(recipe["source"]), source_manifest=Path(recipe["sourceManifest"]), + source_key=recipe["sourceKey"], source_hashes=Path(recipe["sourceHashes"]), + source_snapshot=recipe["sourceSnapshot"], source_checkpoint=recipe["sourceSnapshot"], + source_fixture_key=recipe.get("sourceFixtureKey"), target_manifest=Path(args.target115_manifest).resolve(), + java8=Path(args.java8).resolve(), output=output, ticks=512, port=args.port) + + +def preflight_source115(args): + """Read-only approval checks. No output directory, process, or canonical write.""" + if not 1 <= args.port <= 65535: + raise ValueError("A leased server port is required") + output = native117.output_path(args.output) + manifest = _approved(args.target115_manifest, ARCHIVED115_F_SHA256, "archived F manifest") + routes = _approved(args.source_routes, ARCHIVED115_ROUTES_SHA256, "archived source plan") + target = native115.pinned_profile(manifest, "target115") + for protected in (manifest, routes, target[0], *target[2]): + native117.require_disjoint(output, protected) + files = {str(manifest): digest(manifest), str(routes): digest(routes)} + canonical = getattr(args, "routes", None) + if canonical is not None: + canonical = Path(canonical).resolve() + native117.require_disjoint(output, canonical) + files[str(canonical)] = digest(canonical) + protocols = (matrix, connected, native115, native115_connected) + files.update({str(Path(module.__file__).resolve()): digest(Path(module.__file__)) for module in protocols}) + rows, runtimes = [], {} + recipes = _read(routes)["routes"] + selections = [("frozen", "native115/full", "legacy/original110-chained-113")] + selections += [("connected", "native115/" + name, "legacy/" + name) for name in HISTORICAL] + for kind, identity, selected in selections: + found = [row for row in recipes if row["kind"] == kind and row["id"] == selected] + if len(found) != 1: + raise AssertionError("Each approved native115 source recipe is required exactly once: " + selected) + recipe = found[0] + if canonical is not None: + matches = [row for row in _read(canonical)["routes"] if row["kind"] == kind and row["id"] == selected] + keys = ("source", "sourceManifestSha256", "sourceKey", "sourceHashesSha256", "sourceSnapshotSha256") + if len(matches) != 1 or any(matches[0].get(key) != recipe.get(key) for key in keys): + raise AssertionError("Canonical native117 recipe differs from the sealed F source lineage: " + selected) + source = Path(recipe["source"]).resolve() + native117.require_disjoint(output, source) + source_manifest = _approved(recipe["sourceManifest"], recipe["sourceManifestSha256"], "source manifest") + hashes = _approved(recipe["sourceHashes"], recipe["sourceHashesSha256"], "source hashes") + filename = recipe["sourceSnapshot"] if kind == "frozen" else "migration-" + recipe["sourceSnapshot"] + ".json" + snapshot = (source / filename).resolve() + if snapshot.parent != source or not filename.startswith("migration-") or not filename.endswith(".json"): + raise ValueError("Unsafe retained source snapshot") + _approved(snapshot, recipe["sourceSnapshotSha256"], "source snapshot") + for protected in (source_manifest, hashes): + native117.require_disjoint(output, protected) + if not _read(hashes) or fingerprint(source / WORLD) != _read(hashes): + raise AssertionError("Approved source world fingerprint changed: " + str(source)) + native115.pinned_profile(source_manifest, recipe["sourceKey"]) + if kind == "frozen": + document = _read(snapshot) + if (len(document), sum("nbtBase64" in row for row in document)) != (355, 332): + raise AssertionError("Source must retain all 355 blocks and 332 tiles") + else: + native115_connected.verify_source(source115_settings(args, recipe, output / identity.replace("/", "-"))) + files.update({str(path): digest(path) for path in (source_manifest, hashes, snapshot)}) + runtimes[str(source)] = fingerprint(source) + rows.append(dict(id=identity, kind=kind, recipe=recipe)) + return dict(output=str(output), target115Manifest=str(manifest), target115ManifestSha256=digest(manifest), + sourcePlan=str(routes), sourcePlanSha256=digest(routes), sources=rows, files=files, + protectedRuntimes=runtimes, seed=str(target[0]), seedCodeFingerprint=_seed_code(target[0])) + + +def verify_source115_inputs(plan): + for name, checksum in plan["files"].items(): + _approved(name, checksum, "source-generation input/protocol") + if _seed_code(Path(plan["seed"])) != plan["seedCodeFingerprint"]: + raise AssertionError("Protected native115 seed code changed") + for name, before in plan["protectedRuntimes"].items(): + if fingerprint(Path(name)) != before: + raise AssertionError("Protected canonical source runtime changed: " + name) + + +def run_source115(settings, kind): + """Reuse historical runner code; observe only the newly staged process exits.""" + stops = [] + expected = settings.output if kind == "frozen" else settings.output / "target115" + + def check_runtime(runtime, launcher): + if runtime.resolve() != expected.resolve() or launcher != native115.LAUNCHER: + raise AssertionError("Only the NEW disposable native115 copy may launch") + + def stopped(server): + stops.append(dict(label=Path(server.log.name).stem.removeprefix("console-"), + returncode=server.process.returncode, commands=list(server.commands))) + + if kind == "frozen": + class FrozenServer(matrix.Server): + def __init__(self, runtime, java, launcher, label): + check_runtime(runtime, launcher) + self.commands = [] + super().__init__(runtime, java, launcher, label) + + def send(self, command): + if command not in ("save-all flush", "stop") and not command.startswith("advprobe snapshot "): + raise AssertionError("Forbidden source-generation mutation: " + command) + self.commands.append(command) + super().send(command) + + def close(self): + super().close() + stopped(self) + + snapshot = types.FunctionType(matrix.start_snapshot.__code__, + dict(matrix.start_snapshot.__globals__, Server=FrozenServer), + matrix.start_snapshot.__name__, matrix.start_snapshot.__defaults__) + original_stage = native115.stage + + def stage(runtime, target): + launcher = original_stage(runtime, target) + path = runtime / "server.properties" + lines = [line for line in path.read_text().splitlines() if not line.startswith(("server-port=", "generator-settings="))] + path.write_text("\n".join(lines) + f"\nserver-port={settings.port}\ngenerator-settings=\n", encoding="utf-8") + return launcher + + with patch.object(native115, "start_snapshot", snapshot), patch.object(native115, "stage", stage): + code = native115.run(settings) + else: + base = native115_connected.NativeConnectedServer + + class ConnectedServer(base): + def __init__(self, runtime, java, launcher, label, on_loaded): + check_runtime(runtime, launcher) + super().__init__(runtime, java, launcher, label, on_loaded) + + def close(self): + super().close() + stopped(self) + + with patch.object(native115_connected, "NativeConnectedServer", ConnectedServer): + code = native115_connected.run(settings) + return code, stops + + +def source115_result(kind, settings, stops): + if ([row.get("label") for row in stops] != ["converted", "restarted"] or any( + row.get("returncode") != 0 or not {"save-all flush", "stop"}.issubset(row.get("commands", [])) for row in stops)): + raise AssertionError("Both native115 boots must be cleanly saved and stopped") + runtime = settings.output if kind == "frozen" else settings.output / "target115" + for label in ("converted", "restarted"): + log = runtime / ("console-" + label + ".log") + native115_connected.require_native_boot(log) + text = log.read_text(encoding="utf-8") + if not all(marker in text for marker in ("Saved the game", "Stopping server", "Saving worlds", "All chunks are saved")): + raise AssertionError("Missing actual native115 save/shutdown evidence") + proof = dict(stoppedCleanly=True, processStops=stops) + if kind == "frozen": + report = _read(runtime / "state-comparison.json") + for gate in ("converted", "restarted", "idempotence"): + selected = report.get(gate, {}) + if selected.get("status") != "PASS" or any(selected.get("witnesses", {}).get(key) != count + for key, count in (("blocks", 355), ("tiles", 332))): + raise AssertionError("Native115 comprehensive semantic comparison/counts failed") + rows = _read(runtime / "migration-restarted.json") + if (len(rows), sum("nbtBase64" in row for row in rows)) != (355, 332): + raise AssertionError("Saved native115 source must retain 355 blocks and 332 tiles") + proof.update(blocks=355, tiles=332, semanticComparison=report) + return runtime, "migration-restarted.json", None, proof + report = _read(settings.output / "native115-connected-results.json") + required = {label + "_" + gate for label in ("converted", "restarted") + for gate in ("preservation", "operation", "native_boot")} | {"real_output_across_restarts"} + if (report.get("status") != "PASS" or report.get("sourcePreservation", {}).get("status") != "PASS" + or set(report.get("results", {})) != required or any(row.get("status") != "PASS" for row in report["results"].values())): + raise AssertionError("Native115 connected source failed preservation/operating gates") + definition = _read(runtime / "connected-fixture.json") + requested, advanced = [], [] + for label in ("converted", "restarted"): + loaded = _read(runtime / ("connected-" + label + "-loaded.json")) + worked = _read(runtime / ("connected-" + label + "-worked.json")) + if loaded.get("requestedTicks") != 0 or loaded.get("advancedTicks") != 0: + raise AssertionError("Native115 loaded source must be frozen before operation") + for document in (loaded, worked): + state = native115_connected.require_document(document, definition, destination=True) + if set(state["cases"]) != CONNECTED_CASES: + raise AssertionError("Native115 source lost a connected lane/drill/battery") + if connected.operation(loaded, worked, 512)["status"] != "PASS": + raise AssertionError("Native115 raw source did not operate for an exact 512-tick interval") + requested.append(worked["requestedTicks"]) + advanced.append(worked["advancedTicks"]) + if requested != [512, 512] or advanced != [512, 512]: + raise AssertionError("Native115 source requires exact 512+512 measured ticks") + proof.update(preservationStatus="PASS", requestedTicks=requested, advancedTicks=advanced, + connectedCases=sorted(CONNECTED_CASES), operatingComparison=report["results"]) + return runtime, "restarted-worked", definition["sourceProfile"], proof diff --git a/test-rig/migration/native117_sources116.py b/test-rig/migration/native117_sources116.py new file mode 100644 index 00000000..8b555a62 --- /dev/null +++ b/test-rig/migration/native117_sources116.py @@ -0,0 +1,546 @@ +"""Create stopped 1.16 sources for 1.17; never promote an old destination PASS. + +``prepare`` is read-only. ``run`` launches only NEW disposable copies through the +unchanged native116 runners. It requires a separately leased port. This is source +approval only: a successful receipt says nothing about 1.17 migration or operation. +""" + +import argparse +from argparse import Namespace +import hashlib +import json +from pathlib import Path +import re +import shutil +import traceback +import types +from unittest.mock import patch + +import connected +import matrix +import native116 +import native116_code +import native116_connected +import native116_save_io +from matrix import WORLD, digest, fingerprint, write_json +from native116_resource_audit import compare_snapshot_resources +from semantic_machine_audit import connected_interval + + +RIG = Path(__file__).resolve().parents[1] +ACCEPTED_POWER116 = "c310d0ca63ad9a8408dd4bba75172afafa5c71b7" +TARGET116_MANIFEST = Path("D:/PowerAdvantage/_verification/backport-coverage-20261030/pa116-candidate-c/inputs.json") +TARGET116_SHA256 = "D191DADA303689D6575ABC2BE63DD5439499A048F433E84FB0CDEA598D1483BB" +SOURCE_ROUTES = Path("D:/PowerAdvantage/_verification/advantage-116-port-20261027/Source Recovery B/native116-upgrade-routes-ready.json") +SOURCE_ROUTES_SHA256 = "CB2C1D8151D3C8262B057A888AB826278B66646426410B31E4B5EA056A5892D9" +HISTORICAL = ("early110", "legacy112", "maintained110", "maintained112", "original110") +SOURCE116 = {"frozen": {"native116/full"}, "connected": {"native116/" + name for name in HISTORICAL}} +SCOPE = "DISPOSABLE_NATIVE116_SOURCE_ONLY" +SOURCE_STATUS = "GENERATED_DISPOSABLE_116_VERIFIED" +CONNECTED_CASES = {"C-CONVEYOR", "C-CRUSHER", "C-EDRILL", "C-ELECTRIC", "C-SDRILL", "C-STEAM", "battery"} +# Git's LF-normalized bytes at the accepted baseline; runtime custody uses raw bytes. +RUNNER_BASELINE = { + "matrix.py": "F0B1C19645464583E776849A82BE3F8EAAC5129A2B17614F222E7AAFA1E2B28E", + "connected.py": "9E34BAE4E9FDF43AC58C9897FA2F813C53C97145FBDE472102476DCAB4E1051E", + "native116.py": "5C4B2E615B7CBC7B679F91E782E9F7A314EC4B1DD59E7BE5A5A025520B8C4D7D", + "native116_connected.py": "34BD1E66EE427980473FC9ACC8031E3BDC195D04C8C05A4CA3E50F30398AAE12", +} + + +def _read(path): + return json.loads(Path(path).read_text(encoding="utf-8")) + + +def _approved(path, checksum, label): + path = Path(path).resolve() + if digest(path) != checksum.upper(): + raise AssertionError("Changed " + label + ": " + str(path)) + return path + + +def _disjoint(output, protected): + native116.require_disjoint(Path(output), Path(protected)) + + +def _new_output(path): + path = Path(path).resolve() + if not path.is_relative_to((RIG / "build").resolve()) or path == (RIG / "build").resolve(): + raise ValueError("Source116 outputs must be NEW paths under Power117/test-rig/build") + if path.exists(): + raise FileExistsError(path) + return path + + +def _protocol(): + root = Path(__file__).parent + for name, checksum in RUNNER_BASELINE.items(): + actual = hashlib.sha256((root / name).read_bytes().replace(b"\r\n", b"\n")).hexdigest().upper() + if actual != checksum: + raise AssertionError("Historical source runner differs from accepted baseline: " + name) + names = {"matrix.py", "connected.py", "actors.py", "transformer_order.py", "semantic_machine_audit.py"} + names.update(path.name for path in root.glob("native116*.py") if not path.name.startswith("test_")) + names.add(Path(__file__).name) + return {str((root / name).resolve()): digest(root / name) for name in sorted(names)} + + +def pending_routes(): + return [dict(id=identity, kind=kind, sourceStatus="REQUIRES_APPROVED_DISPOSABLE_116_SOURCE") + for kind, identities in SOURCE116.items() for identity in sorted(identities)] + + +def source116_settings(args, recipe, output): + return Namespace(source=Path(recipe["source"]), source_manifest=Path(recipe["sourceManifest"]), + source_key=recipe["sourceKey"], source_hashes=Path(recipe["sourceHashes"]), + source_snapshot=recipe["sourceSnapshot"], source_checkpoint=recipe["sourceSnapshot"], + source_fixture_key=recipe.get("sourceFixtureKey"), target_manifest=Path(args.target116_manifest).resolve(), + java8=Path(args.java8).resolve(), output=Path(output), ticks=512, port=args.port) + + +def _java8(java): + java = Path(java).resolve() + release = java.parent.parent / "release" + if not java.is_file() or not re.search(r'^JAVA_VERSION="1\.8\.', release.read_text(), re.MULTILINE): + raise ValueError("Source116 generation requires an installed Java 8, not the target's Java 16") + return {str(java): digest(java), str(release): digest(release)} + + +def prepare(args): + """Validate six archived recipes and final accepted 1.16 inputs; no writes/launch.""" + if type(args.port) is not int or not 1 <= args.port <= 65535: + raise ValueError("An explicitly leased server port is required") + output = _new_output(args.output) + manifest = _approved(args.target116_manifest, TARGET116_SHA256, "final accepted native116 manifest") + routes = _approved(args.source_routes, SOURCE_ROUTES_SHA256, "archived Ready source plan") + target = native116.pinned_profile(manifest, "target116") + identities = {entry["id"] for entry in _read(manifest)["target116"]["jars"]} + if not {"advantageworkstest", "poweradvantage", "steamadvantage", "electricadvantage", "basemetals", "orespawn"}.issubset(identities): + raise AssertionError("The final accepted source manifest must include all three mods and its harness/dependencies") + files = {str(path): digest(path) for path in (manifest, routes)} + files.update(_java8(args.java8)) + files.update(_protocol()) + for protected in (manifest, routes, target[0], *target[2], *map(Path, files)): + _disjoint(output, protected) + canonical = getattr(args, "routes", None) + if canonical is not None: + canonical = Path(canonical).resolve() + _disjoint(output, canonical) + files[str(canonical)] = digest(canonical) + recipes, rows, runtimes = _read(routes)["routes"], [], {} + selections = [("frozen", "native116/full", "legacy/original110-chained-113")] + selections += [("connected", "native116/" + name, "legacy/" + name) for name in HISTORICAL] + for kind, identity, selected in selections: + matches = [row for row in recipes if row["kind"] == kind and row["id"] == selected] + if len(matches) != 1: + raise AssertionError("Each archived source recipe is required exactly once: " + selected) + recipe = matches[0] + if canonical is not None: + found = [row for row in _read(canonical)["routes"] if row["kind"] == kind and row["id"] == selected] + if len(found) != 1 or any(found[0].get(key) != value for key, value in recipe.items()): + raise AssertionError("Canonical route changed archived authority: " + selected) + source = Path(recipe["source"]).resolve() + _disjoint(output, source) + pins = _approved(recipe["sourceManifest"], recipe["sourceManifestSha256"], "source manifest") + hashes = _approved(recipe["sourceHashes"], recipe["sourceHashesSha256"], "source world hashes") + name = recipe["sourceSnapshot"] if kind == "frozen" else "migration-" + recipe["sourceSnapshot"] + ".json" + if not re.fullmatch(r"migration-[A-Za-z0-9_-]+\.json", name): + raise ValueError("Unsafe archived snapshot name") + snapshot = _approved(source / name, recipe["sourceSnapshotSha256"], "source snapshot") + native116.pinned_profile(pins, recipe["sourceKey"]) + if not _read(hashes) or fingerprint(source / WORLD) != _read(hashes): + raise AssertionError("Archived source world fingerprint changed: " + str(source)) + fixture = source / "migration-fixture.json" + if not fixture.is_file(): + raise FileNotFoundError("Missing canonical migration fixture") + if kind == "frozen": + document = _read(snapshot) + if (len(document), sum("nbtBase64" in row for row in document)) != (355, 332): + raise AssertionError("Frozen source must retain all 355 blocks and 332 tiles") + else: + native116_connected.verify_source(source116_settings(args, recipe, output / identity.replace("/", "-"))) + files[str((source / "connected-fixture.json").resolve())] = digest(source / "connected-fixture.json") + for path in (pins, hashes, snapshot, fixture): + _disjoint(output, path) + files[str(path.resolve())] = digest(path) + runtimes[str(source)] = fingerprint(source) + rows.append(dict(id=identity, kind=kind, recipe=recipe)) + return dict(format=1, output=str(output), acceptedPower116=ACCEPTED_POWER116, + target116Manifest=str(manifest), target116ManifestSha256=digest(manifest), + sourcePlan=str(routes), sourcePlanSha256=digest(routes), sources=rows, files=files, + protocolFingerprint=_protocol(), protectedRuntimes=runtimes, seed=str(target[0]), + seedCode=native116_code.verify_code(target[0])) + + +def verify_source116_inputs(plan): + if plan.get("acceptedPower116") != ACCEPTED_POWER116: + raise AssertionError("Source plan accepted baseline changed") + for name, checksum in plan["files"].items(): + _approved(name, checksum, "source-generation input/protocol") + if _protocol() != plan["protocolFingerprint"]: + raise AssertionError("Source-generation protocol fingerprint changed") + native116_code.verify_code(Path(plan["seed"]), plan["seedCode"]) + for name, before in plan["protectedRuntimes"].items(): + if fingerprint(Path(name)) != before: + raise AssertionError("Protected canonical source runtime changed: " + name) + + +def _commands(kind, label): + actions = (["advprobe snapshot " + label] if kind == "frozen" else + ["advconnected snapshot " + label + "-loaded", "advconnected step 512 " + label + "-worked"]) + return actions + ["save-all flush", "stop"] + + +def run_source116(settings, kind): + """Inject process observation only, leaving the source runners/oracles intact.""" + if kind not in SOURCE116 or settings.ticks != 512: + raise ValueError("Exactly 512 measured ticks are required for source generation") + stops = [] + runtime = settings.output if kind == "frozen" else settings.output / "target116" + + class Observation: + def before_boot(self, current, java, launcher, label): + if (current.resolve() != runtime.resolve() or launcher != native116.LAUNCHER + or Path(java).resolve() != settings.java8.resolve() or label not in ("converted", "restarted")): + raise AssertionError("Only the NEW disposable native116 copy may launch") + self.observed_label, self.observed_closed = label, False + self.previous_logs = {name: digest(current / "logs" / name) if (current / "logs" / name).is_file() else None + for name in ("latest.log", "debug.log")} + for name in ("console-" + label + ".log", "latest-" + label + ".log", "debug-" + label + ".log"): + if (current / name).exists(): + raise FileExistsError("Refusing to replace source evidence: " + name) + + def close(self): + if self.observed_closed: + return + try: + return super().close() + finally: + self.observed_closed = True + record = dict(label=self.observed_label, returncode=self.process.returncode, + pid=self.process.pid, commands=list(self.commands), logs={}) + stops.append(record) + for name in ("latest.log", "debug.log"): + path = runtime / "logs" / name + if not path.is_file() or not path.stat().st_size or digest(path) == self.previous_logs[name]: + raise AssertionError("Missing or stale source boot log: " + str(path)) + archive = runtime / (name.removesuffix(".log") + "-" + self.observed_label + ".log") + if archive.exists(): + raise FileExistsError(archive) + shutil.copy2(path, archive) + record["logs"][archive.name] = digest(archive) + record["logs"]["console-" + self.observed_label + ".log"] = digest(Path(self.log.name)) + + if kind == "frozen": + class FrozenServer(Observation, native116.Server): + def __init__(self, current, java, launcher, label): + self.before_boot(current, java, launcher, label) + self.commands = [] + super().__init__(current, java, launcher, label) + + def send(self, command): + if command not in _commands("frozen", self.observed_label): + raise AssertionError("Forbidden source-generation mutation: " + command) + self.commands.append(command) + super().send(command) + + snapshot = types.FunctionType(native116.start_snapshot.__code__, + dict(native116.start_snapshot.__globals__, Server=FrozenServer), + native116.start_snapshot.__name__, native116.start_snapshot.__defaults__) + original_stage = native116.stage + + def stage(current, target): + if current.resolve() != runtime.resolve(): + raise AssertionError("Refusing to stage outside the NEW native116 copy") + launcher = original_stage(current, target) + path = current / "server.properties" + lines = [line for line in path.read_text().splitlines() if not line.startswith("server-port=")] + path.write_text("\n".join(lines) + f"\nserver-port={settings.port}\n", encoding="utf-8") + return launcher + + with patch.object(native116, "start_snapshot", snapshot), patch.object(native116, "stage", stage): + code = native116.run(settings) + else: + class ConnectedServer(Observation, native116_connected.NativeConnectedServer): + def __init__(self, current, java, launcher, label, on_loaded): + self.before_boot(current, java, launcher, label) + super().__init__(current, java, launcher, label, on_loaded) + + def send(self, command): + if command not in _commands("connected", self.observed_label): + raise AssertionError("Forbidden source-generation mutation: " + command) + super().send(command) + + with patch.object(native116_connected, "NativeConnectedServer", ConnectedServer): + code = native116_connected.run(settings) + return code, stops + + +def _logs_and_stops(runtime, kind, stops): + if [row.get("label") for row in stops] != ["converted", "restarted"]: + raise AssertionError("Both source boots must be cleanly saved and stopped") + for row in stops: + label = row["label"] + if (row.get("returncode") != 0 or type(row.get("pid")) is not int or row["pid"] <= 0 + or row.get("commands") != _commands(kind, label)): + raise AssertionError("Both source boots need exact commands and a clean stop/exit status") + names = {prefix + "-" + label + ".log" for prefix in ("console", "latest", "debug")} + if set(row.get("logs", {})) != names: + raise AssertionError("All console/latest/debug source boot logs are required") + for name in sorted(names): + path = _approved(runtime / name, row["logs"][name], "stopped-source log") + if not path.stat().st_size: + raise AssertionError("Empty source boot log") + native116_connected.require_native_boot(path) + text = path.read_text(encoding="utf-8", errors="replace") + if any(re.search(pattern, text, re.IGNORECASE) for pattern in ( + r"Missing minecraft:", r"missing (?:registry entries|mappings)\b", + r"unidentified mapping", r"registry remapping failed")): + raise AssertionError("Missing mappings in native116 source boot") + for marker in ("Saved the game", "Stopping server", "Saving worlds", "All chunks are saved"): + if marker not in text: + raise AssertionError("Missing actual source save/shutdown evidence: " + marker) + if stops[0]["pid"] == stops[1]["pid"]: + raise AssertionError("Source restart must use a separate process") + + +def source116_result(kind, settings, stops): + """Recheck raw evidence, including saved-format/resource and operation oracles.""" + runtime = settings.output if kind == "frozen" else settings.output / "target116" + _logs_and_stops(runtime, kind, stops) + target = native116.pinned_profile(settings.target_manifest, "target116") + native116_code.verify_code(runtime, _read(settings.target_manifest)["target116"]["serverCode"]) + pins = sorted((jar.name, digest(jar)) for jar in target[2]) + if (sorted((jar.name, digest(jar)) for jar in (runtime / "mods").glob("*.jar")) != pins + or sorted((row["file"], row["sha256"].upper()) for row in _read(runtime / "input-jars.json")) != pins): + raise AssertionError("Generated source mods differ from final accepted native116 pins") + if digest(runtime / "migration-fixture.json") != digest(settings.source / "migration-fixture.json"): + raise AssertionError("Source fixture specification was rebuilt or changed") + proof = dict(stoppedCleanly=True, processStops=stops, nativeVersion="1.16.5", codeInventoryVerified=True) + source_snapshot = settings.source / (settings.source_snapshot if kind == "frozen" else + "migration-" + settings.source_checkpoint + ".json") + if kind == "frozen": + report = _read(runtime / "state-comparison.json") + for gate in ("converted", "restarted", "idempotence"): + result = report.get(gate, {}) + if result.get("status") != "PASS" or any(result.get("witnesses", {}).get(key) != count + for key, count in (("blocks", 355), ("tiles", 332))): + raise AssertionError("Source116 comprehensive semantic comparison/counts failed") + for label in ("converted", "restarted"): + path = runtime / ("migration-" + label + ".json") + document = _read(path) + if (len(document), sum("nbtBase64" in row for row in document)) != (355, 332): + raise AssertionError("Source116 must retain all 355 blocks and 332 tiles") + if (matrix.compare(source_snapshot, path, native116.native_normalized)["status"] != "PASS" + or compare_snapshot_resources(source_snapshot, path)["status"] != "PASS"): + raise AssertionError("Raw frozen source state/resources were not preserved") + save = _read(runtime / "native116-provenance.json")["nativeSaveFormat"] + proof.update(blocks=355, tiles=332, semanticComparison=report) + snapshot, fixture = "migration-restarted.json", None + else: + report = _read(settings.output / "native116-connected-results.json") + required = {label + "_" + gate for label in ("converted", "restarted") + for gate in ("preservation", "exact_resources", "operation", "completed_work", "native_boot")} + required.add("real_output_across_restarts") + if (report.get("status") != "PASS" or report.get("sourcePreservation", {}).get("status") != "PASS" + or report.get("sourceRuntimeBefore") != fingerprint(settings.source) + or report.get("sourceRuntimeAfter") != fingerprint(settings.source) + or set(report.get("results", {})) != required + or any(row.get("status") != "PASS" for row in report["results"].values())): + raise AssertionError("Source116 connected preservation/operating gates failed") + definition = _read(runtime / "connected-fixture.json") + if definition != _read(settings.source / "connected-fixture.json"): + raise AssertionError("Connected source fixture changed") + requested, advanced = [], [] + previous = source_snapshot + for label in ("converted", "restarted"): + loaded_path = runtime / ("migration-" + label + "-loaded.json") + loaded = connected.read(runtime, label + "-loaded") + worked = connected.read(runtime, label + "-worked") + if loaded.get("requestedTicks") != 0 or loaded.get("advancedTicks") != 0: + raise AssertionError("Loaded source must be frozen before operation") + for suffix, document in (("loaded", loaded), ("worked", worked)): + state = native116_connected.require_document(document, definition, destination=True) + if set(state["cases"]) != CONNECTED_CASES: + raise AssertionError("Source116 lost a connected lane/drill/battery") + if document["snapshot"] != _read(runtime / ("migration-" + label + "-" + suffix + ".json")): + raise AssertionError("Connected and migration snapshots disagree") + if (matrix.compare(previous, loaded_path, native116.native_normalized)["status"] != "PASS" + or compare_snapshot_resources(previous, loaded_path)["status"] != "PASS"): + raise AssertionError("Raw connected source state/resources were not preserved") + if (connected.operation(loaded, worked, 512)["status"] != "PASS" + or connected_interval(loaded, worked, definition, 512)["status"] != "PASS"): + raise AssertionError("Source116 requires exact 512-tick completed work on both boots") + requested.append(worked["requestedTicks"]) + advanced.append(worked["advancedTicks"]) + previous = runtime / ("migration-" + label + "-worked.json") + if requested != [512, 512] or advanced != [512, 512]: + raise AssertionError("Source116 requires both exact 512-tick boots") + if native116_connected.continued_output(connected.read(runtime, "converted-loaded"), + connected.read(runtime, "restarted-worked"))["status"] != "PASS": + raise AssertionError("Connected source did not produce across full restart") + save = report["nativeSaveFormat"] + proof.update(preservationStatus="PASS", requestedTicks=requested, advancedTicks=advanced, + connectedCases=sorted(CONNECTED_CASES), operatingComparison=report["results"]) + snapshot, fixture = "restarted-worked", definition["sourceProfile"] + for label in ("converted", "restarted"): + native116_save_io.audit_save(runtime, save[label]) + native116_save_io.require_same_settings(save["converted"], save["restarted"]) + proof["nativeSaveFormat"] = save + return runtime, snapshot, fixture, proof + + +def _source_row(folder, selected, runtime, snapshot, fixture, manifest): + hashes = folder / "world-hashes.json" + write_json(hashes, fingerprint(runtime / WORLD)) + path = runtime / (snapshot if selected["kind"] == "frozen" else "migration-" + snapshot + ".json") + return dict(id=selected["id"], kind=selected["kind"], source=str(runtime.resolve()), + sourceKey="target116", sourceManifest=str(manifest), sourceManifestSha256=digest(manifest), + sourceHashes=str(hashes.resolve()), sourceHashesSha256=digest(hashes), + sourceSnapshot=snapshot, sourceSnapshotSha256=digest(path), sourceFixtureKey=fixture) + + +def verify_source_receipt(row): + """Required fail-closed guard for every native116/* 1.17 route.""" + kind, identity = row.get("kind"), row.get("id") + if kind not in SOURCE116 or identity not in SOURCE116[kind]: + raise AssertionError("Unknown or mismatched Source116 route") + if row.get("sourceStatus") != SOURCE_STATUS: + raise AssertionError("An explicit stopped-source receipt is required; prior destination PASS is not approval") + path = _approved(row["source116Receipt"], row["source116ReceiptSha256"], "source116 receipt") + receipt = _read(path) + runtime = Path(row["source"]).resolve() + if not runtime.is_relative_to((RIG / "build").resolve()): + raise ValueError("Approved source must remain a disposable Power117 copy") + if (receipt.get("status") != "PASS" or receipt.get("scope") != SCOPE + or receipt.get("acceptedPower116") != ACCEPTED_POWER116 or receipt.get("stoppedCleanly") is not True + or (receipt.get("id"), receipt.get("kind")) != (identity, kind) + or receipt.get("sourceRuntime") != str(runtime) + or receipt.get("runtimeFingerprint") != fingerprint(runtime) + or receipt.get("worldFingerprint") != fingerprint(runtime / WORLD)): + raise AssertionError("Source116 stopped-source identity/fingerprint changed or was never approved") + plan_path = _approved(receipt["inputProvenance"], receipt["inputProvenanceSha256"], "source116 provenance") + plan = _read(plan_path) + verify_source116_inputs(plan) + selections = [item for item in plan["sources"] if item["id"] == identity and item["kind"] == kind] + if len(selections) != 1 or selections[0]["recipe"] != receipt["sourceRecipe"]: + raise AssertionError("Source116 archived recipe authority changed") + manifest = _approved(receipt["target116Manifest"], TARGET116_SHA256, "final accepted native116 manifest") + if (receipt.get("target116ManifestSha256") != TARGET116_SHA256 or row.get("sourceKey") != "target116" + or Path(row["sourceManifest"]).resolve() != manifest or row.get("sourceManifestSha256") != TARGET116_SHA256): + raise AssertionError("Source116 manifest/route authority changed") + args = Namespace(target116_manifest=manifest, java8=receipt["java8"], port=receipt["port"]) + settings = source116_settings(args, receipt["sourceRecipe"], runtime if kind == "frozen" else runtime.parent) + actual, snapshot, fixture, proof = source116_result(kind, settings, receipt["processStops"]) + if any(receipt.get(key) != value for key, value in proof.items()): + raise AssertionError("Source116 receipt no longer matches the raw stopped evidence") + if (actual.resolve() != runtime or row.get("sourceSnapshot") != snapshot or row.get("sourceFixtureKey") != fixture): + raise AssertionError("Source116 checkpoint/fixture identity changed") + hashes = _approved(row["sourceHashes"], row["sourceHashesSha256"], "generated source world hashes") + if _read(hashes) != fingerprint(runtime / WORLD): + raise AssertionError("Generated source world hash authority differs") + filename = snapshot if kind == "frozen" else "migration-" + snapshot + ".json" + _approved(runtime / filename, row["sourceSnapshotSha256"], "generated source snapshot") + return receipt + + +def run(args): + """Generate exactly six independent sources; retain failures and partial evidence.""" + plan = prepare(args) + if getattr(args, "validate_only", False): + print(json.dumps(dict(status="PREFLIGHT_ONLY_NO_RUNTIME", sourceCount=len(plan["sources"]), + target116ManifestSha256=plan["target116ManifestSha256"], output=plan["output"]), indent=2)) + return 0 + output = Path(plan["output"]) + output.mkdir(parents=True, exist_ok=False) + provenance = output / "source116-input-provenance.json" + write_json(provenance, plan) + bundle = dict(format=1, status="RUNNING", scope=SCOPE, acceptedPower116=ACCEPTED_POWER116, receipts=[]) + routes = dict(format=1, qualificationStatus="UNQUALIFIED", acceptedPower116=ACCEPTED_POWER116, routes=pending_routes()) + try: + for selected in plan["sources"]: + verify_source116_inputs(plan) + folder = output / selected["id"].replace("/", "-") + folder.mkdir() + settings = source116_settings(args, selected["recipe"], folder / "runtime") + receipt_path = folder / "source116-receipt.json" + receipt = dict(status="RUNNING", scope=SCOPE, acceptedPower116=ACCEPTED_POWER116, + id=selected["id"], kind=selected["kind"], stoppedCleanly=False, + target116Manifest=plan["target116Manifest"], target116ManifestSha256=plan["target116ManifestSha256"], + sourceRecipe=selected["recipe"], java8=str(settings.java8), port=args.port, + inputProvenance=str(provenance.resolve()), inputProvenanceSha256=digest(provenance)) + write_json(receipt_path, receipt) + try: + code, stops = run_source116(settings, selected["kind"]) + if code != 0: + raise AssertionError("Native116 source-generation protocol failed") + runtime, snapshot, fixture, proof = source116_result(selected["kind"], settings, stops) + verify_source116_inputs(plan) + receipt.update(proof, status="PASS", sourceRuntime=str(runtime.resolve()), + runtimeFingerprint=fingerprint(runtime), worldFingerprint=fingerprint(runtime / WORLD)) + write_json(receipt_path, receipt) + row = _source_row(folder, selected, runtime, snapshot, fixture, settings.target_manifest) + row.update(sourceStatus=SOURCE_STATUS, source116Receipt=str(receipt_path.resolve()), + source116ReceiptSha256=digest(receipt_path)) + verify_source_receipt(row) + index = next(i for i, item in enumerate(routes["routes"]) if item["id"] == selected["id"]) + routes["routes"][index] = row + bundle["receipts"].append(dict(id=selected["id"], receipt=str(receipt_path.resolve()), sha256=digest(receipt_path))) + print("PASS stopped source " + selected["id"] + "; 1.17 remains UNQUALIFIED", flush=True) + except BaseException as error: + receipt.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + write_json(receipt_path, receipt) + raise + finally: + write_json(output / "source116-routes.json", routes) + write_json(output / "source116-receipts.json", bundle) + verify_source116_inputs(plan) + bundle.update(status="PASS", canonicalSourcesUnchanged=True, seedCodeUnchanged=True) + except BaseException as error: + bundle.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + raise + finally: + try: + verify_source116_inputs(plan) + except BaseException as error: + bundle.update(status="FAIL", protectionFailure=str(error)) + raise + finally: + write_json(output / "source116-receipts.json", bundle) + write_json(output / "source116-routes.json", routes) + return 0 + + +# Matching names for the existing native115 guard's campaign integration pattern. +preflight_source116 = prepare +require_source116_receipt = verify_source_receipt + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + for name in ("prepare", "run"): + command = commands.add_parser(name) + command.add_argument("--source-routes", type=Path, default=SOURCE_ROUTES) + command.add_argument("--target116-manifest", type=Path, default=TARGET116_MANIFEST) + command.add_argument("--routes", type=Path, help="Optional canonical 1.17 recipe lineage check; never modified") + command.add_argument("--java8", type=Path, required=True) + command.add_argument("--port", type=int, required=True, help="Port leased by the caller") + command.add_argument("--output", type=Path, required=True) + command.add_argument("--validate-only", action="store_true") + verify = commands.add_parser("verify") + verify.add_argument("--routes", type=Path, required=True, help="Generated source116-routes.json") + args = parser.parse_args() + if args.command == "verify": + rows = _read(args.routes)["routes"] + if len(rows) != 6 or {(row["kind"], row["id"]) for row in rows} != { + (kind, name) for kind, names in SOURCE116.items() for name in names}: + raise AssertionError("Exactly six stopped source116 receipts are required") + for row in rows: + verify_source_receipt(row) + print("PASS six stopped native116 sources; 1.17 remains UNQUALIFIED") + return 0 + args.validate_only = args.command == "prepare" or args.validate_only + return run(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_startup_diagnostics.py b/test-rig/migration/native117_startup_diagnostics.py new file mode 100644 index 00000000..54c76352 --- /dev/null +++ b/test-rig/migration/native117_startup_diagnostics.py @@ -0,0 +1,66 @@ +"""Read-only JVM evidence before a failed startup is shut down; never a retry.""" + +from datetime import datetime, timezone +import json +from pathlib import Path +import re +import subprocess +import traceback + + +def require_label(label): + if not re.fullmatch(r"[A-Za-z0-9_.-]{1,100}", label) or label in (".", ".."): + raise ValueError("Unsafe startup diagnostic label") + + +def observe(runtime, java, process, label, failure): + runtime, java = Path(runtime), Path(java) + require_label(label) + record = dict(status="STARTUP_FAILURE_NOT_QUALIFIED", runtime=str(runtime), label=label, + failureType=type(failure).__name__, failure=str(failure), + traceback="".join(traceback.format_exception(failure)), + observedUtc=datetime.now(timezone.utc).isoformat(), + pid=process.pid, processExitBefore=process.poll(), + startupRetry=False, productionMutation=False) + if record["processExitBefore"] is not None: + record["threadDumpStatus"] = "PROCESS_ALREADY_EXITED" + return record + jcmd = java.with_name("jcmd.exe" if java.suffix.lower() == ".exe" else "jcmd") + if not jcmd.is_file(): + record["threadDumpStatus"] = "JCMD_UNAVAILABLE" + return record + dump = runtime / ("startup-thread-" + label + ".txt") + if dump.exists(): + raise FileExistsError(dump) + command = [str(jcmd), str(process.pid), "Thread.print", "-l"] + record.update(threadDumpCommand=command, threadDumpTimeoutSeconds=20) + try: + result = subprocess.run(command, cwd=runtime, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, timeout=20, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)) + payload = result.stdout + record.update(threadDumpStatus="CAPTURED" if result.returncode == 0 else "JCMD_FAILED", + threadDumpExitCode=result.returncode) + except subprocess.TimeoutExpired as error: + payload = error.output or b"" + record["threadDumpStatus"] = "JCMD_TIMED_OUT" + if isinstance(payload, str): + payload = payload.encode("utf-8") + with dump.open("xb") as stream: + stream.write(payload) + record["threadDump"] = str(dump) + return record + + +def retain(runtime, label, record, cleanup_failure=None): + require_label(label) + path = Path(runtime) / ("startup-failure-" + label + ".json") + value = dict(record) + value["cleanupFailure"] = None if cleanup_failure is None else dict( + type=type(cleanup_failure).__name__, message=str(cleanup_failure), + traceback="".join(traceback.format_exception(cleanup_failure))) + value["completedUtc"] = datetime.now(timezone.utc).isoformat() + with path.open("x", encoding="utf-8") as stream: + json.dump(value, stream, indent=2) + stream.write("\n") + return path diff --git a/test-rig/migration/native117_steam_control_audit.py b/test-rig/migration/native117_steam_control_audit.py new file mode 100644 index 00000000..9622339b --- /dev/null +++ b/test-rig/migration/native117_steam_control_audit.py @@ -0,0 +1,53 @@ +"""Use the unchanged Steam control oracle with the native 1.17 validator.""" + +import argparse +import hashlib +import json +from pathlib import Path + +import native117_machine_operations +from native117_actor_io import bind +import steam_control_audit as historical + + +def native(line): + historical.require(line == '117', 'Unsupported Minecraft line') + return native117_machine_operations + + +_audit_documents = bind(historical.audit_documents, native=native) + + +def audit_documents(catalog, operation, fixture, cold, line): + return _audit_documents(catalog, operation, fixture, cold, line, namespace="basesteam") + + +_audit = bind(historical.audit, native=native, audit_documents=audit_documents) + + +def audit(runtime, line='117'): + result = _audit(runtime, line) + result['adapterSha256'] = hashlib.sha256(Path(__file__).read_bytes()).hexdigest().upper() + result['adapterScope'] = 'Native validator and expected basesteam identity; no observed ID translation' + return result + + +def __getattr__(name): + return getattr(historical, name) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--runtime', type=Path, required=True) + parser.add_argument('--line', choices=('117',), default='117') + args = parser.parse_args(argv) + try: + result = audit(args.runtime, args.line) + except Exception as error: + result = dict(status='FAIL', line=args.line, runtime=str(args.runtime), failures=[str(error)]) + print(json.dumps(result, indent=2, sort_keys=True, allow_nan=False)) + return int(result['status'] != 'PASS') + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/test-rig/migration/native117_stopped_entities.py b/test-rig/migration/native117_stopped_entities.py new file mode 100644 index 00000000..8b720f51 --- /dev/null +++ b/test-rig/migration/native117_stopped_entities.py @@ -0,0 +1,136 @@ +"""Retain and corroborate the actual FMLServerStoppingEvent capture after close. + +Only native117_entity_storage.audit performs entity comparisons; no live record, +expected projection, ignored fields or tick-drift allowance is substituted here. +""" + +import hashlib +import json +from pathlib import Path +import uuid + +from matrix import WORLD +import native117_entity_storage as entity_storage + +SNAPSHOT = "native117-stopped-entities.json" +DIMENSIONS = {"minecraft:overworld": (0, ""), "minecraft:the_nether": (-1, "DIM-1"), "minecraft:the_end": (1, "DIM1")} + + +def _sha(data): + return hashlib.sha256(data).hexdigest().upper() + + +def _unique_object(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError("Duplicate stopping snapshot JSON key: " + key) + result[key] = value + return result + + +def audit_stopped(runtime, evidence_folder): + """Call after clean process shutdown; retain a NEW immutable snapshot copy. + + Empty dimensions are explicit skips, not inferred-empty saves. An all-empty + capture returns NO_NONEMPTY_DIMENSIONS, never a vacuous entity-proof PASS. + This checks final saved NBT, not the earlier live fixture's expected behavior. + """ + runtime = Path(runtime).resolve(strict=True) + source = runtime / SNAPSHOT + world = (runtime / WORLD).resolve(strict=True) + evidence = Path(evidence_folder).resolve() + if (evidence.is_relative_to(world) or world.is_relative_to(evidence) + or source.is_relative_to(evidence) or evidence == source): + raise ValueError("Stopped-entity evidence overlaps protected inputs") + evidence.mkdir(parents=True, exist_ok=False) + receipt = dict(format=1, status="FAIL", scope="STOP_BOUNDARY_SAVED_ENTITY_NBT_ONLY", dimensions={}) + try: + if not any((runtime / name).is_file() and not (runtime / name).is_symlink() for name in + ("advantage-migration.allow", "advantage-machine-operation.allow")): + raise ValueError("Runtime is not explicitly marked for Advantage Works observation") + if source.is_symlink() or not source.is_file(): + raise ValueError("No regular native stopping snapshot") + data = source.read_bytes() + receipt["snapshotSha256"] = _sha(data) + with (evidence / SNAPSHOT).open("xb") as output: + 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("enumeration") != "ServerLevel.getAllEntities"): + raise ValueError("Not a complete native stopping-event snapshot") + if str(uuid.UUID(document.get("bootId", ""))) != document["bootId"]: + raise ValueError("Invalid stopping snapshot boot UUID") + if Path(document.get("worldDirectory", "")).resolve() != world: + raise AssertionError("Stopping snapshot belongs to another world directory") + dimensions = document.get("dimensions") + if not isinstance(dimensions, dict) or set(dimensions) != set(DIMENSIONS): + raise ValueError("Stopping snapshot must cover all three vanilla dimensions") + if document['format'] == 2: + from native117_helper_contract import CONTRACT, LIFECYCLE, validate + contract = (runtime / CONTRACT).read_bytes() + (evidence / CONTRACT).write_bytes(contract) + lifecycle = None + if document.get('nonpersistentHelperCount'): + candidates = [] + for path in runtime.glob('dimensions-*.json'): + value = json.loads(path.read_bytes(), object_pairs_hook=_unique_object) + key = value.get('worldDimensionKey') + if (key in dimensions and type(value.get('gameTime')) is int + and value['gameTime'] <= dimensions[key]['gameTime']): + candidates.append((value['gameTime'], path.name, path, value)) + if not candidates: + raise AssertionError('No actual dimension helper lifecycle observation') + _, _, path, lifecycle = max(candidates, key=lambda row: row[:2]) + (evidence / LIFECYCLE).write_bytes(path.read_bytes()) + receipt.update(format=2, helperLifecycle=validate(document, contract, lifecycle)) + count, matched, audited = 0, set(), 0 + for key, (legacy_id, directory) in DIMENSIONS.items(): + record = dimensions[key] + if (not isinstance(record, dict) or record.get("worldDimensionKey") != key + or type(record.get("dimension")) is not int or record["dimension"] != legacy_id + or type(record.get("gameTime")) is not int or not isinstance(record.get("entities"), list) + or type(record.get("entityCount")) is not int or record["entityCount"] != len(record["entities"])): + raise ValueError("Incomplete native dimension stopping record: " + key) + count += len(record["entities"]) + for entry in record["entities"]: + if (not isinstance(entry, dict) or entry.get("worldDimensionKey") != key + or entry.get("id") == "minecraft:player" or not isinstance(entry.get("uuid"), str)): + raise ValueError("Invalid non-player dimension observation") + if not record["entities"]: + receipt["dimensions"][key] = dict(status="SKIP_EMPTY_STOP_SNAPSHOT", entityCount=0) + continue + # Decode actual recorded NBT, never an expected fixture projection. + _, 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")) + if result.get("status") != "PASS": + raise AssertionError("Raw entity audit did not pass: " + key) + identities = set(result["matched"]) + if identities & matched: + raise AssertionError("Duplicate saved entity UUID across dimensions") + matched.update(identities) + receipt["dimensions"][key] = result + audited += 1 + if type(document.get("entityCount")) is not int or document["entityCount"] != count: + raise AssertionError("Stopping snapshot entity count differs from dimension records") + if source.read_bytes() != data: + raise AssertionError("Stopping snapshot changed during the after-close audit") + 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']) + receipt.update(status="PASS" if audited else "NO_NONEMPTY_DIMENSIONS", bootId=document["bootId"], + auditedDimensions=audited, snapshotRecords=count, matchedEntities=len(matched)) + except Exception as error: + receipt["error"] = type(error).__name__ + ": " + str(error) + raise + finally: + with (evidence / "receipt.json").open("x", encoding="utf-8") as output: + json.dump(receipt, output, indent=2, sort_keys=True) + output.write("\n") + return receipt diff --git a/test-rig/migration/native117_ticket_networks.py b/test-rig/migration/native117_ticket_networks.py new file mode 100644 index 00000000..6a3c0196 --- /dev/null +++ b/test-rig/migration/native117_ticket_networks.py @@ -0,0 +1,176 @@ +"""Ticket-halo-aware native network layouts; historical oracles remain unchanged.""" + +from pathlib import Path +import json +import shutil +import time + +import extended_lifecycle as historical +from native117_identity import fixture_ids +from matrix import digest, write_json +from native117 import RIG, jar_identity, output_path, pinned_profile, require_disjoint +import native117_extended +from native117_partial import (NativeExtendedRun, NativePartialRun, campaign, + common_parser, exercise_bridge, exercise_partial, + selected_chunks) + + +def halo_chunks(fixture, group): + cores = fixture["groups"][group] + 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)) <= 2 for a, b in cores)] + + +def linear_fixtures(): + result = [] + for index, name in enumerate(("C-STEAM", "C-ELECTRIC", "C-FLUID")): + base = 20480 + index * 512 + origin, sink = [base + 8, 80, 2056], [base + 136, 80, 2056] + pipes = [[x, 80, 2056] for x in range(base + 9, base + 136)] + fixture = dict(id=name, nativeTicketNetwork=True, min=[base, 72, 2048], max=[base + 159, 90, 2079], + witnesses=[origin, *pipes[:3], sink, *pipes[3:]], pipes=pipes, + groups=dict(supply=[[base >> 4, 128]], machine=[[(base >> 4) + 8, 128]])) + for group in ("supply", "machine"): + fixture["groups"][group + "halo"] = halo_chunks(fixture, group) + result.append(fixture) + return result + + +def bridge_fixtures(): + result = [] + for index, name in enumerate(("C-STEAM", "C-ELECTRIC", "C-FLUID")): + base = 24576 + index * 512 + origin, sink = [base + 13, 80, 2056], [base + 9, 80, 2056] + path = [[base + 13, 80, z] for z in range(2057, 2185)] + path += [[base + x, 80, 2184] for x in range(12, 8, -1)] + path += [[base + 9, 80, z] for z in range(2183, 2056, -1)] + fixture = dict(id=name, nativeTicketNetwork=True, min=[base, 72, 2048], max=[base + 31, 90, 2207], + witnesses=[origin, sink, *path], pipes=path, + groups=dict(bridge=[[base >> 4, 136]], endpoints=[[base >> 4, 128]])) + fixture["groups"]["bridgehalo"] = halo_chunks(fixture, "bridge") + result.append(fixture) + return result + + +def extended_fixtures(): + return [f for f in fixture_ids(historical.fixtures()) if "route" in f] + linear_fixtures() + + +def archive_harness(base_manifest, harness, output): + """Generate a new immutable input set, never editing its parent manifest.""" + output = output_path(output) + require_disjoint(output, base_manifest) + require_disjoint(output, harness) + pinned_profile(base_manifest.resolve(), "target117") + definition = json.loads(base_manifest.read_text()) + entry = next(j for j in definition["target117"]["jars"] if j["id"] == "advantageworkstest") + jar_identity(harness, entry["id"], entry["version"]) + checksum = digest(harness) + archive = RIG / "build/native117-artifacts" / checksum / harness.name + archive.parent.mkdir(parents=True, exist_ok=True) + if archive.exists(): + if digest(archive) != checksum: + raise AssertionError("Immutable harness archive changed") + else: + shutil.copy2(harness, archive) + if digest(archive) != checksum or digest(harness) != checksum: + raise AssertionError("Harness changed during archive staging") + entry.update(path=str(archive.resolve()), sha256=checksum, originPath=str(harness.resolve())) + definition["target117"].update(sourceManifest=str(base_manifest.resolve()), sourceManifestSha256=digest(base_manifest)) + write_json(output, definition) + pinned_profile(output, "target117") + return output + + +def fill_commands(definition, command): + parts = command.split() + if len(parts) != 9 or parts[0] != "fill" or parts[7:] != ["minecraft:air", "replace"]: + return None + minimum, maximum = list(map(int, parts[1:4])), list(map(int, parts[4:7])) + f = next((f for f in definition if f.get("nativeTicketNetwork") and f["min"] == minimum and f["max"] == maximum), None) + if f is None: + return None + result = [] + for x in range(minimum[0], maximum[0] + 1, 16): + for z in range(minimum[2], maximum[2] + 1, 16): + low = [x, minimum[1], z] + high = [min(x + 15, maximum[0]), maximum[1], min(z + 15, maximum[2])] + result.append("fill " + " ".join(map(str, low + high)) + " minecraft:air replace") + return result + + +class NativeTicketNetworkRun(NativePartialRun): + def command(self, command): + pieces = fill_commands(self.definition, command) + for piece in pieces if pieces is not None else [command]: + super().command(piece) + + def probe(self, operation, label): + if operation.startswith("release-"): + group = operation[8:] + if selected_chunks(self.definition, group + "halo"): + before = self._raw_probe("snapshot", label + "-native-before") + self._raw_probe("release-" + group + "halo", label + "-native-halo") + result = self._raw_probe(operation, label) + self._released_groups[group] = dict(before=before, selected=selected_chunks(self.definition, group), + deadline=time.monotonic() + 45) + return result + if operation.startswith("load-") and selected_chunks(self.definition, operation[5:] + "halo"): + self._raw_probe(operation + "halo", label + "-native-halo") + return super().probe(operation, label) + + +class NativeTicketExtendedRun(NativeTicketNetworkRun, NativeExtendedRun): + def build(self): + self.probe("load", "loaded") + for f in self.definition: + self.command("fill " + " ".join(map(str, f["min"] + f["max"])) + " minecraft:air replace") + if "route" in f: + self.block(f["controls"][0], "minecraft:redstone_block") + self.block(f["from"], f["block"] + "[facing=" + f["facing"] + "]") + for pos in f["route"][1:] + f.get("alternatives", []): + self.block(pos, f["block"].replace("_drill", "_track")) + for pos in f["route"]: + self.block(historical.offset(pos, f["direction"]), "minecraft:bedrock") + self.inventory(f["from"], 0, 'minecraft:dirt{AdvWorksWitness:"' + f["id"] + '"}', 7) + self.inventory(f["from"], 1, "minecraft:apple", 13) + continue + fluid, steam = f["id"] == "C-FLUID", f["id"] == "C-STEAM" + pipe = "basepower:fluid_pipe" if fluid else "basesteam:steam_pipe" if steam else "baseelectric:electric_conduit" + self.block(f["witnesses"][0], pipe if fluid else "basepower:infinite_" + ("steam" if steam else "electricity")) + for pos in f["pipes"]: + self.block(pos, pipe) + self.block(f["witnesses"][4], "basepower:fluid_metal_tank" if fluid else + "basesteam:steam_furnace" if steam else "baseelectric:electric_furnace") + if not fluid: + if steam: + self.inventory(f["witnesses"][4], 0, "minecraft:coal", 64) + self.inventory(f["witnesses"][4], 1 if steam else 0, "minecraft:iron_ore", 4) + self.probe("offer", "commission-fluid") + self.probe("watch", "watching") + + +def main(): + parser = common_parser(__doc__) + parser.add_argument("--campaign", choices=("partial", "bridge", "extended"), required=True) + args = parser.parse_args() + definition = {"partial": linear_fixtures, "bridge": bridge_fixtures, "extended": extended_fixtures}[args.campaign]() + operation = {"partial": exercise_partial, "bridge": exercise_bridge, "extended": native117_extended.exercise}[args.campaign] + runner_type = NativeTicketNetworkRun if args.campaign == "bridge" else NativeTicketExtendedRun + own_path, own_hash = Path(__file__), digest(Path(__file__)) + def exercise(runner, progress): + archive = runner.runtime / "native117-ticket-network-protocol.py" + shutil.copy2(own_path, archive) + result = operation(runner, progress) + result["native_ticket_protocol_unchanged"] = digest(own_path) == own_hash == digest(archive) + result["native_ticket_protocol_sha256"] = own_hash + if not result["native_ticket_protocol_unchanged"]: + result["status"] = "FAIL" + progress(result) + return result + return campaign(args, "halo-" + args.campaign, definition, exercise, runner_type) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native117_ticket_precondition.py b/test-rig/migration/native117_ticket_precondition.py new file mode 100644 index 00000000..9d9132bc --- /dev/null +++ b/test-rig/migration/native117_ticket_precondition.py @@ -0,0 +1,148 @@ +"""Static Forge 36 ticket isolation, never a runtime qualification or log waiver. + +Input is a complete, single-dimension ticket-map export, not fixture bounds or +observedForcedLevels alone. Each ticket records its map-key chunk position, +Ticket.getType().toString(), getTicketLevel(), and whether it remains active. +An optional forceTicks flag does not change its chunk dependency influence. +No ticket types, fixtures, namespaces, or distant ticket records are filtered. + +Schema: format=1, dimension, ticketMapComplete=true, ticketGeometry (the existing +native six-field observation), tickets=[{pos:[x,z], type, level, active}]. +G lacks the complete ticket records and therefore cannot pass this input gate. +Tests can supply explicitly modelled records; they do not become live evidence. + +Native source: TicketManager.func_219364_a installs FORCED at 31; +ChunkDistanceGraph.func_215480_b adds one per eight-neighbour step; ChunkManager +retains holders through 33 + ChunkStatus.maxDistance() = 44. Thus distance is +Chebyshev, and a level-31 ticket influences a holder through distance 13 inclusive. +""" + +import json +from pathlib import Path +import re + + +FORCED_LEVEL = 31 +FULL_LEVEL = 33 +LAST_HOLDER_LEVEL = 44 +HOLDER_RADIUS = LAST_HOLDER_LEVEL - FORCED_LEVEL +_INT_MIN, _INT_MAX = -(2 ** 31), 2 ** 31 - 1 +_GEOMETRY = dict(fullLevel=33, entityTickingLevel=31, maxDistance=11, + lastHolderLevel=44, holderRadius=13) + + +def _integer(value, low, high, label): + if type(value) is not int or not low <= value <= high: + raise ValueError("Invalid " + label) + return value + + +def _position(value): + if not isinstance(value, (list, tuple)) or len(value) != 2: + raise ValueError("Expected a two-integer chunk position") + return tuple(_integer(n, _INT_MIN, _INT_MAX, "chunk coordinate") for n in value) + + +def _dimension(value): + if not isinstance(value, str) or re.fullmatch(r"[a-z0-9_.-]+:[a-z0-9_./-]+", value) is None: + raise ValueError("Expected a namespaced dimension key") + return value + + +def validate_ticket_snapshot(snapshot): + """Fail closed on incomplete exports or unsupported native geometry.""" + fields = {"format", "dimension", "ticketMapComplete", "ticketGeometry", "tickets"} + if not isinstance(snapshot, dict) or set(snapshot) != fields: + raise ValueError("Expected exact complete native ticket-map schema") + if type(snapshot["format"]) is not int or snapshot["format"] != 1: + raise ValueError("Unsupported ticket-map format") + _dimension(snapshot["dimension"]) + if snapshot["ticketMapComplete"] is not True: + raise ValueError("Complete global dimension ticket map required") + geometry = snapshot["ticketGeometry"] + if not isinstance(geometry, dict) or set(geometry) != set(_GEOMETRY) | {"observedForcedLevels"}: + raise ValueError("Missing native geometry observation") + if any(type(geometry[k]) is not int or geometry[k] != v for k, v in _GEOMETRY.items()): + raise ValueError("Native ticket geometry differs from Forge 36 contract") + records = snapshot["tickets"] + if not isinstance(records, list): + raise ValueError("Expected a ticket record list") + forced = set() + required = {"pos", "type", "level", "active"} + for record in records: + if (not isinstance(record, dict) or not required <= set(record) + or set(record) - required - {"forceTicks"}): + raise ValueError("Invalid native ticket record fields") + _position(record["pos"]) + if not isinstance(record["type"], str) or not record["type"].strip(): + raise ValueError("Missing ticket type; unknown types must not be discarded") + _integer(record["level"], 0, _INT_MAX, "ticket level") + if type(record["active"]) is not bool: + raise ValueError("Explicit ticket active state required") + if "forceTicks" in record and type(record["forceTicks"]) is not bool: + raise ValueError("Invalid forceTicks state") + if record["type"] == "forced": + if record["level"] != FORCED_LEVEL: + raise ValueError("Native FORCED ticket must have level 31") + if record["active"]: + forced.add(record["level"]) + observed = geometry["observedForcedLevels"] + if (not isinstance(observed, list) or any(type(n) is not int for n in observed) + or observed != sorted(forced)): + raise ValueError("Observed forced levels disagree with complete ticket records") + return snapshot + + +def _unique_object(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError("Duplicate JSON field: " + key) + result[key] = value + return result + + +def _invalid_constant(value): + raise ValueError("Non-finite JSON constant: " + value) + + +def read_ticket_snapshot(path): + """Read actual exported records without inferring absent tickets or expiry.""" + with Path(path).open(encoding="utf-8") as stream: + value = json.load(stream, object_pairs_hook=_unique_object, parse_constant=_invalid_constant) + return validate_ticket_snapshot(value) + + +def isolation_precondition(snapshot, witnesses, dimension): + """Evaluate every active ticket globally; ISOLATED is not an unload witness.""" + validate_ticket_snapshot(snapshot) + if _dimension(dimension) != snapshot["dimension"]: + raise ValueError("Witness dimension differs from ticket map") + if not isinstance(witnesses, (list, tuple)) or not witnesses: + raise ValueError("At least one explicit released witness is required") + positions = [_position(pos) for pos in witnesses] + if len(set(positions)) != len(positions): + raise ValueError("Duplicate released witness") + blockers = [] + for position in positions: + for index, record in enumerate(snapshot["tickets"]): + if not record["active"]: + continue + distance = max(abs(a - b) for a, b in zip(position, record["pos"])) + effective = record["level"] + distance + if effective <= LAST_HOLDER_LEVEL: + blockers.append(dict(witness=list(position), ticketIndex=index, + ticketPos=list(record["pos"]), ticketType=record["type"], + ticketLevel=record["level"], distance=distance, effectiveLevel=effective, + fullDependency=effective <= FULL_LEVEL)) + return dict(status="BLOCKED" if blockers else "ISOLATED", + scope="STATIC_TICKET_PRECONDITION_ONLY_NOT_RUNTIME_PASS", + dimension=dimension, witnesses=[list(p) for p in positions], blockers=blockers) + + +def require_isolation(snapshot, witnesses, dimension): + result = isolation_precondition(snapshot, witnesses, dimension) + if result["blockers"]: + raise AssertionError("Native holder isolation precondition failed: " + + json.dumps(result["blockers"], sort_keys=True)) + return result diff --git a/test-rig/migration/native117_transformer_order.py b/test-rig/migration/native117_transformer_order.py new file mode 100644 index 00000000..439b2e25 --- /dev/null +++ b/test-rig/migration/native117_transformer_order.py @@ -0,0 +1,394 @@ +"""Observe real Forge 37 transformer callbacks and their emitted migration hooks. + +The three shared class targets can be registered in either order. Worldgen guards +use different methods, and Power's entity-storage transformer is unpaired. +""" + +import argparse +import json +import os +from pathlib import Path +import shutil +import socket +import subprocess +import time +from collections import Counter, defaultdict + +from native117 import boot as native117_boot +import actors +from native117_actor_io import capture_actors, capture_dimensions +from native117_actors import ready_entities +from native117_stopped_entities import audit_stopped +import transformer_order as historical +from matrix import WORLD, compare, copy_fixture, digest, fingerprint, write_json +from native117_code import Server +from native117_observation_plan import BRIDGE_SOURCE, CUSTODY, make_plan, seal_runtime, audit_runtime, read +from native117 import LEGACY_POWER, native_normalized, output_path, pinned_profile, require_clean_log, require_disjoint + + +# These are the packaged Forge 37.1.1 installer libraries, not the newer dev-cache versions. +COREMODS = Path("libraries/net/minecraftforge/coremods/5.0.1/coremods-5.0.1.jar") +BRIDGE_LIBRARIES = ( + "org/ow2/asm/asm/9.1/asm-9.1.jar", "org/ow2/asm/asm-tree/9.1/asm-tree-9.1.jar", + "cpw/mods/modlauncher/9.0.7/modlauncher-9.0.7.jar", + "com/google/code/gson/gson/2.8.0/gson-2.8.0.jar", +) +SUFFIXES = { + "world_info": ("net/minecraft/world/level/storage/LevelStorageSource", ["prepareLegacyWorld"]), + "chunk_status": ("net/minecraft/world/level/chunk/storage/ChunkStorage", ["prepareLegacyChunk", "finalizeLegacyChunk"]), + "worldgen_guard": ("net/minecraft/server/level/WorldGenRegion", ["shouldBlockWorldgenWrite"]), +} +ENTITY_NAME = "poweradvantage_separate_entity_storage" +ENTITY_CLASS = "net/minecraft/world/level/chunk/storage/EntityStorage" +NBT = "Lnet/minecraft/nbt/CompoundTag;" +WORLD_METHOD = ("(Lcom/mojang/serialization/DynamicOps;Lnet/minecraft/world/level/DataPackConfig;" + "Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;" + "Ljava/io/File;Lcom/mojang/datafixers/DataFixer;)Lnet/minecraft/world/level/storage/PrimaryLevelData;") +CHUNK_METHOD = "(Lnet/minecraft/resources/ResourceKey;Ljava/util/function/Supplier;" + NBT + ")" + NBT +ENTITY_METHOD = "(" + NBT + ")" + NBT +GUARD_METHODS = {"bm": "(Lnet/minecraft/core/BlockPos;)Z", + "pa": "(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;II)Z"} +OWNERS = {"bm": "zone/moddev/mc/basemetals/migration/LegacyWorldDataHook", + "pa": "zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook"} + + +def inspection_command(runtime): + custody = runtime / CUSTODY + receipt = read(custody / "receipt.json") + plan_path = custody / "observation-plan.json" + audit_runtime(runtime, plan_path, receipt["planSha256"]) + plan = read(plan_path) + build = read(plan["buildProof"]) + # Load the inspector from the pinned observer jar, never loose, mutable class output. + jars = [plan["observerJar"], *(str(Path(plan["seed"]) / name) for name in plan["dependencies"])] + return [build["commands"][1][0], "-cp", os.pathsep.join(jars), "qualification.order.TransformerOrderBridge", "--inspect"] + + +def inspect_dump(command, path): + result = subprocess.run([*command, str(path)], capture_output=True, text=True, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)) + if result.returncode: + raise AssertionError("Bytecode inspection failed: " + result.stdout + result.stderr) + return json.loads(result.stdout) + + +def event_dump(runtime, event, phase, label, command): + relative = Path(event[phase + "Dump"]) + if (relative.is_absolute() or "\\" in event[phase + "Dump"] or ":" in event[phase + "Dump"] + or ".." in relative.parts or len(relative.parts) < 2 or relative.parts[0] != "transformer-order-bytecode"): + raise AssertionError("Unsafe bytecode dump path") + if label: + relative = Path("transformer-order-bytecode-" + label, *relative.parts[1:]) + path = runtime / relative + if not path.resolve().is_relative_to(runtime.resolve()) or digest(path) != event[phase + "Sha256"]: + raise AssertionError("Bytecode dump hash changed") + inspected = inspect_dump(command, path) + if inspected != {"class": event["class"], "hooks": event[phase]}: + raise AssertionError("Event hooks differ from actual bytecode dump") + + +def hook_owner(hook, power_mod="poweradvantage"): + for mod, owner in OWNERS.items(): + if mod == "pa": + owner = "zone/moddev/mc/" + power_mod + "/migration/LegacyWorldDataHook" + if hook["owner"] == owner: + return mod + raise AssertionError("Unexpected hook owner") + + +def own_hooks(event, phase, mod, name, power_mod): + return [hook for hook in event[phase] if hook_owner(hook, power_mod) == mod and hook["name"] == name] + + +def require_injection(event, mod, name, method_descriptor, call_descriptor, multiple=False, *, + power_mod="poweradvantage"): + before = own_hooks(event, "before", mod, name, power_mod) + after = own_hooks(event, "after", mod, name, power_mod) + if before or not after or (not multiple and len(after) != 1): + raise AssertionError("Missing or duplicate injection: " + mod + "/" + name) + if any(not hook["method"].endswith(method_descriptor) or hook["descriptor"] != call_descriptor for hook in after): + raise AssertionError("Wrong native hook descriptor or target method: " + name) + + +def native_order_evidence(runtime, mode, label=None, *, power_mod="poweradvantage"): + if power_mod not in ("poweradvantage", "basepower"): + raise ValueError("Unsupported Power mod identity") + inject = lambda *args, **kwargs: require_injection(*args, power_mod=power_mod, **kwargs) + runtime = Path(runtime) + suffix = "-" + label if label else "" + events = [json.loads(line) for line in (runtime / ("transformer-order-events" + suffix + ".jsonl")).read_text().splitlines()] + registrations = [event for event in events if event["event"] == "registration"] + if (len(registrations) != 1 or not events or events[0] != registrations[0] + or registrations[0].get("mode") != mode or registrations[0].get("bridge") != "native117-v1" + or [event.get("sequence") for event in events] != list(range(1, len(events) + 1))): + raise AssertionError("Missing, duplicated or unordered real Forge registration evidence") + registration = registrations[0] + natural, forced = registration["natural"], registration["forced"] + selected = [mod + "_legacy_" + suffix for suffix in SUFFIXES for mod in ("basemetals", "poweradvantage")] + [ENTITY_NAME] + if any(natural.count(name) != 1 for name in selected) or Counter(natural) != Counter(forced): + raise AssertionError("Missing, duplicate or substituted real transformer registration") + expected_registration = list(natural) + for suffix in SUFFIXES: + bm, pa = (expected_registration.index(mod + "_legacy_" + suffix) for mod in ("basemetals", "poweradvantage")) + if mode != "natural" and ((bm < pa) != (mode == "bm-first")): + expected_registration[bm], expected_registration[pa] = expected_registration[pa], expected_registration[bm] + if mode not in historical.ORDERS or forced != expected_registration: + raise AssertionError("Registration changed beyond the requested real pairs") + command = inspection_command(runtime) + groups = defaultdict(list) + for event in events[1:]: + if event.get("event") != "application" or event.get("transformer") not in selected: + raise AssertionError("Unexpected transformer application event") + name = event["transformer"] + owner = "basemetals" if name.startswith("basemetals_") else power_mod + if event.get("labels") != [owner, name] or event.get("delegate") != "net.minecraftforge.coremod.transformer.CoreModClassTransformer": + raise AssertionError("Application is not a labelled real CoreMods class transformer") + for phase in ("before", "after"): + event_dump(runtime, event, phase, label, command) + groups[event["class"]].append(event) + if set(groups) != {value[0] for value in SUFFIXES.values()} | {ENTITY_CLASS}: + raise AssertionError("Missing or unexpected native class applications") + proof = {} + for suffix, (class_name, methods) in SUFFIXES.items(): + applications = groups[class_name] + expected = [name for name in forced if name in ("basemetals_legacy_" + suffix, "poweradvantage_legacy_" + suffix)] + if [event["transformer"] for event in applications] != expected: + raise AssertionError("Wrong actual application order for " + class_name) + if applications[1]["before"] != applications[0]["after"]: + raise AssertionError("Transformers did not receive the same evolving class node") + for event in applications: + mod = "bm" if event["transformer"].startswith("basemetals_") else "pa" + if suffix == "world_info": + inject(event, mod, "prepareLegacyWorld", WORLD_METHOD, "(Ljava/io/File;)V") + elif suffix == "chunk_status": + inject(event, mod, "prepareLegacyChunk", CHUNK_METHOD, "(" + NBT + ")V") + inject(event, mod, "finalizeLegacyChunk", CHUNK_METHOD, ENTITY_METHOD, multiple=True) + if mod == "pa": + inject(event, mod, "prepareLegacyEntities", CHUNK_METHOD, "(" + NBT + ")V") + inject(event, mod, "copyChunkForMigration", "(Lnet/minecraft/world/level/ChunkPos;)" + NBT, ENTITY_METHOD, multiple=True) + else: + call = "(Lnet/minecraft/server/level/WorldGenRegion;Lnet/minecraft/core/BlockPos;)Z" if mod == "bm" else GUARD_METHODS["bm"] + inject(event, mod, "shouldBlockWorldgenWrite", GUARD_METHODS[mod], call) + execution = {} + for method in methods: + grouped = defaultdict(list) + for hook in applications[-1]["after"]: + if hook["name"] == method: + grouped[hook["method"]].append(hook_owner(hook, power_mod)) + mods = ["bm" if name.startswith("basemetals_") else "pa" for name in expected] + if suffix != "worldgen_guard": + pair = mods if method == "finalizeLegacyChunk" else list(reversed(mods)) + if not grouped or any(len(values) % 2 or values != pair * (len(values) // 2) for values in grouped.values()): + raise AssertionError("Wrong emitted hook order: " + method) + else: + if sorted(grouped.values()) != [["bm"], ["pa"]]: + raise AssertionError("Independent native worldgen guard sites were conflated") + execution[method] = dict(grouped) + if suffix == "chunk_status": + hooks = [hook for hook in applications[-1]["after"] if hook["method"].endswith(CHUNK_METHOD)] + entities = next(index for index, hook in enumerate(hooks) if hook["name"] == "prepareLegacyEntities") + prepares = [index for index, hook in enumerate(hooks) if hook["name"] == "prepareLegacyChunk"] + finalizers = [index for index, hook in enumerate(hooks) if hook["name"] == "finalizeLegacyChunk"] + if len(prepares) != 2 or max(prepares) >= entities or min(finalizers) <= entities: + raise AssertionError("Entity preparation must follow both chunk preparation hooks and precede finalization") + proof[suffix] = {"application_order": expected, "emitted_hook_order": execution, + "bytecode_sha256": applications[-1]["afterSha256"]} + entities = groups[ENTITY_CLASS] + if len(entities) != 1 or entities[0]["transformer"] != ENTITY_NAME: + raise AssertionError("Missing or duplicated Power-only entity-storage observation") + inject(entities[0], "pa", "prepareEntityChunkForMigration", ENTITY_METHOD, ENTITY_METHOD) + inject(entities[0], "pa", "finalizeLegacyChunk", ENTITY_METHOD, ENTITY_METHOD, multiple=True) + if entities[0]["after"][0]["name"] != "prepareEntityChunkForMigration": + raise AssertionError("Separate entity preparation must precede finalization") + if any(hook_owner(hook, power_mod) != "pa" for hook in entities[0]["after"]): + raise AssertionError("Invented Base Metals entity-storage partner") + proof["separate_entity_storage"] = {"application_order": [ENTITY_NAME], "paired": False, + "hooks": entities[0]["after"], "bytecode_sha256": entities[0]["afterSha256"]} + return {"status": "PASS", "mode": mode, "classes": proof, "registration": registration} + + +def build_bridge(root, target, java): + seed = target[0] + libraries = seed / "libraries" + dependencies = [libraries / path for path in BRIDGE_LIBRARIES] + for dependency in dependencies: + if not dependency.is_file(): + raise FileNotFoundError("Missing exact packaged bridge library: " + str(dependency)) + classes = root / "probe-classes" + classes.mkdir() + source = BRIDGE_SOURCE + cp = os.pathsep.join(str(path) for path in dependencies) + commands = [[str(java.with_name("javac.exe" if os.name == "nt" else "javac")), "--release", "16", "-encoding", "UTF-8", + "-cp", cp, "-d", str(classes), str(source)], + [str(java), "-cp", str(classes) + os.pathsep + cp, "qualification.order.TransformerOrderBridge", + str(seed / COREMODS), str(root / "coremods-order-test.jar"), str(classes)]] + for command in commands: + result = subprocess.run(command, capture_output=True, text=True, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)) + if result.returncode: + raise RuntimeError(result.stdout + result.stderr) + proof = historical.validate_patch(seed / COREMODS, root / "coremods-order-test.jar") + proof.update({"sourceSha256": digest(source), "commands": commands, + "dependencies": {str(path): digest(path) for path in dependencies}}) + write_json(root / "probe-build.json", proof) + return root / "coremods-order-test.jar" + + +def checkpoint(runtime, java, launcher, label, first): + server = Server(runtime, java, launcher, label) + try: + server.snapshot(label) + readiness = ready_entities(server, runtime, "actors", "order-" + label) + actors.actor_command(server, True, "snapshot" if first else "read", label) + frozen = actors.freeze_result(json.loads((runtime / f"actors-{label}.json").read_text())) + state = capture_actors(runtime, label) + historical.retain_player_evidence(runtime, label) + if first: + actors.actor_command(server, True, "read", label + "-adapter") + adapter = capture_actors(runtime, label + "-adapter") + adapter_freeze = actors.freeze_result(json.loads((runtime / f"actors-{label}-adapter.json").read_text())) + historical.retain_player_evidence(runtime, label + "-adapter") + else: + adapter, adapter_freeze = state, frozen + adapter.pop("embedded", None) + if not first: + state.pop("embedded", None) + if frozen["status"] != "PASS" or adapter_freeze["status"] != "PASS": + raise AssertionError("Transformer controls require genuinely frozen actors") + remote = 1024 if first else 2048 + server.send(f"forceload add {remote} 1024") + server.send("say NATIVE117-ORDER-GUARD-SCHEDULED") + server.wait("NATIVE117-ORDER-GUARD-SCHEDULED", 30) + event_file = runtime / "transformer-order-events.jsonl" + deadline = time.monotonic() + 90 + while time.monotonic() < deadline: + if event_file.exists() and event_file.read_text().count('"class":"net/minecraft/server/level/WorldGenRegion"') == 2: + break + time.sleep(0.2) + else: + raise AssertionError("Native remote generation did not apply both real guard transformers") + server.send(f"forceload remove {remote} 1024") + server.send("save-all flush") + server.wait("Saved the", 45) + finally: + server.close() + stopped = audit_stopped(runtime, runtime / "order-stopped-entities" / label) + if stopped["status"] != "PASS" or stopped["matchedEntities"] < 1: + raise AssertionError("Transformer route lacks nonempty saved-entity proof") + for prefix in ("console", "latest", "debug"): + require_clean_log(runtime / f"{prefix}-{label}.log") + proof = native_order_evidence(runtime, (runtime / "transformer-order.mode").read_text().strip()) + for old, new in (("transformer-order-events.jsonl", f"transformer-order-events-{label}.jsonl"), + ("transformer-order-bytecode", f"transformer-order-bytecode-{label}")): + shutil.move(str(runtime / old), str(runtime / new)) + write_json(runtime / f"order-proof-{label}.json", proof) + write_json(runtime / f"semantic-{label}.json", state) + write_json(runtime / f"semantic-{label}-adapter.json", adapter) + return state, adapter, {"status": "PASS", "order": proof, "freeze": frozen, + "adapterFreeze": adapter_freeze, "readiness": readiness, + "savedEntities": {key: stopped[key] for key in + ("status", "snapshotSha256", "matchedEntities", "auditedDimensions")}} + + +def run(args): + root = output_path(args.output) + require_disjoint(root, args.fixtures) + target = pinned_profile(args.target_manifest.resolve(), "target117") + if not args.probe_only: + with socket.socket() as port: + port.bind(("127.0.0.1", args.port)) + root.mkdir(parents=True) + patched = build_bridge(root, target, args.java8) + plan_path = root / "observation-plan.json" + write_json(plan_path, make_plan(target[0], digest(target[0] / "native117-code-inventory.json"), + patched, digest(patched), root / "probe-build.json", digest(root / "probe-build.json"))) + plan_sha256 = digest(plan_path) + if args.probe_only: + write_json(root / "probe-only-result.json", dict(status="PASS_BRIDGE_BUILD_ONLY_NO_MC", + coremods=COREMODS.as_posix(), modLauncher="9.0.7", observerJarSha256=digest(patched), + observationPlanSha256=plan_sha256, realApplicationOrders="NOT_RUN", gameplay="NOT_RUN")) + print("PASS_BRIDGE_BUILD_ONLY_NO_MC: real transformer application and gameplay checks remain pending") + return 0 + results, cross, fingerprints = {}, {}, {} + for name in args.sources: + source = args.fixtures / name + before = fingerprint(source) + if fingerprint(source / WORLD) != json.loads((source / "fixture-hashes.json").read_text()): + raise AssertionError("Protected original actor fixture changed") + fingerprints[name] = {"source": str(source.resolve()), "fingerprint": before} + write_json(root / "source-fingerprints.json", fingerprints) + try: + mixed = root / (name + "-mixed-input") + mixed.mkdir() + expected = historical.prepare_fixture(source, mixed) + mixed_hashes = fingerprint(mixed) + blocks = LEGACY_POWER / "run-migration-matrix" / ( + "original-cyano-20261005-r1/original110" if name == "original110" else "full-20261005-r1/maintained112") + modes = {} + for mode in args.orders: + runtime = root / (name + "-" + mode) + launcher = native117_boot(runtime, target, args.java8, args.port) + (runtime / "advantage-works.allow").touch() + shutil.copy2(patched, runtime / COREMODS) + seal_runtime(runtime, plan_path, plan_sha256) + (runtime / "transformer-order.mode").write_text(mode) + copy_fixture(mixed, runtime) + shutil.copy2(mixed / "actor-fixture.json", runtime / "actor-fixture.json") + converted, adapter, first = checkpoint(runtime, args.java8, launcher, "converted", True) + backups = historical.migration_backups(runtime) + provenance = historical.backup_provenance(mixed, runtime) + restarted, _, second = checkpoint(runtime, args.java8, launcher, "restarted", False) + checks = { + "conversion": actors.compare({k: v for k, v in expected.items() if k != "adapter"}, converted), + "adapter": actors.compare({k: v for k, v in expected.items() if k != "embedded"}, adapter), + "restart": actors.compare({k: v for k, v in expected.items() if k != "embedded"}, restarted), + "idempotence": historical.strict_equal(adapter, restarted), + "backup-stability": historical.strict_equal(backups, historical.migration_backups(runtime)), + "backup-provenance": provenance, "converted-controls": first, "restart-controls": second, + "observation-code-custody": dict(status="PASS", proof=audit_runtime(runtime, plan_path, plan_sha256)), + } + for label in ("converted", "restarted"): + entries = json.loads((runtime / f"migration-{label}.json").read_text()) + stock = runtime / f"migration-stock-{label}.json" + write_json(stock, entries[:-len(historical.METAL_BLOCKS)]) + checks["blocks-" + label] = compare(blocks / "migration-source.json", stock, native_normalized) + checks["metals-" + label] = historical.strict_equal(list(historical.METAL_BLOCKS), + [entry["actualName"] for entry in entries[-len(historical.METAL_BLOCKS):]]) + historical.assert_staged(runtime) + modes[mode] = {"actors": adapter, "actorsRestart": restarted, + "blocks": historical.block_semantics(runtime, "converted"), + "blocksRestart": historical.block_semantics(runtime, "restarted")} + results[name + "/" + mode] = checks + write_json(runtime / "order-results.json", checks) + write_json(root / "results.json", results) + print(name + "/" + mode + ": " + ", ".join(key + "=" + value["status"] for key, value in checks.items()), flush=True) + cross[name] = {mode: historical.strict_equal(modes[args.orders[0]], value) for mode, value in modes.items()} + if fingerprint(mixed) != mixed_hashes: + raise AssertionError("Disposable mixed input was altered by destination runs") + finally: + if fingerprint(source) != before: + raise AssertionError("Protected transformer source changed") + failed = any(value["status"] != "PASS" for checks in results.values() for value in checks.values()) + failed |= any(value["status"] != "PASS" for modes in cross.values() for value in modes.values()) + dependent = any(not checks["backup-provenance"]["power_backup_matches_original"] for checks in results.values()) + status = "FAIL" if failed else "PASS_WITH_BACKUP_ORDER_DEPENDENCE" if dependent else "PASS" + write_json(root / "summary.json", {"status": status, "semanticStatus": "FAIL" if failed else "PASS", "crossOrder": cross, + "runtimes": len(results), "serverLaunches": len(results) * 2, "sourceFixtures": "UNCHANGED", + "targetManifestSha256": digest(args.target_manifest), "patchedLibrarySha256": digest(patched), + "observationPlanSha256": plan_sha256, + "powerBackupOriginOrderDependent": dependent}) + return int(failed) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--fixtures", type=Path, required=True) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java16", "--java8", dest="java8", type=Path, required=True, + help="Java 16 executable for the native 1.17 server and probe compiler") + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, choices=(25574, *range(25584, 25588)), required=True) + parser.add_argument("--sources", nargs="+", choices=("original110", "maintained112"), default=["original110", "maintained112"]) + parser.add_argument("--orders", nargs="+", choices=historical.ORDERS, default=list(historical.ORDERS)) + parser.add_argument("--probe-only", action="store_true") + raise SystemExit(run(parser.parse_args())) diff --git a/test-rig/migration/oil_transfer.py b/test-rig/migration/oil_transfer.py index 017e832b..a71f2b86 100644 --- a/test-rig/migration/oil_transfer.py +++ b/test-rig/migration/oil_transfer.py @@ -11,7 +11,7 @@ from matrix import POWER, RIG, digest, stage, write_json -def fixtures(): +def fixtures(namespace="poweradvantage"): result = [] for index, (oil, tank) in enumerate(( (oil, tank) for oil in ("crude_oil", "refined_oil") @@ -24,8 +24,8 @@ def fixtures(): output = [x + 12, y - 3, z] result.append({"id": f"OIL-{oil}-{tank}", "min": [x, 72, 2048], "max": [x + 31, 88, 2079], "source": source, "drain": drain, "pipes": pipes, "output": output, - "fluid": "poweradvantage:" + oil, "bucket": "poweradvantage:" + oil + "_bucket", - "tank": "poweradvantage:" + tank, "witnesses": [source, drain, *pipes, output]}) + "fluid": f"{namespace}:{oil}", "bucket": f"{namespace}:{oil}_bucket", + "tank": f"{namespace}:{tank}", "witnesses": [source, drain, *pipes, output]}) return result @@ -36,11 +36,12 @@ def inspect(snapshot, definition, expected): source = at(snapshot, f["id"], f["source"]) drain = at(snapshot, f["id"], f["drain"]) output = at(snapshot, f["id"], f["output"]) + namespace = f["tank"].split(":", 1)[0] require(source["block"] == "minecraft:air", f"Source not collected: {f['id']}") - require(drain["block"] == "poweradvantage:fluid_drain", f"Drain missing: {f['id']}") + require(drain["block"] == f"{namespace}:fluid_drain", f"Drain missing: {f['id']}") require(output["block"] == f["tank"], f"Container changed: {f['id']}") for pos in f["pipes"]: - require(at(snapshot, f["id"], pos)["block"] == "poweradvantage:fluid_pipe", f"Pipe missing: {f['id']}") + require(at(snapshot, f["id"], pos)["block"] == f"{namespace}:fluid_pipe", f"Pipe missing: {f['id']}") held = parse(drain).get("Tank", {}).get("Amount", 0) tank = parse(output).get("Tank", {}) require(held == 0, f"Oil stuck in drain ({held} mB): {f['id']}") diff --git a/test-rig/migration/progression.py b/test-rig/migration/progression.py index 8197d1d4..f27e79c3 100644 --- a/test-rig/migration/progression.py +++ b/test-rig/migration/progression.py @@ -41,15 +41,15 @@ def fixture(): "machineBounds": {"min": [16384, 78, 16384], "max": [16415, 88, 16415]}} -def validate_tags(result, mode, rubber): +def validate_tags(result, mode, rubber, *, power="poweradvantage", electric="electricadvantage"): tags = result["tags"] - assert "electricadvantage:electric_conduit" in tags["wires"] + assert f"{electric}:electric_conduit" in tags["wires"] assert {"basemetals:coal_powder", "basemetals:charcoal_powder"} <= set(tags["dusts/carbon"]) - assert ("poweradvantage:bioplastic_ingot" in tags["rubber"]) == rubber - assert {"poweradvantage:bioplastic_ingot", "electricadvantage:petrolplastic_ingot"} <= set(tags["plastic"]) - assert "electricadvantage:control_circuit" in tags["circuit_boards"] - assert "electricadvantage:blank_circuit_board" not in tags["circuit_boards"] - assert "electricadvantage:solder" in tags["solder"] + assert (f"{power}:bioplastic_ingot" in tags["rubber"]) == rubber + assert {f"{power}:bioplastic_ingot", f"{electric}:petrolplastic_ingot"} <= set(tags["plastic"]) + assert f"{electric}:control_circuit" in tags["circuit_boards"] + assert f"{electric}:blank_circuit_board" not in tags["circuit_boards"] + assert f"{electric}:solder" in tags["solder"] for metal in ("lead", "tin", "silver"): assert (f"basemetals:{metal}_ingot" in tags["solder"]) == (mode == "NORMAL") diff --git a/test-rig/migration/semantic_machine_audit.py b/test-rig/migration/semantic_machine_audit.py index a2a6fee1..7d8f0188 100644 --- a/test-rig/migration/semantic_machine_audit.py +++ b/test-rig/migration/semantic_machine_audit.py @@ -189,7 +189,7 @@ def audit_connected(root, ticks=512): status="FAIL" if counts["FAIL"] else "INCOMPLETE" if counts["INCOMPLETE"] else "PASS") -def endurance_batch(documents, definitions): +def endurance_batch(documents, definitions, *, identity=lambda value: value): """Check measured endpoint facts, not the ledger's asserted output counts.""" failures, frames = [], {} try: @@ -222,7 +222,7 @@ def contents(station, relative): return counts for station, pos, neighbours, block, product, fuel in BATCH_MACHINES: - if at(station, pos)[0]["block"] != block: raise ValueError("Wrong processing machine") + if at(station, pos)[0]["block"] != identity(block): raise ValueError("Wrong processing machine: " + block) counts = sum((contents(station, p) for p in (pos, *neighbours)), Counter()) if fuel: counts.pop("minecraft:coal", None) if counts != {"minecraft:" + product: 4}: raise ValueError("Incorrect exact processing yield: " + block) @@ -252,7 +252,7 @@ def contents(station, relative): amount = tank.get("Amount") if not isinstance(amount, int) or amount < 0 or "FluidName" not in tank: raise ValueError("Malformed oil amount/identity") if amount: fluids[tank["FluidName"]] += amount - if fluids != {"poweradvantage:refined_oil": 1000}: raise ValueError("Incorrect oil quantity/identity") + if fluids != {identity("poweradvantage:refined_oil"): 1000}: raise ValueError("Incorrect oil quantity/identity") except (KeyError, ValueError, TypeError) as error: failures.append(str(error)) return dict(status="FAIL" if failures else "PASS", failures=failures) diff --git a/test-rig/migration/steam_control_audit.py b/test-rig/migration/steam_control_audit.py index 2c513211..0533e166 100644 --- a/test-rig/migration/steam_control_audit.py +++ b/test-rig/migration/steam_control_audit.py @@ -155,7 +155,7 @@ def progress(data): return int(value) -def control_case(row, saved, declaration, base, line): +def control_case(row, saved, declaration, base, line, namespace="steamadvantage"): index = saved.get("index") require(type(index) is int and index >= 0, "Missing station index") require(type(row.get("startTick")) is int and type(row.get("finishTick")) is int @@ -185,15 +185,15 @@ def at(x, y=4, z=8): "Tile hold must remove ticking, retain the same tile and leave NBT unchanged") observed = measured.get(at(*point)) require(observed is not None and "nbt" in observed - and observed["block"] == ("steamadvantage:steam_furnace" if row["id"] in FURNACES - else "steamadvantage:steam_crusher"), + and observed["block"] == namespace + (":steam_furnace" if row["id"] in FURNACES + else ":steam_crusher"), "Held tile is not the measured native machine") actual.add(point) require(actual == expected, "Incomplete measured tile hold coverage") return at -def audit_documents(catalog, operation, fixture, cold, line): +def audit_documents(catalog, operation, fixture, cold, line, namespace="steamadvantage"): report = {"status": "FAIL", "line": line, "scope": "Six Steam first-tick/control cases plus full declared operation/cold checks", "cases": {}, "failures": []} try: @@ -214,7 +214,7 @@ def audit_documents(catalog, operation, fixture, cold, line): require(report["cold"]["status"] == "PASS", "Base exact cold-restart audit failed") for case_id in sorted(IDS): row = observed[case_id] - at = control_case(row, saved[case_id], declared[case_id], base, line) + at = control_case(row, saved[case_id], declared[case_id], base, line, namespace) require(positions(saved[case_id]["positions"], base, line) == positions(restarted[case_id]["positions"], base, line), "Control typed NBT changed across cold restart") @@ -228,7 +228,7 @@ def audit_documents(catalog, operation, fixture, cold, line): for stage in ("before", "after"): require(set(positions(row[stage], base, line)) == expected_positions, "Wrong comparator layout") for i, count in enumerate(counts): - data = tile(row[stage], at(3 + 6 * i), "steamadvantage:steam_crusher", line) + data = tile(row[stage], at(3 + 6 * i), namespace + ":steam_crusher", line) inventory(data, 0, "minecraft:stone", count) require(energy(data) == 0 and progress(data) == 0, "Comparator witness must remain unpowered") for i, count in enumerate(counts): @@ -245,7 +245,7 @@ def audit_documents(catalog, operation, fixture, cold, line): require(set(snapshot) == expected_positions, "Wrong controlled-machine layout") if signal: require(snapshot[at(7)]["block"] == "minecraft:redstone_block", "Missing actual redstone source") - block = "steamadvantage:steam_crusher" if is_crusher else "steamadvantage:steam_furnace" + block = namespace + (":steam_crusher" if is_crusher else ":steam_furnace") before = tile(row["before"], at(8), block, line) after = tile(row["after"], at(8), block, line) item = "minecraft:stone" if is_crusher else "minecraft:sand" diff --git a/test-rig/migration/test_audit_native117_partial.py b/test-rig/migration/test_audit_native117_partial.py new file mode 100644 index 00000000..88f1268b --- /dev/null +++ b/test-rig/migration/test_audit_native117_partial.py @@ -0,0 +1,75 @@ +"""Independent native audit must fail closed on missing or altered engine proof.""" + +import copy +import json +from pathlib import Path +import tempfile +import unittest + +from audit_native117_partial import geometry_evidence, require_fixture_definition, snapshot + + +GEOMETRY = dict(fullLevel=33, entityTickingLevel=31, maxDistance=11, + lastHolderLevel=44, holderRadius=13, observedForcedLevels=[31]) + + +class NativeFixtureAuditTests(unittest.TestCase): + def test_only_expected_legacy_registry_ids_are_translated(self): + expected = [{"block": "steamadvantage:steam_drill", "pos": (1, 2, 3)}] + require_fixture_definition([{"block": "basesteam:steam_drill", "pos": [1, 2, 3]}], expected) + for observed in ([{"block": "steamadvantage:steam_drill", "pos": [1, 2, 3]}], + [{"block": "basesteam:steam_drill", "pos": [1, 2, 4]}]): + with self.subTest(observed=observed), self.assertRaisesRegex(AssertionError, "registry IDs differ"): + require_fixture_definition(observed, expected) + + +class NativeGeometryAuditTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.runtime = Path(self.temp.name) + self.results = self.runtime / "advantage-works-results" + self.results.mkdir() + + def observation(self, label, geometry): + path = self.results / ("lifecycle-" + label + ".json") + path.write_text(json.dumps(dict(worldTime=42, ticketGeometry=geometry)), encoding="utf-8") + return path + + def test_raw_geometry_is_checked_without_mutating_evidence(self): + path = self.observation("valid", GEOMETRY) + original = path.read_bytes() + self.assertEqual(snapshot(self.runtime, "valid")["ticketGeometry"], GEOMETRY) + result = geometry_evidence(self.runtime) + self.assertEqual(result["snapshots"], 1) + self.assertEqual(result["forced_ticket_snapshots"], 1) + self.assertEqual(path.read_bytes(), original) + + def test_an_altered_raw_observation_cannot_hide_behind_valid_canonical(self): + self.observation("canonical", GEOMETRY) + altered = copy.deepcopy(GEOMETRY) + altered["lastHolderLevel"] = 43 + self.observation("pending", altered) + with self.assertRaisesRegex(AssertionError, "geometry differs"): + geometry_evidence(self.runtime) + + def test_no_observed_forced_ticket_is_not_runtime_geometry_proof(self): + missing = copy.deepcopy(GEOMETRY) + missing["observedForcedLevels"] = [] + self.observation("unforced", missing) + with self.assertRaisesRegex(AssertionError, "No observed live"): + geometry_evidence(self.runtime) + + def test_missing_geometry_and_wrong_forced_level_are_rejected(self): + for value in ({}, dict(GEOMETRY, observedForcedLevels=[30])): + self.observation("invalid", value) + with self.assertRaises(AssertionError): + snapshot(self.runtime, "invalid") + + def test_empty_evidence_is_rejected(self): + with self.assertRaisesRegex(AssertionError, "No raw native"): + geometry_evidence(self.runtime) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_broad_result_audit.py b/test-rig/migration/test_broad_result_audit.py index 2353916f..47083bf2 100644 --- a/test-rig/migration/test_broad_result_audit.py +++ b/test-rig/migration/test_broad_result_audit.py @@ -11,9 +11,12 @@ import zipfile import broad_result_audit as audit +from historical_test_inputs import POWER116, STATIONS, git_blob -LINE = Path(__file__).resolve().parents[3].name.removeprefix("PA") +# The 1.17 auditor has its own fixtures. Exercise the last supported legacy +# dialect here without tying the test to the name of the checkout directory. +LINE = "116" def write(path, data): @@ -32,8 +35,7 @@ def setUp(self): self.root = Path(self.directory.name) self.runtime = self.root / "broad" self.runtime.mkdir() - source = Path(__file__).resolve().parents[1] / "src/main/resources" / audit.RESOURCE - definition = source.read_bytes() + definition = git_blob(POWER116, STATIONS) self.stations = json.loads(definition)["stations"] self.definitions = {s["id"]: s for s in self.stations} jars = [] diff --git a/test-rig/migration/test_native114_broad.py b/test-rig/migration/test_native114_broad.py index 4bd6b421..090c372d 100644 --- a/test-rig/migration/test_native114_broad.py +++ b/test-rig/migration/test_native114_broad.py @@ -8,11 +8,12 @@ import nbtlib import native114_broad as broad +from historical_test_inputs import POWER116, STATIONS, git_blob class BroadOracleTests(unittest.TestCase): def setUp(self): - definition = broad.read_json(broad.RIG / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json") + definition = json.loads(git_blob(POWER116, STATIONS)) self.stations = [s for s in definition["stations"] if s["id"] in broad.DRILL_BAYS] def snapshot(self, station, moved=False): diff --git a/test-rig/migration/test_native115_broad.py b/test-rig/migration/test_native115_broad.py index d95e72c6..631863ec 100644 --- a/test-rig/migration/test_native115_broad.py +++ b/test-rig/migration/test_native115_broad.py @@ -8,11 +8,12 @@ import nbtlib import native115_broad as broad +from historical_test_inputs import POWER116, STATIONS, git_blob class BroadOracleTests(unittest.TestCase): def setUp(self): - definition = broad.read_json(broad.RIG / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json") + definition = json.loads(git_blob(POWER116, STATIONS)) self.stations = [s for s in definition["stations"] if s["id"] in broad.DRILL_BAYS] def snapshot(self, station, moved=False): diff --git a/test-rig/migration/test_native115_geometry.py b/test-rig/migration/test_native115_geometry.py index 2b418daa..944045ed 100644 --- a/test-rig/migration/test_native115_geometry.py +++ b/test-rig/migration/test_native115_geometry.py @@ -8,7 +8,7 @@ import unittest import tempfile -from matrix import digest +from historical_test_inputs import POWER116, oracle_bytes from native115 import RIG from native115_geometry import require_geometry from native115_campaign import ACCEPTED_POWER114, REQUIRED, require_routes, require_source114_receipt @@ -138,9 +138,10 @@ def verify(data, expected, record): for relative, checksum in ledger["hashes"].items(): with self.subTest(path=relative): if relative in records: - verify((RIG / relative).read_bytes(), checksum, records[relative]) + verify(oracle_bytes(POWER116, relative), checksum, records[relative]) else: - self.assertEqual(digest(RIG / relative), checksum) + data = oracle_bytes(ledger["sourceCommit"], relative) + self.assertEqual(hashlib.sha256(data).hexdigest().upper(), checksum) # Updating only a byte hash must not bless a lower threshold or unrelated oracle edit. source = (RIG / "migration/extended_lifecycle.py").read_bytes() diff --git a/test-rig/migration/test_native115_harvest.py b/test-rig/migration/test_native115_harvest.py index 66a90952..1bc4e8fa 100644 --- a/test-rig/migration/test_native115_harvest.py +++ b/test-rig/migration/test_native115_harvest.py @@ -8,6 +8,7 @@ from uuid import UUID import native115_harvest as native +from historical_test_inputs import JAVA, POWER115, git_blob def sample(label="exercise"): @@ -44,8 +45,7 @@ def test_original_270_harvest_oracles_are_exactly_preserved(self): self.assertEqual(270, len(native.EXPECTED)) def test_java_witness_inputs_masks_trial_counts_and_thresholds_are_unchanged(self): - source = (Path(__file__).resolve().parents[1] / - "src/main/java/zone/moddev/mc/advantageworks/NativeHarvestProbe.java").read_text() + source = git_blob(POWER115, JAVA + "NativeHarvestProbe.java").decode("utf-8") region = source.split(" String sulfur =", 1)[1].split(" JsonObject solar =", 1)[0] self.assertEqual("3FF0AC4C13CA1B737679F67128CE2F41C5A0873E89258D456628BA8A309BAA9A", hashlib.sha256(region.encode()).hexdigest().upper()) diff --git a/test-rig/migration/test_native116_broad.py b/test-rig/migration/test_native116_broad.py index 3ed31a87..4c53a8e2 100644 --- a/test-rig/migration/test_native116_broad.py +++ b/test-rig/migration/test_native116_broad.py @@ -8,11 +8,12 @@ import nbtlib import native116_broad as broad +from historical_test_inputs import POWER116, STATIONS, git_blob class BroadOracleTests(unittest.TestCase): def setUp(self): - definition = broad.read_json(broad.RIG / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json") + definition = json.loads(git_blob(POWER116, STATIONS)) self.stations = [s for s in definition["stations"] if s["id"] in broad.DRILL_BAYS] def snapshot(self, station, moved=False): diff --git a/test-rig/migration/test_native116_geometry.py b/test-rig/migration/test_native116_geometry.py index 54d4e9fa..b62e59ea 100644 --- a/test-rig/migration/test_native116_geometry.py +++ b/test-rig/migration/test_native116_geometry.py @@ -8,7 +8,7 @@ import unittest import tempfile -from matrix import digest +from historical_test_inputs import POWER116, oracle_bytes from native116 import RIG from native116_geometry import require_geometry from native116_campaign import ACCEPTED_POWER114, REQUIRED, require_routes, require_source114_receipt @@ -138,9 +138,10 @@ def verify(data, expected, record): for relative, checksum in ledger["hashes"].items(): with self.subTest(path=relative): if relative in records: - verify((RIG / relative).read_bytes(), checksum, records[relative]) + verify(oracle_bytes(POWER116, relative), checksum, records[relative]) else: - self.assertEqual(digest(RIG / relative), checksum) + data = oracle_bytes(ledger["sourceCommit"], relative) + self.assertEqual(hashlib.sha256(data).hexdigest().upper(), checksum) # Updating only a byte hash must not bless a lower threshold or unrelated oracle edit. source = (RIG / "migration/extended_lifecycle.py").read_bytes() diff --git a/test-rig/migration/test_native116_harvest.py b/test-rig/migration/test_native116_harvest.py index 66dd2d7d..de86649b 100644 --- a/test-rig/migration/test_native116_harvest.py +++ b/test-rig/migration/test_native116_harvest.py @@ -8,6 +8,7 @@ from uuid import UUID import native116_harvest as native +from historical_test_inputs import JAVA, POWER116, git_blob def sample(label="exercise"): @@ -44,8 +45,7 @@ def test_original_270_harvest_oracles_are_exactly_preserved(self): self.assertEqual(270, len(native.EXPECTED)) def test_java_witness_inputs_masks_trial_counts_and_thresholds_are_unchanged(self): - source = (Path(__file__).resolve().parents[1] / - "src/main/java/zone/moddev/mc/advantageworks/NativeHarvestProbe.java").read_text() + source = git_blob(POWER116, JAVA + "NativeHarvestProbe.java").decode("utf-8") region = source.split(" String sulfur =", 1)[1].split(" JsonObject solar =", 1)[0] self.assertEqual("3FF0AC4C13CA1B737679F67128CE2F41C5A0873E89258D456628BA8A309BAA9A", hashlib.sha256(region.encode()).hexdigest().upper()) diff --git a/test-rig/migration/test_native116_redstone_reload.py b/test-rig/migration/test_native116_redstone_reload.py index 8a2fd1c7..9d3adb8b 100644 --- a/test-rig/migration/test_native116_redstone_reload.py +++ b/test-rig/migration/test_native116_redstone_reload.py @@ -172,7 +172,9 @@ def test_readiness_is_bounded_and_not_resampled_until_operating_verdict_passes(s with self.assertRaises(AssertionError): validate_readiness(bad, "seed", 100) def test_probe_never_drives_ticks_or_loads_expected_nbt(self): - source = (Path(__file__).parents[1] / "src/main/java/zone/moddev/mc/advantageworks/RedstoneReloadProbe.java").read_text() + from historical_test_inputs import JAVA, POWER116, git_blob + + source = git_blob(POWER116, JAVA + "RedstoneReloadProbe.java").decode("utf-8") for forbidden in (".tickUpdate(", ".powerUpdate(", ".tick();", ".load("): self.assertNotIn(forbidden, source) self.assertIn("tickableBlockEntities.clear()", source) @@ -181,7 +183,7 @@ def test_probe_never_drives_ticks_or_loads_expected_nbt(self): self.assertIn('else if (ready("seed"))', source) self.assertIn('else if (ready("restart"))', source) self.assertIn("StandardOpenOption.CREATE_NEW", source) - mod = (Path(__file__).parents[1] / "src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java").read_text() + mod = git_blob(POWER116, JAVA + "AdvantageWorksMod.java").decode("utf-8") self.assertEqual(1, mod.count("RedstoneReloadProbe.register();")) diff --git a/test-rig/migration/test_native117.py b/test-rig/migration/test_native117.py new file mode 100644 index 00000000..3c0f23ff --- /dev/null +++ b/test-rig/migration/test_native117.py @@ -0,0 +1,143 @@ +"""Destination-only safety checks; no Minecraft process or source world is opened.""" + +import json +from pathlib import Path +import tempfile +import unittest +import zipfile + +from native117 import LEGACY_POWER, RIG, TEST_FLAT, configure_creation, jar_identity, native_normalized, output_path, pinned_profile, require_clean_log, require_disjoint, stage +from matrix import differences, normalized + + +class Native117DestinationTest(unittest.TestCase): + def setUp(self): + root = RIG / "build/port-evidence" + root.mkdir(parents=True, exist_ok=True) + self.temp = tempfile.TemporaryDirectory(dir=root) + self.root = Path(self.temp.name) + + def tearDown(self): + self.temp.cleanup() + + def test_legacy_source_root_is_explicit_pa113(self): + self.assertEqual(LEGACY_POWER.name, "PowerAdvantage") + self.assertEqual(LEGACY_POWER.parent.name, "PA113") + + def test_output_must_be_new_and_inside_native_build_scope(self): + self.assertEqual(output_path(self.root / "fresh"), self.root / "fresh") + with self.assertRaises(FileExistsError): + output_path(self.root) + with self.assertRaises(ValueError): + output_path(RIG.parent / "world") + + def test_neither_ancestor_nor_descendant_of_source_is_writable(self): + source = self.root / "source" + for output in (source, source / "nested", self.root): + with self.assertRaises(ValueError): + require_disjoint(output, source) + require_disjoint(self.root / "destination", source) + + def test_native_identity_and_version_are_both_required(self): + jar = self.root / "example.jar" + with zipfile.ZipFile(jar, "w") as archive: + archive.writestr("META-INF/mods.toml", '[[mods]]\nmodId="poweradvantage"\nversion="2.4.6.117011"\n') + jar_identity(jar, "poweradvantage", "2.4.6.117011") + for identity, version in (("steamadvantage", "2.4.6.117011"), ("poweradvantage", "2.4.4.113021")): + with self.assertRaises(ValueError): + jar_identity(jar, identity, version) + + def test_pinned_source_hash_failure_is_not_ignored(self): + artifact = self.root / "input.jar" + artifact.write_bytes(b"not-the-pinned-artifact") + manifest = self.root / "source-profiles.json" + manifest.write_text(json.dumps({"target113": {"seed": ".", "launcher": "forge.jar", + "jars": [{"path": "input.jar", "sha256": "0" * 64}]}})) + with self.assertRaises(AssertionError): + pinned_profile(manifest, "target113") + + def test_native_manifest_rejects_historical_runtime_even_with_no_jars(self): + manifest = self.root / "target.json" + manifest.write_text(json.dumps({"target117": {"seed": ".", "launcher": "forge-1.13.2-25.0.223.jar", "jars": []}})) + with self.assertRaises(AssertionError): + pinned_profile(manifest, "target117") + + def test_native_omitted_empty_chest_lock_is_semantically_unlocked(self): + old = {"id": "Chest", "Lock": "", "Items": []} + new = {"id": "minecraft:chest", "Items": []} + self.assertEqual(native_normalized(old), native_normalized(new)) + self.assertNotIn("Lock", normalized(new)) # Historical default is unchanged. + + def test_missing_or_changed_real_chest_locks_remain_failures(self): + unlocked = native_normalized({"id": "minecraft:chest"}) + locked = native_normalized({"id": "minecraft:chest", "Lock": "secret"}) + self.assertTrue(differences(unlocked, locked)) + self.assertTrue(differences(locked, unlocked)) + self.assertTrue(differences(locked, native_normalized({"id": "minecraft:chest", "Lock": "different"}))) + + def test_custom_machine_empty_key_is_not_dropped_or_defaulted(self): + old = native_normalized({"id": "poweradvantage:machine", "Lock": ""}) + new = native_normalized({"id": "poweradvantage:machine"}) + self.assertTrue(differences(old, new)) + + def test_startup_errors_cannot_hide_behind_a_successful_snapshot(self): + log = self.root / "console.log" + log.write_text("[Server thread/INFO] Done\n[Server thread/ERROR] Failed to reload data packs\n") + with self.assertRaises(AssertionError): + require_clean_log(log) + log.write_text("[Server thread/WARN] No data fixer registered for custom entity\n[Server thread/INFO] Done\n") + require_clean_log(log) + + def test_fatal_and_uncaught_java_failures_are_rejected_on_the_destination(self): + log = self.root / "console.log" + for failure in ("[Server thread/FATAL] [orespawn]: Loading Integration For basemetals", + 'Exception in thread "Server thread" java.lang.IllegalStateException: broken', + "Caused by: java.lang.NoClassDefFoundError: missing/Dependency", + "java.lang.VerifyError: invalid bytecode"): + with self.subTest(failure=failure): + log.write_text("[Server thread/INFO] Done\n" + failure + "\n") + with self.assertRaises(AssertionError): + require_clean_log(log) + + def test_renamed_advantage_pack_warnings_fail_even_after_successful_startup(self): + log = self.root / "legacy-pack.log" + for mod in ("poweradvantage", "steamadvantage", "electricadvantage"): + with self.subTest(mod=mod): + log.write_text(f"[main/WARN] [minecraft/MinecraftServer]: Missing data pack mod:{mod}\n" + "[Server thread/INFO] Done\n") + with self.assertRaisesRegex(AssertionError, "Missing data pack mod:" + mod): + require_clean_log(log) + log.write_text("[main/WARN] [minecraft/MinecraftServer]: Missing data pack mod:poweradvantage_extra\n") + require_clean_log(log) + + def test_new_stage_uses_harness_preset_not_vanilla_flat_json(self): + seed = self.root / "seed" + (seed / "libraries").mkdir(parents=True) + (seed / "forge.jar").write_bytes(b"launcher") + runtime = self.root / "new-runtime" + stage(runtime, (seed, "forge.jar", [])) + properties = dict(line.split("=", 1) for line in (runtime / "server.properties").read_text().splitlines() if "=" in line) + self.assertEqual(TEST_FLAT, properties["level-type"]) + self.assertEqual("false", properties["generate-structures"]) + self.assertEqual("", properties["generator-settings"]) + + def test_creation_properties_never_rewrite_an_imported_level_dat(self): + from matrix import WORLD, fingerprint + runtime = self.root / "disposable-import" + (runtime / WORLD).mkdir(parents=True) + (runtime / WORLD / "level.dat").write_bytes(b"immutable imported world data") + (runtime / "server.properties").write_text("level-type=FLAT\ngenerator-settings={}\ngenerate-structures=true\nmax-tick-time=60000\n") + before = fingerprint(runtime / WORLD) + configure_creation(runtime) + self.assertEqual(before, fingerprint(runtime / WORLD)) + self.assertIn("max-tick-time=60000", (runtime / "server.properties").read_text()) + + def test_registry_ops_error_remains_fatal_even_after_done(self): + log = self.root / "registry-ops-failure.log" + log.write_text("[main/ERROR] [minecraft/DimensionGeneratorSettings]: Not a registry ops\n[Server thread/INFO] Done\n") + with self.assertRaisesRegex(AssertionError, "Native runtime errors"): + require_clean_log(log) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_actor_io.py b/test-rig/migration/test_native117_actor_io.py new file mode 100644 index 00000000..bf0690b9 --- /dev/null +++ b/test-rig/migration/test_native117_actor_io.py @@ -0,0 +1,69 @@ +import copy +import unittest + +import actors +import dimensions +from native117_actor_io import capture_actors, capture_dimensions, semantic, source_identities +from native117_format import actor_nbt + + +class Native117ActorFormatTests(unittest.TestCase): + def test_only_source_registry_fields_are_rebranded(self): + old = {"file": {"Inventory": [{"id": "steamadvantage:musket", "Count": 1, + "tag": {"Enchantments": [{"id": "steamadvantage:recoil", "lvl": 1}], + "MigrationWitness": "steamadvantage:musket", + "Custom": {"id": "poweradvantage:starch"}}}]}, + "entities": {"turbine": {"id": "electricadvantage:hydroturbine"}}, + "generator": {"name": "electricadvantage:hydroelectric_generator", + "tile": {"id": "electricadvantage:hydroelectric_generator"}}} + expected = source_identities(old) + self.assertEqual("basesteam:musket", expected["file"]["Inventory"][0]["id"]) + self.assertEqual("baseelectric:hydroturbine", expected["entities"]["turbine"]["id"]) + self.assertEqual("baseelectric:hydroelectric_generator", expected["generator"]["tile"]["id"]) + tag = expected["file"]["Inventory"][0]["tag"] + self.assertEqual("basesteam:recoil", tag["Enchantments"][0]["id"]) + self.assertEqual("steamadvantage:musket", tag["MigrationWitness"]) + self.assertEqual("poweradvantage:starch", tag["Custom"]["id"]) + self.assertEqual("steamadvantage:musket", old["file"]["Inventory"][0]["id"]) + self.assertEqual("PASS", dimensions.verdict(expected, copy.deepcopy(expected))["status"]) + self.assertEqual("FAIL", dimensions.verdict(expected, old)["status"]) + changed = copy.deepcopy(expected) + changed["file"]["Inventory"][0]["tag"]["MigrationWitness"] = "basesteam:musket" + self.assertEqual("FAIL", dimensions.verdict(expected, changed)["status"]) + + def test_capture_algorithms_are_identical_without_mutating_historical_globals(self): + self.assertIs(capture_actors.__code__, actors.capture.__code__) + self.assertIs(capture_dimensions.__code__, dimensions.capture.__code__) + self.assertIsNot(capture_actors.__globals__, actors.capture.__globals__) + self.assertIsNot(actors.semantic, semantic) + + def test_modern_uuid_dimension_and_inventory_match_the_exact_old_identity(self): + old = dict(UUIDMost=1, UUIDLeast=2, Dimension=-1, Health=7.0, + Inventory=[dict(id="poweradvantage:starch", Count=3, Slot=1)]) + new = dict(UUID=[0, 1, 0, 2], Dimension="minecraft:the_nether", Health=7.0, + Inventory=copy.deepcopy(old["Inventory"])) + before = copy.deepcopy(new) + keys = tuple(old) + self.assertEqual(semantic(old, keys, {}), semantic(new, keys, {})) + self.assertEqual(before, new) + altered = copy.deepcopy(new) + altered["Inventory"][0]["Count"] -= 1 + self.assertTrue(actors.compare(dict(file=semantic(old, keys, {}), entities={}), + dict(file=semantic(altered, keys, {}), entities={}))['status'] == 'FAIL') + + def test_changed_uuid_health_and_dimension_are_not_ignored(self): + original = dict(UUID=[0, 1, 0, 2], Dimension="minecraft:overworld", Health=7.0) + for field, value in (("UUID", [0, 1, 0, 3]), ("Dimension", "minecraft:the_end"), ("Health", 20.0)): + altered = dict(original, **{field: value}) + self.assertNotEqual(actor_nbt(original), actor_nbt(altered)) + + def test_owner_uuid_is_exact_and_duplicates_are_rejected(self): + self.assertEqual("00000000-0000-0001-0000-000000000002", actor_nbt(dict(Owner=[0, 1, 0, 2]))['OwnerUUID']) + with self.assertRaises(ValueError): + actor_nbt(dict(UUID=[0, 1, 0, 2], UUIDMost=1)) + with self.assertRaises(ValueError): + actor_nbt(dict(Owner=[0, 1, 0, 2], OwnerUUID="other")) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_actor_readiness.py b/test-rig/migration/test_native117_actor_readiness.py new file mode 100644 index 00000000..be95f687 --- /dev/null +++ b/test-rig/migration/test_native117_actor_readiness.py @@ -0,0 +1,36 @@ +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import Mock, patch + +from native117_actors import ready_entities + + +class ActorReadinessTests(unittest.TestCase): + def test_observe_in_the_actual_fixture_dimension_before_snapshotting(self): + with tempfile.TemporaryDirectory() as temporary: + runtime = Path(temporary) + for kind, definition, expected in ( + ('actors', {}, 'minecraft:overworld'), + ('dimensions', {'dimension': -1}, 'minecraft:the_nether'), + ('dimensions', {'dimension': 1}, 'minecraft:the_end')): + name = 'actor-fixture.json' if kind == 'actors' else 'dimension-fixture.json' + (runtime / name).write_text(json.dumps(definition)) + server = Mock() + with patch('native117_actors.await_ready', return_value={'dimension': expected, 'status': 'PASS'}) as ready: + self.assertEqual('PASS', ready_entities(server, runtime, kind, 'converted')['status']) + server.send.assert_called_once_with('execute in ' + expected + ' run forceload add 96 -32 223 47') + self.assertEqual((6, -2, 13, 2), ready.call_args.args[2]) + + def test_overworld_readiness_cannot_stand_in_for_a_nether_fixture(self): + with tempfile.TemporaryDirectory() as temporary: + runtime = Path(temporary) + (runtime / 'dimension-fixture.json').write_text('{"dimension":-1}') + with patch('native117_actors.await_ready', return_value={'dimension': 'minecraft:overworld'}): + with self.assertRaisesRegex(AssertionError, 'wrong dimension'): + ready_entities(Mock(), runtime, 'dimensions', 'converted') + + +if __name__ == '__main__': + unittest.main() diff --git a/test-rig/migration/test_native117_actors.py b/test-rig/migration/test_native117_actors.py new file mode 100644 index 00000000..8a9a8cda --- /dev/null +++ b/test-rig/migration/test_native117_actors.py @@ -0,0 +1,170 @@ +import unittest +from pathlib import Path +import tempfile +from unittest.mock import Mock, patch +import json + +import dimensions +from native117_actors import await_natural_ticks, baseline, expected_state, native_dimension_state, release +from native117_audit_actors import inherited_sources, persistence_controls, replay + + +class NativeActorOracleTest(unittest.TestCase): + def test_actor_duration_waits_for_eighty_real_ticks_without_stepping_the_world(self): + server = Mock() + with patch("native117_actors.time.sleep"), patch("native117_actors.raw", side_effect=[ + {"gameTime": 100}, {"gameTime": 100}, {"gameTime": 179}, {"gameTime": 180}, + ]): + await_natural_ticks(server, Path("unused"), "dimensions", "release") + self.assertEqual(["advdimensions snapshot release-clock-" + str(i) for i in range(3)], + [call.args[0] for call in server.send.call_args_list]) + + def test_actor_clock_regression_is_not_retried(self): + server = Mock() + with patch("native117_actors.time.sleep"), patch("native117_actors.raw", side_effect=[ + {"gameTime": 100}, {"gameTime": 99}, + ]), self.assertRaisesRegex(AssertionError, "clock moved backwards"): + await_natural_ticks(server, Path("unused"), "actors", "release") + self.assertEqual(1, server.send.call_count) + + def test_frozen_comparison_retains_positions_and_nested_items(self): + old = {"file": {"Inventory": [{"Count": 17, "tag": {"nested": {"keep": 3}}}]}, + "entities": {"cart": {"Pos": [1, 2, 3], "Items": [{"Count": 4}]}}} + result = expected_state(old, "actors") + self.assertEqual(old, result) + result["entities"]["cart"]["Items"][0]["Count"] = 0 + self.assertEqual(4, old["entities"]["cart"]["Items"][0]["Count"]) + + def test_survival_only_removes_historical_movement_keys(self): + old = {"file": {}, "entities": {"wolf": {"Pos": [1, 2, 3], "Motion": [0, 0, 0], + "Rotation": [0, 0], "OwnerUUID": "owner", "ForgeData": {"keep": 8}}}} + self.assertEqual({"OwnerUUID": "owner", "ForgeData": {"keep": 8}}, + expected_state(old, "actors", survival=True)["entities"]["wolf"]) + + def test_dimension_oracle_keeps_live_relationships_and_quantities(self): + old = {"file": {"Inventory": [{"id": "steamadvantage:musket", "Count": 3}]}, + "entities": {"horse": {"Pos": [1, 2, 3], "Health": 20}}, + "relationships": {"horse": {"passengerUuids": ["wolf"]}}, "adapter": {"Dimension": -1}} + result = expected_state(old, "dimensions", survival=True) + self.assertEqual(old["relationships"], result["relationships"]) + self.assertEqual(old["file"], result["file"]) + + def test_native_leash_byte_is_derived_only_from_actual_saved_compound(self): + state = {"entities": {"horse": {"id": "minecraft:horse", "Leash": {"X": 1, "Y": 2, "Z": 3}}, + "wolf": {"id": "minecraft:wolf"}}, "relationships": {"horse": {"leashHolderType": "minecraft:leash_knot"}}} + result = native_dimension_state(state) + self.assertEqual(1, result["entities"]["horse"]["Leashed"]) + self.assertEqual(0, result["entities"]["wolf"]["Leashed"]) + self.assertNotIn("Leashed", state["entities"]["horse"]) + del state["entities"]["horse"]["Leash"] + self.assertEqual(0, native_dimension_state(state)["entities"]["horse"]["Leashed"]) + + def test_native_adapter_does_not_hide_vanilla_wolf_health_changes(self): + old = {"entities": {"wolf": {"id": "minecraft:wolf", "Health": 8.0}}, + "relationships": {}} + actual = {"entities": {"wolf": {"id": "minecraft:wolf", "Health": 20.0}}, + "relationships": {}} + expected = expected_state(old, "dimensions", survival=True) + result = dimensions.verdict(expected, native_dimension_state(actual)) + self.assertEqual("FAIL", result["status"]) + self.assertIn({"path": ".entities.wolf.Health", "expected": 8.0, "actual": 20.0}, + result["differences"]) + self.assertEqual(8.0, old["entities"]["wolf"]["Health"]) + + def test_dimension_release_never_saves_before_native_initialization(self): + server = Mock() + release(server, Path("unused"), "dimensions", "released") + server.send.assert_called_once_with("advdimensions operate released") + server.wait.assert_called_once_with("ADVDIMENSIONS COMPLETE released", 120) + + def test_actor_release_requests_real_tickets_not_synthetic_ticks(self): + server = Mock() + with patch("native117_actors.command", return_value="captured") as command: + self.assertEqual("captured", release(server, Path("unused"), "actors", "released")) + self.assertEqual("forceload add 96 -32 223 47", server.send.call_args_list[0].args[0]) + command.assert_called_once_with(server, Path("unused"), "actors", "operate", "released") + + def test_retained_baseline_selection_includes_player_layout_restart(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + for selected in ("source", "restarted", "runtime-restart"): + (root / ("semantic-" + selected + ".json")).write_text("{}") + with patch("native117_actors.capture_actors", return_value={"retain": True}) as capture: + self.assertEqual(({"retain": True}, selected), baseline(root, "actors")) + capture.assert_called_once_with(root, selected) + + def test_inherited_proof_reuses_old_fingerprint_and_rejects_new_data(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + proof = root / "old-proof.json" + source = root / "original-source" + expected = {"level.dat": "old-approved-hash"} + proof.write_text(json.dumps({"source": str(source), "sourceFingerprint": expected})) + with patch("native117_audit_actors.fingerprint", return_value=expected): + result = inherited_sources([proof]) + self.assertEqual(expected, result["receipts"][0]["sources"][0]["fingerprint"]) + with patch("native117_audit_actors.fingerprint", return_value={"level.dat": "fresh-but-not-approved"}): + with self.assertRaisesRegex(AssertionError, "inherited proof"): + inherited_sources([proof]) + + def test_aborted_route_reports_failure_without_reconstructing_unsaved_state(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + runtime = root / "broken" + runtime.mkdir() + source = root / "protected-source" + manifest = root / "archived-inputs.json" + manifest.write_text("{}") + error = {"status": "FAIL", "error": "Unclean shutdown"} + (root / "results.json").write_text(json.dumps({"broken": {"campaign-error": error}})) + (runtime / "native117-source-provenance.json").write_text(json.dumps({ + "source": str(source), "sourceFingerprint": {"approved": "hash"}, + "targetManifest": str(manifest), "targetManifestSha256": "manifest-hash"})) + (runtime / "input-jars.json").write_text("[]") + (runtime / "console-conversion.log").write_text("[Server thread/ERROR] Exception stopping the server") + with patch("native117_audit_actors.fingerprint", return_value={"approved": "hash"}), \ + patch("native117_audit_actors.validate_mod_inventory", return_value={"status": "PASS"}), \ + patch("native117_audit_actors.pinned_profile"), \ + patch("native117_audit_actors.dimensions.capture") as capture: + result = replay(root, "dimensions") + capture.assert_not_called() + self.assertEqual("FAIL", result["status"]) + self.assertEqual(error, result["routes"]["broken"]["recordedCampaignError"]) + self.assertEqual(2, len(result["findings"])) + + def test_persistence_controls_require_every_bounded_save_receipt(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + for kind, counts in (("actors", (2, 3, 2)), ("dimensions", (2, 2, 1))): + for label, count in zip(("conversion", "restart", "runtime-restart"), counts): + (root / ("console-" + label + ".log")).write_text( + "Done (1.0s)!\n" + "Saved the game\n" * count + "Stopping server\n") + result = persistence_controls(root, kind) + self.assertEqual("PASS", result["status"]) + self.assertEqual(45, result["liveRunnerBudgetsSeconds"]["saveReceipt"]) + self.assertIn("not inferred", result["nativeHookListInspection"]) + (root / "console-conversion.log").write_text("Done (1.0s)!\nSaved the game\nStopping server\n") + self.assertEqual("FAIL", persistence_controls(root, "dimensions")["status"]) + + def test_persistence_controls_reject_load_save_npe_even_with_receipts(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + for label, count in zip(("conversion", "restart", "runtime-restart"), (2, 2, 1)): + (root / ("console-" + label + ".log")).write_text( + "Done (1.0s)!\n" + "Saved the game\n" * count + "Stopping server\n") + with (root / "console-conversion.log").open("a") as stream: + stream.write("WorldPersistenceHooks.lambda$handleWorldDataLoad$1\nNullPointerException\n" + "WorldPersistenceHooks.lambda$handleWorldDataSave$0\n") + result = persistence_controls(root, "dimensions") + self.assertEqual("FAIL", result["status"]) + self.assertEqual(3, len(result["boots"]["conversion"]["errors"])) + + def test_persistence_controls_reject_missing_boots(self): + with tempfile.TemporaryDirectory() as temp: + result = persistence_controls(Path(temp), "dimensions") + self.assertEqual("FAIL", result["status"]) + self.assertEqual(3, len(result["boots"])) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_audit_mod_inventory.py b/test-rig/migration/test_native117_audit_mod_inventory.py new file mode 100644 index 00000000..ea09a83e --- /dev/null +++ b/test-rig/migration/test_native117_audit_mod_inventory.py @@ -0,0 +1,213 @@ +"""Exercise the actor auditor with real sealed inventories and worldless oracles.""" + +from contextlib import ExitStack +import copy +import json +from pathlib import Path +import stat +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +from matrix import digest, fingerprint +import native117_audit_actors as audit +import native117_mod_inventory as inventory + + +class AuditModInventoryTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory(prefix="native117 auditor spaces ") + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.run = self.root / "evidence" + self.runtime = self.run / "route" + self.mods = self.runtime / "mods" + self.mods.mkdir(parents=True) + self.source = self.root / "protected source" + self.source.mkdir() + (self.source / "original.txt").write_text("unchanged source", encoding="ascii") + self.manifest = self.root / "sealed manifest.json" + self.ledger = self.runtime / "input-jars.json" + self.rows, jars = [], [] + for identity in sorted(inventory.EXPECTED_IDS): + name = identity + "-1.0.0.117011.jar" + source = self.root / name + source.write_bytes(identity.encode("ascii")) + (self.mods / name).write_bytes(source.read_bytes()) + checksum = digest(source) + self.rows.append(dict(file=name, source=str(source), sha256=checksum)) + jars.append(dict(id=identity, version="1.0.0.117011", path=str(source), sha256=checksum)) + self.write(self.manifest, {"target117": {"jars": jars}}) + self.write(self.ledger, self.rows) + self.provenance = dict(source=str(self.source), sourceFingerprint=fingerprint(self.source), + targetManifest=str(self.manifest), targetManifestSha256=digest(self.manifest)) + self.write_provenance() + self.checks = {key: {"status": "PASS"} for key in ( + "conversion", "adapter", "restart", "idempotence", "runtime", "runtime-restart", + "runtime-ticks", "restart-ticks", "conversion-frozen", "adapter-frozen", "restart-frozen")} + self.checks["backup-stability"] = {"status": "PASS", "backups": {}} + self.native_checks = {prefix + label: {"status": "PASS"} + for prefix in ("readiness-", "saved-entities-") + for label in ("conversion", "restart", "runtime-restart")} + self.checks.update(copy.deepcopy(self.native_checks)) + for prefix in ("semantic-", "dimension-semantic-"): + for label in ("converted", "adapter", "restarted", "runtime", "runtime-restart"): + self.write(self.runtime / (prefix + label + ".json"), {}) + + @staticmethod + def write(path, value): + path.write_text(json.dumps(value), encoding="utf-8") + + def write_provenance(self): + self.write(self.runtime / "native117-source-provenance.json", self.provenance) + + def replay(self, native_difference=False): + kind = "dimensions" if native_difference else "actors" + checks = copy.deepcopy(self.checks) + if native_difference: + for key in ("conversion-frozen", "adapter-frozen", "restart-frozen"): + del checks[key] + checks["conversion-ticks"] = {"status": "PASS"} + keys = ("conversion", "adapter", "restart", "idempotence", "runtime", "runtime-restart") + for key in keys: + checks[key] = {"status": "FAIL"} + platform = dict(assessment={"status": "PASS_WITH_NATIVE_DIFFERENCE"}, + semanticAssessments={key: {"status": "PASS_WITH_NATIVE_DIFFERENCE"} for key in keys}, + proofPath=None, assessorSha256=digest(Path(audit.native117_dimension_assessment.__file__))) + self.write(self.runtime / "native117-platform-assessment.json", platform) + self.write(self.run / "summary.json", {"kind": kind, "routes": ["route"]}) + self.write(self.run / "results.json", {"route": checks}) + with ExitStack() as stack: + replacements = ((audit, "pinned_profile", None), + (audit, "native_controls", self.native_checks), + (audit, "baseline", ({"file": {"Inventory": [], "EnderItems": []}}, None)), + (audit, "expected_state", {}), (audit, "capture_actors", {}), + (audit, "capture_dimensions", {}), (audit, "native_dimension_state", {}), + (audit, "raw", {}), (audit, "require_clean_log", None), + (audit.actors, "backup_state", {}), (audit.actors, "compare", {"status": "PASS"}), + (audit.actors, "tick_result", {"status": "PASS"}), + (audit.actors, "freeze_result", {"status": "PASS"}), + (audit.dimensions, "verdict", {"status": "FAIL"}), + (audit.dimensions, "tick_result", {"status": "PASS"}), + (audit.dimensions, "exact_item_differences", []), + (audit, "assess", {"status": "PASS_WITH_NATIVE_DIFFERENCE"}), + (audit, "summarize_checks", {"status": "PASS_WITH_NATIVE_DIFFERENCE"})) + for owner, name, value in replacements: + stack.enter_context(patch.object(owner, name, return_value=value)) + return audit.replay(self.run, kind), checks + + def reject(self, native_difference=False): + result, checks = self.replay(native_difference) + self.assertEqual("FAIL", result["status"]) + self.assertEqual(checks, result["routes"]["route"]["oracles"]) + self.assertEqual("FAIL", result["routes"]["route"]["modInventory"]["status"]) + self.assertTrue(any("mod inventory" in finding for finding in result["findings"])) + return result + + def test_exact_six_real_inventory_preserves_existing_strict_checks(self): + result, checks = self.replay() + self.assertEqual("PASS", result["status"]) + self.assertEqual(checks, result["routes"]["route"]["oracles"]) + receipt = result["routes"]["route"]["modInventory"] + self.assertEqual("PASS", receipt["status"]) + self.assertEqual(self.provenance["targetManifestSha256"], receipt["manifestSha256"]) + self.assertEqual(6, len(receipt["jars"])) + self.assertFalse(receipt["atomicSnapshot"]) + + def test_extra_jar_is_an_integrity_failure_with_unchanged_raw_oracles(self): + (self.mods / "extra.jar").write_bytes(b"extra") + self.reject() + + def test_missing_physical_jar_is_a_reported_failure_not_a_crash(self): + (self.mods / self.rows[0]["file"]).unlink() + self.reject() + + def test_missing_ledger_row_fails(self): + self.write(self.ledger, self.rows[:-1]) + self.reject() + + def test_nonjar_physical_entry_fails(self): + (self.mods / "unapproved.txt").write_text("not a mod", encoding="ascii") + self.reject() + + def test_aborted_route_still_reports_inventory_failure(self): + self.checks = {"campaign-error": {"status": "FAIL", "error": "incomplete"}} + (self.mods / "extra.jar").write_bytes(b"extra") + result, _ = self.replay() + self.assertEqual("FAIL", result["status"]) + route = result["routes"]["route"] + self.assertEqual("FAIL", route["modInventory"]["status"]) + self.assertEqual("NOT_POSSIBLE_WITH_INCOMPLETE_SAVED_EVIDENCE", route["semanticReplay"]) + + def test_case_changed_ledger_filename_fails(self): + self.rows[0]["file"] = self.rows[0]["file"].upper().replace(".JAR", ".jar") + self.write(self.ledger, self.rows) + self.reject() + + def test_case_colliding_duplicate_ledger_row_fails(self): + self.rows[1] = dict(self.rows[0], file=self.rows[0]["file"].upper().replace(".JAR", ".jar")) + self.write(self.ledger, self.rows) + self.reject() + + def test_altered_disk_bytes_cannot_be_authorized_by_self_declared_ledger_hash(self): + target = self.mods / self.rows[0]["file"] + target.write_bytes(b"altered") + self.rows[0]["sha256"] = digest(target) + self.write(self.ledger, self.rows) + self.reject() + + def test_unchanged_ledger_cannot_hide_altered_disk_bytes(self): + (self.mods / self.rows[0]["file"]).write_bytes(b"altered") + self.reject() + + def test_missing_manifest_sha_is_not_inferred(self): + del self.provenance["targetManifestSha256"] + self.write_provenance() + self.reject() + + def test_incorrect_manifest_sha_fails(self): + self.provenance["targetManifestSha256"] = "0" * 64 + self.write_provenance() + self.reject() + + def test_manifest_bytes_are_bound_to_sealed_provenance_sha(self): + self.manifest.write_bytes(self.manifest.read_bytes() + b"\n") + self.reject() + + def test_ledger_source_must_match_the_sealed_manifest(self): + self.rows[0]["source"] = str(self.root / "elsewhere.jar") + self.write(self.ledger, self.rows) + self.reject() + + def test_reparse_physical_jar_fails_through_real_validator(self): + target = self.mods / self.rows[0]["file"] + original = Path.lstat + + def reparse(path, *args, **kwargs): + metadata = original(path, *args, **kwargs) + if path == target: + fields = {name: getattr(metadata, name) for name in dir(metadata) if name.startswith("st_")} + fields["st_file_attributes"] = getattr(metadata, "st_file_attributes", 0) | stat.FILE_ATTRIBUTE_REPARSE_POINT + return SimpleNamespace(**fields) + return metadata + + with patch.object(Path, "lstat", reparse): + self.reject() + + def test_inventory_failure_is_never_waived_by_native_health_classification(self): + (self.mods / "extra.jar").write_bytes(b"extra") + result = self.reject(native_difference=True) + self.assertEqual("FAIL", result["nativePlatformAssessment"]["status"]) + self.assertTrue(any("mod inventory" in value for value in result["nativePlatformAssessment"]["otherFindings"])) + + def test_inventory_success_does_not_turn_raw_native_health_failure_into_pass(self): + result, checks = self.replay(native_difference=True) + self.assertEqual("FAIL", result["status"]) + self.assertEqual(checks, result["routes"]["route"]["oracles"]) + self.assertEqual("PASS", result["routes"]["route"]["modInventory"]["status"]) + self.assertEqual("PASS_WITH_NATIVE_DIFFERENCE", result["nativePlatformAssessment"]["status"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_broad.py b/test-rig/migration/test_native117_broad.py new file mode 100644 index 00000000..4f4ba6aa --- /dev/null +++ b/test-rig/migration/test_native117_broad.py @@ -0,0 +1,185 @@ +"""Negative controls for the repaired broad drill oracle, not Minecraft passes.""" + +from copy import deepcopy +import json +from pathlib import Path +import tempfile +import unittest + +import nbtlib +import native117_broad as broad + + +class BroadOracleTests(unittest.TestCase): + def setUp(self): + definition = broad.read_json(broad.RIG / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json") + self.stations = [s for s in definition["stations"] if s["id"] in broad.DRILL_BAYS] + + def snapshot(self, station, moved=False): + rows = [] + for case in broad.drill_cases(station): + position = case["rear"] if moved else case["origin"] + marker = json.dumps(case["id"]) + data = ('{id:"' + case["block"] + '",x:' + str(position[0]) + ',y:' + str(position[1]) + + ',z:' + str(position[2]) + ',ForgeData:{AdvBayWitness:' + marker + '},Energy:[I;0],' + + 'Items:[{Slot:0b,id:"minecraft:diamond",Count:7b,tag:{AdvBayStock:' + marker + '}}]}') + row = dict(pos=list(position), block=case["block"], state='facing=' + case["facing"], nbt=data) + rows.append(row) + if not moved: + rows.append(dict(pos=list(case["rear"]), block="basesteam:steam_track" + if station["id"] == "S-05" else "baseelectric:electric_track", state="track")) + if moved: + rows.append(dict(pos=list(case["origin"]), block="basesteam:steam_pipe" + if station["id"] == "S-05" else "baseelectric:electric_conduit", state="pipe")) + return dict(snapshot=rows) + + def states(self, moved=False): + states = {} + for station in self.stations: + states.update(broad.drill_state(station, self.snapshot(station, moved), initial=not moved)) + return states + + def test_all_twelve_initial_then_moved_then_restarted(self): + initial, moved = self.states(), self.states(True) + self.assertEqual(12, broad.operating_state(initial, moved)["moved"]) + self.assertTrue(broad.restarted_state(moved, deepcopy(moved))["exactEnergy"]) + + def test_movement_cannot_replace_initial_placement_test(self): + for station in self.stations: + with self.assertRaisesRegex(AssertionError, "initial placement"): + broad.drill_state(station, self.snapshot(station, True), initial=True) + + def test_no_movement_cannot_clear_the_original_failure(self): + with self.assertRaisesRegex(AssertionError, "actual track movement"): + broad.operating_state(self.states(), self.states()) + + def test_initial_track_is_required(self): + station = self.stations[0] + doc = self.snapshot(station) + doc["snapshot"][1]["block"] = "minecraft:air" + with self.assertRaisesRegex(AssertionError, "initial declared rear track"): + broad.drill_state(station, doc, initial=True) + + def test_live_steam_bit_may_replace_the_conduit_left_in_the_old_cell(self): + station = self.stations[0] + doc = self.snapshot(station, True) + doc["snapshot"][1]["block"] = "basesteam:drillbit" + self.assertTrue(broad.drill_state(station, doc)["S-05:down-facing"]["moved"]) + + def test_missing_duplicate_wrong_facing_and_off_track_fail(self): + station = self.stations[0] + for mutation in ("missing", "duplicate", "facing", "off-track"): + with self.subTest(mutation=mutation): + doc = self.snapshot(station) + if mutation == "missing": + doc["snapshot"].pop(0) + elif mutation == "duplicate": + clone = deepcopy(doc["snapshot"][0]) + clone["pos"] = list(broad.drill_cases(station)[0]["rear"]) + doc["snapshot"].append(clone) + elif mutation == "facing": + doc["snapshot"][0]["state"] = "facing=west" + else: + doc["snapshot"][0]["pos"][0] += 1 + with self.assertRaises(AssertionError): + broad.drill_state(station, doc) + + def test_witness_and_inventory_and_coordinate_changes_fail(self): + station = self.stations[0] + for mutation in ("marker", "lost-stock", "duplicated-stock", "coordinates", "energy"): + with self.subTest(mutation=mutation): + doc = self.snapshot(station) + data = nbtlib.parse_nbt(doc["snapshot"][0]["nbt"]) + if mutation == "marker": + data["ForgeData"]["AdvBayWitness"] = nbtlib.String("replacement") + elif mutation == "lost-stock": + data["Items"][0]["Count"] = nbtlib.Byte(6) + elif mutation == "duplicated-stock": + data["Items"][0]["Count"] = nbtlib.Byte(8) + elif mutation == "coordinates": + data["x"] = nbtlib.Int(999) + else: + data["Energy"] = nbtlib.IntArray([2139095040]) + doc["snapshot"][0]["nbt"] = data.snbt() + with self.assertRaises(AssertionError): + broad.drill_state(station, doc) + + def test_stock_can_export_to_rear_chest_without_loss(self): + station = self.stations[0] + doc = self.snapshot(station, True) + row = doc["snapshot"][0] + data = nbtlib.parse_nbt(row["nbt"]) + stock = data.pop("Items") + row["nbt"] = data.snbt() + doc["snapshot"].append(dict(pos=[-61, 5, 70], block="minecraft:chest", state="chest", + nbt=nbtlib.Compound({"Items": stock}).snbt())) + self.assertEqual(7, broad.drill_state(station, doc)["S-05:down-facing"]["stock"]) + + def test_energy_loss_during_restart_is_not_accepted(self): + before = self.states(True) + before["E-06:up-facing"]["energyBits"] = [1132068864] + before["E-06:up-facing"]["energy"] = 250 + with self.assertRaisesRegex(AssertionError, "Restart changed"): + broad.restarted_state(before, self.states(True)) + + def test_report_cannot_drop_an_assertion_or_station(self): + station = self.stations[0] + record = dict(station=station["id"], minecraft="1.17.1", status="MANUAL_REQUIRED", + assertions=[dict(id=a["id"], status="PASS") for a in station["assertions"]], + manualChecks=station["manualChecks"]) + self.assertEqual(6, broad.assertions_state({station["id"]: record}, [station])["assertions"]["PASS"]) + with self.assertRaisesRegex(AssertionError, "omitted"): + broad.assertions_state({}, [station]) + record["assertions"].pop() + with self.assertRaisesRegex(AssertionError, "assertion set changed"): + broad.assertions_state({station["id"]: record}, [station]) + + def test_decoupled_modset_cannot_silently_contain_base_metals(self): + expected = dict(minecraft="1.17.1", forge="37.1.1", basepower="2.4.6.117011") + broad.loaded_mods({"S-05": dict(mods=expected)}, expected) + actual = dict(expected, basemetals="3.0.1.117011") + with self.assertRaisesRegex(AssertionError, "loaded mod identities"): + broad.loaded_mods({"S-05": dict(mods=actual)}, expected) + + def test_live_broad_staging_explicitly_disables_frozen_migration_mode(self): + with tempfile.TemporaryDirectory() as directory: + runtime = Path(directory) + (runtime / "advantage-migration.allow").touch() + broad.authorize_broad(runtime) + self.assertTrue((runtime / "advantage-works.allow").is_file()) + self.assertTrue((runtime / "migration-operate.allow").is_file()) + + def test_works_world_retains_original_no_structure_and_seed_settings(self): + with tempfile.TemporaryDirectory() as directory: + runtime = Path(directory) + properties = runtime / "server.properties" + properties.write_text("level-name=AdvantageWorksMigration\nserver-port=25584\ngenerate-structures=true\n") + broad.configure_broad(runtime, 25588) + result = dict(line.split("=", 1) for line in properties.read_text().splitlines()) + self.assertEqual("false", result["generate-structures"]) + self.assertEqual("advantage-works-1.17.1-v1", result["level-seed"]) + self.assertEqual("25588", result["server-port"]) + self.assertEqual("0", result["difficulty"]) + self.assertEqual("true", result["enable-command-block"]) + self.assertEqual("false", result["spawn-npcs"]) + self.assertEqual("AdvantageWorksMigration", result["level-name"]) + + def test_required_integration_skip_is_explained_by_loaded_mods(self): + station = dict(id="W-03", assertions=[dict(id="mineralogy", requiredMods=["mineralogy"])]) + record = dict(station="W-03", minecraft="1.17.1", status="MANUAL_REQUIRED", mods={}, manualChecks=["GUI"], + assertions=[dict(id="mineralogy", status="SKIP")]) + self.assertEqual({"SKIP": 1}, broad.assertions_state({"W-03": record}, [station])["assertions"]) + record["mods"]["mineralogy"] = "6.0.1.117011" + with self.assertRaisesRegex(AssertionError, "unexplained skip"): + broad.assertions_state({"W-03": record}, [station]) + + def test_duplicate_assertions_cannot_be_counted_as_coverage(self): + station = dict(id="W-03", assertions=[dict(id="one")]) + record = dict(station="W-03", minecraft="1.17.1", status="MANUAL_REQUIRED", manualChecks=["GUI"], + assertions=[dict(id="one", status="PASS"), dict(id="one", status="PASS")]) + with self.assertRaisesRegex(AssertionError, "assertion set changed"): + broad.assertions_state({"W-03": record}, [station]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_broad_audit.py b/test-rig/migration/test_native117_broad_audit.py new file mode 100644 index 00000000..762f31f5 --- /dev/null +++ b/test-rig/migration/test_native117_broad_audit.py @@ -0,0 +1,130 @@ +"""Native 1.17 offline audit controls; these tests never start Minecraft.""" + +from copy import deepcopy +import json +import unittest +import zipfile + +import native117_broad_result_audit as audit +import native117_broad_audit_fixtures as fixtures + + +class Native117AuditRegistrationTests(unittest.TestCase): + def test_native117_engine_has_an_explicit_adapter(self): + self.assertEqual(("1.17.1", "37.1.1"), audit.VERSIONS.get("117")) + + +class Native117StrictAuditTests(unittest.TestCase): + def setUp(self): + self.fixture = fixtures.BroadResultAuditTests("test_complete_both_profiles_accepts_bounded_proof_not_manual_pass") + self.fixture.setUp() + self.addCleanup(self.fixture.doCleanups) + + def result(self): + return audit.audit(self.fixture.runtime, "117") + + def rejected(self, message): + result = self.result() + self.assertEqual("FAIL", result["status"], result) + self.assertIn(message, " ".join(result["errors"])) + + def repin(self): + fixtures.write(self.fixture.manifest, {"target117": self.fixture.pins}) + self.fixture.report["targetManifestSha256"] = audit.sha(self.fixture.manifest.read_bytes()) + self.fixture.save_top() + + def test_complete_native117_fixture_requires_both_profiles_and_all_six_logs(self): + result = self.result() + self.assertEqual("PASS", result["status"], result["errors"]) + for profile in result["profiles"].values(): + self.assertEqual({"first", "restarted"}, set(profile["native117LogArchives"])) + self.assertEqual("NOT_AUTOMATED_BY_BROAD_SWEEP", profile["manualProcedures"]) + self.assertEqual(21, len(profile["checks"]["restarted"]["manualRequired"])) + + def test_missing_debug_cannot_hide_behind_clean_console(self): + (self.fixture.profile() / "debug-restarted.log").unlink() + self.rejected("debug-restarted.log") + + def test_missing_latest_cannot_hide_behind_clean_console(self): + (self.fixture.profile() / "latest-first.log").unlink() + self.rejected("latest-first.log") + + def test_debug_exception_cannot_hide_behind_clean_console(self): + path = self.fixture.profile() / "debug-restarted.log" + path.write_bytes(path.read_bytes() + b"java.lang.IllegalStateException: lost state\n") + self.rejected("native117 runtime error") + + def test_qualified_java_lang_error_is_still_a_runtime_failure(self): + path = self.fixture.profile() / "debug-restarted.log" + path.write_bytes(path.read_bytes() + b"java.lang.Error: failed\n") + self.rejected("native117 runtime error") + + def test_plain_debug_word_error_is_not_an_exception_type(self): + path = self.fixture.profile() / "debug-first.log" + path.write_bytes(path.read_bytes() + b"[main/DEBUG] [mixin/]: Error cleaning class output directory: .mixin.out\n") + result = self.result() + self.assertEqual("PASS", result["status"], result["errors"]) + + def test_latest_fatal_cannot_hide_behind_clean_console(self): + path = self.fixture.profile() / "latest-first.log" + path.write_bytes(path.read_bytes() + b"[Server thread/FATAL] failure\n") + self.rejected("fatal native runtime error") + + def test_debug_must_contain_the_actual_station_activity(self): + path = self.fixture.profile() / "debug-first.log" + path.write_bytes(path.read_bytes().replace(b"Started W-04 -", b"not started W-04 -")) + self.rejected("station activity") + + def test_debug_restart_cannot_refill_or_rebuild(self): + path = self.fixture.profile() / "debug-restarted.log" + path.write_bytes(path.read_bytes() + b"Modified block data of 1, 2, 3\n") + self.rejected("activity after cold restart") + + def test_debug_requires_native_mcp_and_all_dimension_completion(self): + path = self.fixture.profile() / "debug-restarted.log" + path.write_bytes(path.read_bytes().replace(b"with MCP 20210706.113038", b"unknown mapping identity")) + self.rejected("MCP identity") + + def test_latest_requires_completed_all_dimension_shutdown(self): + path = self.fixture.profile() / "latest-restarted.log" + path.write_bytes(path.read_bytes().replace(b"ThreadedAnvilChunkStorage: All dimensions are saved", b"incomplete")) + self.rejected("all-dimension shutdown") + + def test_native117_cannot_omit_the_engine_inventory(self): + self.fixture.pins.pop("serverCode") + self.repin() + self.rejected("sealed native117 engine inventory") + + def test_native117_cannot_substitute_a_root_legacy_launcher(self): + self.fixture.pins["launcher"] = "forge.jar" + (self.fixture.profile() / "forge.jar").write_bytes(b"engine") + self.repin() + self.rejected("modular Forge launcher") + + def test_native117_inventory_requires_all_bootstrap_code(self): + key = sorted(audit.NATIVE117_REQUIRED_CODE)[0] + self.fixture.pins["serverCode"]["files"].pop(key) + self.repin() + self.rejected("omits required code") + + def test_native117_version_cannot_be_a_legacy_mod(self): + self.fixture.pins["jars"][0]["version"] = "1.0.0.116051" + self.repin() + self.rejected("expected mod identities changed") + + def test_native117_staged_metadata_must_match_the_pinned_identity(self): + pin = next(row for row in self.fixture.pins["jars"] if row["id"] == "basepower") + staged = self.fixture.profile() / "mods" / (pin["id"] + ".jar") + with zipfile.ZipFile(staged, "w") as archive: + archive.writestr("META-INF/mods.toml", '[[mods]]\nmodId="notpower"\nversion="1.0.0.117011"\n') + pin["sha256"] = audit.sha(staged.read_bytes()) + receipt = self.fixture.profile() / "input-jars.json" + rows = fixtures.load(receipt) + next(row for row in rows if row["file"] == "basepower.jar")["sha256"] = pin["sha256"] + fixtures.write(receipt, rows) + self.repin() + self.rejected("native117 staged mod identity") + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_campaign.py b/test-rig/migration/test_native117_campaign.py new file mode 100644 index 00000000..9c350a68 --- /dev/null +++ b/test-rig/migration/test_native117_campaign.py @@ -0,0 +1,414 @@ +"""Source approval requires a recorded stopped-runtime receipt, never a fresh hash.""" + +from argparse import Namespace +from pathlib import Path +import copy +import json +import tempfile +import unittest +from unittest.mock import patch + +from matrix import WORLD, digest, fingerprint, write_json +from native117 import RIG +from native117_campaign import ACCEPTED_POWER114, REQUIRED, generate114, require_source114_receipt, source_row +import native117_campaign as campaign +from native117_sources import SOURCE115 +from native117_sources116 import ACCEPTED_POWER116, SOURCE116 +import test_native117_sources as source_tests + + +class SourceReceiptTests(unittest.TestCase): + def setUp(self): + root = RIG / "build/port-evidence" + root.mkdir(parents=True, exist_ok=True) + self.temporary = tempfile.TemporaryDirectory(dir=root) + self.root = Path(self.temporary.name) + + def tearDown(self): + self.temporary.cleanup() + + def receipt(self): + source = self.root / "disposable114" + (source / WORLD).mkdir(parents=True) + (source / WORLD / "level.dat").write_bytes(b"immutable stopped source witness") + manifest = self.root / "accepted114.json" + write_json(manifest, {}) + receipt_path = self.root / "receipt.json" + receipt = dict(status="PASS", acceptedPower114=ACCEPTED_POWER114, sourceRuntime=str(source.resolve()), + runtimeFingerprint=fingerprint(source), worldFingerprint=fingerprint(source / WORLD), + target114Manifest=str(manifest), target114ManifestSha256=digest(manifest), blocks=355, tiles=332) + write_json(receipt_path, receipt) + row = dict(id="native114/full", kind="frozen", source=str(source), source114Receipt=str(receipt_path), + source114ReceiptSha256=digest(receipt_path), sourceStatus="GENERATED_DISPOSABLE_114_VERIFIED") + return source, receipt_path, row + + def test_recorded_whole_runtime_and_world_fingerprints_are_both_verified(self): + source, _, row = self.receipt() + with patch("native117_campaign.native114.pinned_profile"): + require_source114_receipt(row) + (source / WORLD / "level.dat").write_bytes(b"changed") + with self.assertRaisesRegex(AssertionError, "fingerprint changed"): + require_source114_receipt(row) + + def test_counts_cannot_be_downgraded_even_with_a_new_receipt_digest(self): + source, receipt_path, row = self.receipt() + import json + receipt = json.loads(receipt_path.read_text()) + receipt["tiles"] = 331 + write_json(receipt_path, receipt) + row["source114ReceiptSha256"] = digest(receipt_path) + with patch("native117_campaign.native114.pinned_profile"), self.assertRaisesRegex(AssertionError, "355-block/332-tile"): + require_source114_receipt(row) + + def test_generated_source_metadata_is_disjoint_from_the_stopped_runtime(self): + bundle = self.root / "generated" + source = bundle / "native114-full/runtime" + (source / WORLD).mkdir(parents=True) + (source / WORLD / "level.dat").write_bytes(b"stopped world") + write_json(source / "migration-restarted.json", []) + write_json(source / "migration-fixture.json", {}) + manifest = self.root / "accepted114.json" + write_json(manifest, {}) + before = fingerprint(source) + with patch("native117_campaign.native117.pinned_profile"): + with self.assertRaisesRegex(ValueError, "overlaps a protected input"): + source_row(bundle, "frozen", "native114/full", source, manifest, "target114", "migration-restarted.json") + row = source_row(bundle / "source-records", "frozen", "native114/full", source, manifest, + "target114", "migration-restarted.json") + self.assertEqual(fingerprint(source), before) + self.assertTrue(Path(row["sourceHashes"]).is_relative_to(bundle / "source-records")) + self.assertEqual(digest(Path(row["sourceHashes"])), row["sourceHashesSha256"]) + + def test_source_generation_does_not_launch_an_incomplete_comprehensive_fixture(self): + source = self.root / "approved13" + source.mkdir() + write_json(source / "migration-restarted.json", []) + manifest, hashes = self.root / "pins.json", self.root / "hashes.json" + write_json(manifest, {}) + write_json(hashes, {}) + recipe = dict(source=str(source), sourceManifest=str(manifest), sourceManifestSha256=digest(manifest), + sourceHashes=str(hashes), sourceHashesSha256=digest(hashes), sourceKey="target113", + sourceSnapshot="migration-restarted.json", sourceSnapshotSha256=digest(source / "migration-restarted.json")) + rows = [] + for kind, names in REQUIRED.items(): + for name in sorted(names): + row = dict(kind=kind, id=name) + if name.startswith("native114/"): + row.update(native114SourceRecipe=recipe, source114Manifest=str(manifest), + source114ManifestSha256=digest(manifest), sourceStatus="REQUIRES_DISPOSABLE_114_GENERATION") + rows.append(row) + # Put the comprehensive source first; no process is needed to reject it. + rows.sort(key=lambda row: row["id"] != "native114/full") + plan = self.root / "routes.json" + write_json(plan, dict(format=1, acceptedPower114=ACCEPTED_POWER114, routes=rows)) + settings = Namespace(routes=plan, java8=self.root / "java.exe", output=self.root / "new", port=25595) + before = fingerprint(source) + with patch("native117_campaign.native114.pinned_profile", return_value=(self.root / "seed", "launcher", [])), \ + patch("native117_campaign.native114.run") as launch: + with self.assertRaisesRegex(AssertionError, "355-block/332-tile"): + generate114(settings) + launch.assert_not_called() + self.assertEqual(fingerprint(source), before) + self.assertTrue((settings.output / "native114-full/source114-receipt.json").is_file()) + + +class Generate115CampaignTests(unittest.TestCase): + def setUp(self): + self.fixture = source_tests.Native115SourceGeneratorTests() + self.fixture.setUp() + self.addCleanup(self.fixture.doCleanups) + self.args = self.fixture.args + recipes = {(row['kind'], row['id']): row for row in self.fixture.rows} + rows = [] + for kind, identities in REQUIRED.items(): + for identity in sorted(identities - SOURCE115[kind] - SOURCE116[kind]): + rows.append(copy.deepcopy(recipes.get((kind, identity), dict(id=identity, kind=kind)))) + self.plan_document = dict(format=1, acceptedPower114=ACCEPTED_POWER114, routes=rows) + write_json(self.fixture.routes, self.plan_document) + + def fake_protocol(self, settings, kind): + runtime = settings.output if kind == 'frozen' else settings.output / 'target115' + self.fixture.frozen_result(runtime) + if kind == 'connected': + write_json(runtime / 'migration-restarted-worked.json', []) + return 0, self.fixture.stops() + + def fake_result(self, kind, settings, stops): + runtime = settings.output if kind == 'frozen' else settings.output / 'target115' + proof = dict(stoppedCleanly=True, blocks=355, tiles=332, preservationStatus='PASS', + requestedTicks=[512, 512], advancedTicks=[512, 512]) + return runtime, 'migration-restarted.json' if kind == 'frozen' else 'restarted-worked', settings.source_key, proof + + def test_validate_only_does_not_create_any_output_or_launch(self): + self.args.validate_only = True + with self.fixture.pinned(), patch.object(campaign, 'run_source115') as launch: + self.assertEqual(0, campaign.generate115(self.args)) + launch.assert_not_called() + self.assertFalse(self.args.output.exists()) + + def test_six_receipts_and_thirty_six_routes_keep_canonical_inputs_unchanged(self): + original = fingerprint(self.fixture.root / 'protected fixtures') + with self.fixture.pinned(), patch.object(campaign, 'run_source115', side_effect=self.fake_protocol) as run, \ + patch.object(campaign, 'source115_result', side_effect=self.fake_result): + self.assertEqual(0, campaign.generate115(self.args)) + self.assertEqual(6, run.call_count) + for call in run.call_args_list: + self.assertEqual((25575, 512), (call.args[0].port, call.args[0].ticks)) + self.assertTrue(call.args[0].output.is_relative_to(self.args.output)) + bundle = json.loads((self.args.output / 'source115-receipts.json').read_text()) + document = json.loads((self.args.output / 'native117-upgrade-routes.json').read_text()) + self.assertEqual(('PASS', 6), (bundle['status'], len(bundle['receipts']))) + campaign.require_routes(document) + self.assertEqual(36, len(document['routes'])) + for row in document['routes']: + campaign.require_source115_receipt(row) + self.assertEqual('UNQUALIFIED', document['qualificationStatus']) + self.assertEqual(original, fingerprint(self.fixture.root / 'protected fixtures')) + self.assertEqual(self.plan_document, json.loads(self.fixture.routes.read_text())) + + def test_failed_second_source_keeps_first_receipt_and_partial_failure(self): + calls = [] + def protocol(settings, kind): + calls.append(settings) + if len(calls) == 2: + raise RuntimeError('retained server failure') + return self.fake_protocol(settings, kind) + with self.fixture.pinned(), patch.object(campaign, 'run_source115', side_effect=protocol), \ + patch.object(campaign, 'source115_result', side_effect=self.fake_result): + with self.assertRaisesRegex(RuntimeError, 'retained server failure'): + campaign.generate115(self.args) + bundle = json.loads((self.args.output / 'source115-receipts.json').read_text()) + self.assertEqual(('FAIL', 1), (bundle['status'], len(bundle['receipts']))) + first = json.loads(Path(bundle['receipts'][0]['receipt']).read_text()) + self.assertEqual('PASS', first['status']) + failed = json.loads((calls[1].output.parent / 'source115-receipt.json').read_text()) + self.assertEqual(('FAIL', False), (failed['status'], failed['stoppedCleanly'])) + with self.fixture.pinned(), self.assertRaises(FileExistsError): + campaign.generate115(self.args) + + def test_parent_canonical_thirty_routes_are_retained_without_replacing_114(self): + canonical = copy.deepcopy(self.plan_document) + canonical['routes'].extend(campaign.pending_routes()) + canonical['routes'].extend(campaign.pending116_routes()) + for row in canonical['routes']: + if row['id'].startswith('native114/'): + row['sourceStatus'] = 'REQUIRES_DISPOSABLE_114_GENERATION' + self.args.routes = self.fixture.root / 'parent canonical routes.json' + write_json(self.args.routes, canonical) + before = digest(self.args.routes) + with self.fixture.pinned(), patch.object(campaign, 'run_source115', side_effect=self.fake_protocol), \ + patch.object(campaign, 'source115_result', side_effect=self.fake_result): + self.assertEqual(0, campaign.generate115(self.args)) + actual = json.loads((self.args.output / 'native117-upgrade-routes.json').read_text()) + self.assertEqual(before, digest(self.args.routes)) + self.assertEqual([row for row in canonical['routes'] if row['id'].startswith('native114/')], + [row for row in actual['routes'] if row['id'].startswith('native114/')]) + + def test_canonical_recipe_change_is_not_a_new_source_approval(self): + canonical = copy.deepcopy(self.plan_document) + canonical['routes'].extend(campaign.pending_routes()) + canonical['routes'].extend(campaign.pending116_routes()) + next(row for row in canonical['routes'] if row['id'] == 'legacy/original110-chained-113')['sourceHashesSha256'] = '0' * 64 + self.args.routes = self.fixture.root / 'changed routes.json' + write_json(self.args.routes, canonical) + with self.fixture.pinned(), patch.object(campaign, 'run_source115') as launch: + with self.assertRaisesRegex(AssertionError, 'sealed F source lineage'): + campaign.generate115(self.args) + launch.assert_not_called() + + +class Source116CampaignTests(unittest.TestCase): + def setUp(self): + root = RIG / "build/native117-campaign116-unit" + root.mkdir(parents=True, exist_ok=True) + self.temporary = tempfile.TemporaryDirectory(dir=root) + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.routes = self.root / "approved thirty.json" + self.document = dict(format=1, acceptedPower114=ACCEPTED_POWER114, + acceptedPower115=campaign.ACCEPTED_POWER115, qualificationStatus="UNQUALIFIED", + source115Receipts="unchanged historical bundle", source115ReceiptsSha256="A" * 64, routes=[]) + for kind, identities in REQUIRED.items(): + for identity in sorted(identities - SOURCE116[kind]): + self.document["routes"].append(dict(id=identity, kind=kind, + source=str(self.root / "protected" / identity), sourceManifest=str(self.root / "historical pins"), + sourceHashes=str(self.root / "historical hashes"), sourceStatus="RETAINED_UNIT_RECEIPT", + historicalReceiptWitness="byte-for-byte " + kind + ":" + identity)) + write_json(self.routes, self.document) + self.output = self.root / "new upgraded plan" + self.args = Namespace(routes=self.routes, output=self.output, validate_only=False) + self.no_process = patch("subprocess.Popen", side_effect=AssertionError("No Minecraft process in campaign sidecar tests")) + self.no_process.start() + self.addCleanup(self.no_process.stop) + + def guards(self): + from contextlib import ExitStack + stack = ExitStack() + stack.enter_context(patch.object(campaign, "require_source114_receipt")) + stack.enter_context(patch.object(campaign, "require_source115_receipt")) + return stack + + def upgrade(self): + with self.guards(): + self.assertEqual(0, campaign.upgrade116(self.args)) + return self.output / "native117-upgrade-routes.json" + + def attachment(self): + plan = self.upgrade() + sources_path = self.root / "generated sources/source116-routes.json" + bundle_path = sources_path.with_name("source116-receipts.json") + rows, receipts = [], [] + for kind, identities in SOURCE116.items(): + for identity in sorted(identities): + path = self.root / "generated sources" / identity / "source116-receipt.json" + write_json(path, dict(status="PASS", scope="UNIT_ONLY_NEVER_RUNTIME")) + checksum = digest(path) + rows.append(dict(id=identity, kind=kind, source=str(path.parent / "runtime"), + sourceManifest=str(self.root / "pins116.json"), sourceHashes=str(path.parent / "world-hashes.json"), + sourceStatus="GENERATED_DISPOSABLE_116_VERIFIED", source116Receipt=str(path), source116ReceiptSha256=checksum)) + receipts.append(dict(id=identity, receipt=str(path), sha256=checksum)) + generated = dict(format=1, acceptedPower116=ACCEPTED_POWER116, qualificationStatus="UNQUALIFIED", routes=rows) + bundle = dict(status="PASS", scope=campaign.SOURCE116_SCOPE, acceptedPower116=ACCEPTED_POWER116, + canonicalSourcesUnchanged=True, seedCodeUnchanged=True, receipts=receipts) + write_json(sources_path, generated) + write_json(bundle_path, bundle) + args = Namespace(routes=plan, sources=sources_path, receipts=bundle_path, output=self.root / "attached plan") + return args, generated, bundle + + def test_required_route_set_is_now_exactly_sixteen_frozen_twenty_connected(self): + self.assertEqual((16, 20, 36), (len(REQUIRED["frozen"]), len(REQUIRED["connected"]), sum(map(len, REQUIRED.values())))) + + def test_old_thirty_routes_cannot_run_without_explicit_upgrade(self): + with self.assertRaisesRegex(AssertionError, "36"): + campaign.require_routes(self.document) + + def test_every_new_source_is_required_and_duplicates_do_not_substitute(self): + document = copy.deepcopy(self.document) + document["routes"].extend(campaign.pending116_routes()) + campaign.require_routes(document) + for identity in SOURCE116["frozen"] | SOURCE116["connected"]: + for duplicate in (False, True): + changed = copy.deepcopy(document) + changed["routes"] = [row for row in changed["routes"] if row["id"] != identity] + if duplicate: + changed["routes"].append(copy.deepcopy(changed["routes"][0])) + with self.subTest(identity=identity, duplicate=duplicate), self.assertRaises(AssertionError): + campaign.require_routes(changed) + + def test_upgrade_retains_all_thirty_rows_receipts_and_input_hash(self): + before = digest(self.routes) + path = self.upgrade() + result = json.loads(path.read_text()) + self.assertEqual(before, digest(self.routes)) + self.assertEqual(self.document["routes"], [row for row in result["routes"] if not row["id"].startswith("native116/")]) + self.assertEqual(self.document["source115ReceiptsSha256"], result["source115ReceiptsSha256"]) + self.assertEqual(("UNQUALIFIED", ACCEPTED_POWER116, 36), + (result["qualificationStatus"], result["acceptedPower116"], len(result["routes"]))) + self.assertTrue(all(row.get("sourceStatus") == "REQUIRES_APPROVED_DISPOSABLE_116_SOURCE" + for row in result["routes"] if row["id"].startswith("native116/"))) + + def test_upgrade_never_regenerates_114_or_115_sources(self): + with self.guards(), patch.object(campaign, "generate114", side_effect=AssertionError("no regeneration")), \ + patch.object(campaign, "generate115", side_effect=AssertionError("no regeneration")): + self.assertEqual(0, campaign.upgrade116(self.args)) + + def test_upgrade_refuses_missing_duplicate_or_already_upgraded_rows(self): + for rows in (self.document["routes"][:-1], self.document["routes"] + [self.document["routes"][0]], + self.document["routes"] + campaign.pending116_routes()): + write_json(self.routes, dict(self.document, routes=rows)) + with self.guards(), self.assertRaisesRegex(AssertionError, "30 historical"): + campaign.upgrade116(self.args) + self.assertFalse(self.output.exists()) + + def test_upgrade_requires_old_source_receipts_not_destination_pass(self): + with patch.object(campaign, "require_source114_receipt", side_effect=AssertionError("unapproved native114")), self.assertRaisesRegex(AssertionError, "unapproved native114"): + campaign.upgrade116(self.args) + self.assertFalse(self.output.exists()) + + def test_upgrade_does_not_replace_output_and_validate_only_is_read_only(self): + self.args.validate_only = True + with self.guards(): + self.assertEqual(0, campaign.upgrade116(self.args)) + self.assertFalse(self.output.exists()) + self.args.validate_only = False + self.upgrade() + with self.guards(), self.assertRaises(FileExistsError): + campaign.upgrade116(self.args) + + def test_attach_requires_each_of_six_raw_receipt_guards(self): + args, generated, _ = self.attachment() + before = digest(args.routes) + with self.guards(), patch.object(campaign, "require_source116_receipt") as guard: + self.assertEqual(0, campaign.attach116(args)) + self.assertEqual(generated["routes"], [call.args[0] for call in guard.call_args_list]) + result = json.loads((args.output / "native117-upgrade-routes.json").read_text()) + self.assertEqual(before, digest(args.routes)) + self.assertEqual(self.document["routes"], [row for row in result["routes"] if not row["id"].startswith("native116/")]) + campaign.require_routes(result) + self.assertEqual("UNQUALIFIED", result["qualificationStatus"]) + + def test_attach_rejects_prior_destination_pass_bundle(self): + args, _, bundle = self.attachment() + for key, value in (("scope", "DESTINATION_PASS"), ("status", "UNQUALIFIED"), ("acceptedPower116", "0" * 40), + ("canonicalSourcesUnchanged", False), ("seedCodeUnchanged", False)): + write_json(args.receipts, dict(bundle, **{key: value})) + with self.subTest(key=key), self.guards(), patch.object(campaign, "require_source116_receipt") as guard, self.assertRaisesRegex(AssertionError, "stopped source116"): + campaign.attach116(args) + guard.assert_not_called() + self.assertFalse(args.output.exists()) + + def test_attach_rejects_missing_duplicate_or_mixed_receipts(self): + args, generated, bundle = self.attachment() + for receipts in (bundle["receipts"][:-1], bundle["receipts"] + [bundle["receipts"][0]]): + write_json(args.receipts, dict(bundle, receipts=receipts)) + with self.guards(), self.assertRaisesRegex(AssertionError, "Exactly six"): + campaign.attach116(args) + write_json(args.receipts, bundle) + changed = copy.deepcopy(generated) + changed["routes"][-1]["source116ReceiptSha256"] = "0" * 64 + write_json(args.sources, changed) + with self.guards(), patch.object(campaign, "require_source116_receipt"), self.assertRaisesRegex(AssertionError, "bundle and source route"): + campaign.attach116(args) + + def test_attach_does_not_hide_failed_raw_evidence_guard(self): + args, _, _ = self.attachment() + with self.guards(), patch.object(campaign, "require_source116_receipt", side_effect=AssertionError("lost tile")), self.assertRaisesRegex(AssertionError, "lost tile"): + campaign.attach116(args) + self.assertFalse(args.output.exists()) + + def test_attach_refuses_replacement_of_existing_receipts(self): + args, _, _ = self.attachment() + doc = json.loads(args.routes.read_text()) + next(row for row in doc["routes"] if row["id"].startswith("native116/"))["sourceStatus"] = "GENERATED_DISPOSABLE_116_VERIFIED" + write_json(args.routes, doc) + with self.guards(), self.assertRaisesRegex(AssertionError, "replace existing"): + campaign.attach116(args) + + def test_target_cannot_start_before_native116_source_guard(self): + plan = self.upgrade() + target = self.root / "target117.json" + write_json(target, {}) + doc = json.loads(plan.read_text()) + selected = next(row for row in doc["routes"] if row["id"] == "native116/full") + args = Namespace(routes=plan, target_manifest=target, output=self.root / "target runtime", + kind="frozen", ticks=512, java8=self.root / "java16.exe", port=25595) + with self.guards(), patch.object(campaign.native117, "pinned_profile", return_value=(self.root / "seed", "native", [])), \ + patch.object(campaign, "select_shard", return_value=[selected]), \ + patch.object(campaign.native117, "run") as frozen, patch.object(campaign.native117_connected, "run") as connected_run: + with self.assertRaisesRegex(AssertionError, "stopped-source receipt"): + campaign.run(args) + frozen.assert_not_called() + connected_run.assert_not_called() + self.assertFalse(args.output.exists()) + + def test_cli_routes_upgrade_and_attach_without_starting_a_server(self): + with patch.object(campaign, "upgrade116", return_value=0) as upgrade: + self.assertEqual(0, campaign.main(["upgrade116", "--routes", str(self.routes), "--output", str(self.output), "--validate-only"])) + self.assertTrue(upgrade.call_args.args[0].validate_only) + with patch.object(campaign, "attach116", return_value=0) as attach: + self.assertEqual(0, campaign.main(["attach116", "--routes", str(self.routes), "--receipts", "bundle.json", "--output", str(self.output)])) + self.assertEqual(Path("bundle.json"), attach.call_args.args[0].receipts) + + +if __name__ == '__main__': + unittest.main() diff --git a/test-rig/migration/test_native117_campaign_interval.py b/test-rig/migration/test_native117_campaign_interval.py new file mode 100644 index 00000000..6319c35f --- /dev/null +++ b/test-rig/migration/test_native117_campaign_interval.py @@ -0,0 +1,19 @@ +from argparse import Namespace +import unittest +from native117_campaign import observation_ticks + + +class ConnectedObservationIntervalTests(unittest.TestCase): + def test_historical_default_remains_512(self): + self.assertEqual(512, observation_ticks(Namespace())) + + def test_fixed_longer_interval_is_retained(self): + for ticks in (1, 1024, 2048, 4000): + self.assertEqual(ticks, observation_ticks(Namespace(ticks=ticks))) + + def test_unsafe_or_noninteger_intervals_are_rejected(self): + for value in (0, -1, 4001, True, 2048.0, "2048"): + with self.assertRaises(ValueError): observation_ticks(Namespace(ticks=value)) + + +if __name__ == "__main__": unittest.main() diff --git a/test-rig/migration/test_native117_campaign_shards.py b/test-rig/migration/test_native117_campaign_shards.py new file mode 100644 index 00000000..7d90a6f5 --- /dev/null +++ b/test-rig/migration/test_native117_campaign_shards.py @@ -0,0 +1,47 @@ +import copy +import unittest +from native117_campaign import REQUIRED, aggregate_shards, select_shard + + +def shards(): + names = sorted(REQUIRED["connected"]) + return [dict(kind="connected", routesManifestSha256="source", targetManifestSha256="candidate", + observationTicks=2048, shard=dict(index=index, count=3), status="PASS_SHARD", protocolUnchanged=True, + selectedRouteIds=names[index::3], routes=[dict(id=name, status="PASS") for name in names[index::3]]) + for index in range(3)] + + +class CampaignShardTests(unittest.TestCase): + def test_shards_partition_every_route_exactly_once(self): + names = sorted(REQUIRED["connected"]) + actual = [name for index in range(3) for name in select_shard(names, index, 3)] + self.assertCountEqual(names, actual) + self.assertEqual(20, len(aggregate_shards(shards())["routes"])) + + def test_missing_duplicate_or_failed_shard_cannot_pass(self): + good = shards() + for bad in (good[:2], [good[0], good[0], good[2]]): + with self.assertRaises(AssertionError): aggregate_shards(bad) + bad = copy.deepcopy(good); bad[1]["status"] = "FAIL" + with self.assertRaises(AssertionError): aggregate_shards(bad) + + def test_mixed_candidate_or_interval_cannot_pass(self): + for key, value in (("targetManifestSha256", "other"), ("observationTicks", 512)): + bad = shards(); bad[1][key] = value + with self.assertRaises(AssertionError): aggregate_shards(bad) + + def test_missing_duplicate_or_failed_route_cannot_pass(self): + for mode in ("missing", "duplicate", "failed"): + bad = shards() + if mode == "missing": bad[0]["routes"].pop() + elif mode == "duplicate": bad[0]["routes"][1] = dict(bad[0]["routes"][0]) + else: bad[0]["routes"][0]["status"] = "FAIL" + with self.assertRaises(AssertionError): aggregate_shards(bad) + + def test_invalid_and_empty_shards_reject_before_launch(self): + for index, count in ((-1, 3), (3, 3), (0, 0), (0, 16)): + with self.assertRaises(ValueError): select_shard(["one"], index, count) + with self.assertRaises(ValueError): select_shard([], 0, 1) + + +if __name__ == "__main__": unittest.main() diff --git a/test-rig/migration/test_native117_code.py b/test-rig/migration/test_native117_code.py new file mode 100644 index 00000000..f08102f8 --- /dev/null +++ b/test-rig/migration/test_native117_code.py @@ -0,0 +1,63 @@ +"""Server-code drift is rejected before process creation, without opening worlds.""" + +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch +from native117_code import INVENTORY, Server, code_files, verify_code + + +class Native117CodeCustodyTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.root = Path(self.temp.name) + (self.root / "libraries/native").mkdir(parents=True) + (self.root / "libraries/native/server.jar").write_bytes(b"native") + (self.root / "forge.jar").write_bytes(b"forge") + self.expected = {"files": code_files(self.root)} + (self.root / INVENTORY).write_text(json.dumps(self.expected), encoding="utf-8") + + def tearDown(self): + self.temp.cleanup() + + def test_exact_seal_accepts_world_and_config_changes_outside_code(self): + (self.root / "world").mkdir() + (self.root / "world/level.dat").write_bytes(b"mutable world") + (self.root / "server.properties").write_text("mutable configuration") + self.assertEqual(self.expected, verify_code(self.root)) + + def test_changed_missing_extra_and_timestamp_only_archive_bytes_are_rejected(self): + original = self.root / "libraries/native/server.jar" + for mutation in ("changed", "missing", "extra", "zip-timestamp-only"): + original.write_bytes(b"native") + extra = self.root / "libraries/extra.jar" + if extra.exists(): + extra.unlink() + if mutation in ("changed", "zip-timestamp-only"): + original.write_bytes(b"nativE") + elif mutation == "missing": + original.unlink() + else: + extra.write_bytes(b"extra") + with self.subTest(mutation=mutation), self.assertRaises(AssertionError): + verify_code(self.root) + + def test_code_failure_precedes_external_process_creation(self): + (self.root / "forge.jar").write_bytes(b"changed") + with patch("matrix.subprocess.Popen") as process, self.assertRaises(AssertionError): + Server(self.root, Path("java"), "forge.jar", "must-not-start") + process.assert_not_called() + + def test_incomplete_inventory_cannot_authorize_runtime(self): + with self.assertRaises(ValueError): + verify_code(self.root, {"files": {}}) + + def test_extra_root_jar_is_not_exempted(self): + (self.root / "installer.jar").write_bytes(b"unexpected") + with self.assertRaises(AssertionError): + verify_code(self.root) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_commands.py b/test-rig/migration/test_native117_commands.py new file mode 100644 index 00000000..b6fb7644 --- /dev/null +++ b/test-rig/migration/test_native117_commands.py @@ -0,0 +1,24 @@ +import unittest + +from native117_commands import native + + +class NativeCommandTests(unittest.TestCase): + def test_coordinates_slots_counts_and_emptying_are_retained(self): + for item, count in (('minecraft:iron_ore', 16), ('minecraft:air', 1)): + self.assertEqual('item replace block -14 8 230 container.26 with ' + item + ' ' + str(count), + native('replaceitem block -14 8 230 container.26 ' + item + ' ' + str(count))) + + def test_other_commands_are_unchanged(self): + for command in ('save-all flush', 'setblock 1 2 3 minecraft:air', 'advlifecycle snapshot tick'): + self.assertEqual(command, native(command)) + + def test_unknown_legacy_grammar_fails_rather_than_writing_nbt_or_clearing_a_different_inventory(self): + for command in ('replaceitem entity @e container.1 minecraft:stone 1', + 'replaceitem block 1 2 3 container.1 minecraft:stone 0', + 'replaceitem block 1 2 3 container.1 minecraft:stone 1 extra'): + with self.assertRaises(ValueError): native(command) + + +if __name__ == '__main__': + unittest.main() diff --git a/test-rig/migration/test_native117_connected.py b/test-rig/migration/test_native117_connected.py new file mode 100644 index 00000000..8258e5c3 --- /dev/null +++ b/test-rig/migration/test_native117_connected.py @@ -0,0 +1,334 @@ +"""Native117 connected destination safety and unchanged-oracle regression tests.""" + +import copy +import io +import json +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +import zipfile +from unittest.mock import Mock, patch + +import connected +from matrix import WORLD, digest, fingerprint, write_json +from native117 import LAUNCHER, RIG +import native117_connected as native +import native117_code as native_code +from test_connected import advanced, nbt, sample + + +BOOT_PROOF = ("Starting minecraft server version 1.17.1\n" + "Forge mod loading, version 37.1.1, for MC 1.17.1 with MCP 20210706.113038\n") + + +class NativeConnectedTest(unittest.TestCase): + def setUp(self): + root = RIG / "build/port-evidence" + root.mkdir(parents=True, exist_ok=True) + self.temp = tempfile.TemporaryDirectory(dir=root) + self.root = Path(self.temp.name) + self.definition = connected.fixture("original110") + + def tearDown(self): + self.temp.cleanup() + + def source(self): + source = self.root / "original110" + (source / WORLD).mkdir(parents=True) + (source / WORLD / "level.dat").write_bytes(b"unit-fixture-only-no-minecraft-process") + (source / "mods").mkdir() + jar = source / "mods/old.jar" + jar.write_bytes(b"archived-unit-test-artifact") + write_json(source / "input-jars.json", [{"file": jar.name, "sha256": digest(jar)}]) + manifest = self.root / "profiles.json" + write_json(manifest, {"original110": {"seed": str(source), "launcher": "forge-old.jar", + "jars": [{"path": str(jar), "sha256": digest(jar)}]}}) + write_json(source / "connected-fixture.json", self.definition) + write_json(source / "migration-fixture.json", self.definition["blocks"]) + cold, worked = sample(), advanced(sample()) + write_json(source / "connected-cold.json", cold) + write_json(source / "connected-source.json", worked) + write_json(source / "migration-source.json", worked["snapshot"]) + write_json(source / "fixture-hashes.json", fingerprint(source / WORLD)) + write_json(self.root / "connected-results.json", {"status": "FAIL", + "sources": {"original110": {"status": "PASS"}}, "routes": {"old-bridge": {"status": "FAIL"}}}) + return SimpleNamespace(source=source, source_manifest=manifest, source_key="original110", + source_hashes=source / "fixture-hashes.json", source_checkpoint="source", + target_manifest=self.root / "target.json", output=self.root / "new-output", + port=25576, ticks=64, java8=Path("unused-java8")) + + def test_only_observation_save_and_stop_commands_are_permitted(self): + for command in ("stop", "save-all", "save-all flush", "advconnected snapshot imported", + "advconnected step 1 first", "advconnected step 4000 last"): + native.require_command(command) + + def test_build_reset_refill_and_invalid_tick_commands_are_rejected(self): + for command in ("advconnected build source", "advworks reset all", "setblock 0 80 0 stone", + "data merge block 0 80 0 {}", "advconnected step 0 bad", "advconnected step 4001 bad", + "advconnected snapshot ok\nadvworks build all", "advconnected snapshot ../escape"): + with self.subTest(command=command), self.assertRaises(AssertionError): + native.require_command(command) + + def test_operation_cannot_start_before_preservation_gate(self): + server = native.NativeConnectedServer.__new__(native.NativeConnectedServer) + server.loaded_verified = False + with patch.object(connected.ConnectedServer, "send") as sender, self.assertRaises(AssertionError): + server.send("advconnected step 64 worked") + sender.assert_not_called() + + def test_failed_loaded_callback_never_opens_operation_gate(self): + server = native.NativeConnectedServer.__new__(native.NativeConnectedServer) + server.loaded_verified = False + server.on_loaded = Mock(side_effect=AssertionError("lost saved progress")) + with patch.object(connected.ConnectedServer, "observe"), self.assertRaises(AssertionError): + server.observe("snapshot", "converted-loaded") + self.assertFalse(server.loaded_verified) + + def test_valid_destination_document_uses_existing_state_oracle(self): + self.assertEqual(native.require_document(sample(), self.definition, destination=True), connected.state(sample())) + + def test_missing_duplicate_and_moved_witnesses_fail(self): + for mutation in (lambda rows: rows.pop(), lambda rows: rows.append(copy.deepcopy(rows[0])), + lambda rows: rows[0].update(x=99)): + document = sample() + mutation(document["snapshot"]) + with self.assertRaises(AssertionError): + native.require_document(document, self.definition, destination=True) + + def test_rebuild_or_unfrozen_snapshot_is_not_preservation(self): + for field, value in (("buildCallsThisProcess", 1), ("held", False)): + document = sample() + document[field] = value + with self.assertRaises(AssertionError): + native.require_document(document, self.definition, destination=True) + + def test_unchanged_oracle_rejects_quantity_loss_and_wrong_drill_products(self): + for station, contents in (("C-ELECTRIC", '{Items:[]}'), + ("C-EDRILL", '{Items:[{id:"minecraft:gravel",Count:9b}]}')): + document = advanced(sample()) + nbt(connected.entries(document)[station + ("-drill" if station.endswith("DRILL") else "-furnace")], contents) + with self.assertRaises(AssertionError): + native.require_document(document, self.definition, destination=True) + + def test_idle_and_nonmeasured_intervals_still_fail_existing_operation_oracle(self): + before = sample() + idle = copy.deepcopy(before) + idle.update(requestedTicks=64, advancedTicks=64) + self.assertEqual(connected.operation(before, idle, 64)["status"], "FAIL") + worked = advanced(before) + worked["advancedTicks"] = 0 + self.assertEqual(connected.operation(before, worked, 64)["status"], "FAIL") + + def test_finite_water_creation_remains_failure(self): + before = sample() + worked = advanced(before) + nbt(connected.entries(worked)["C-STEAM-water-0"], '{Tank:{FluidName:"water",Amount:10001}}') + self.assertEqual(connected.operation(before, worked, 64)["status"], "FAIL") + + def test_boot_proof_needs_both_native_version_and_clean_log(self): + log = self.root / "console.log" + log.write_text(BOOT_PROOF) + native.require_native_boot(log) + for text in (BOOT_PROOF.replace("1.17.1", "1.13.2"), BOOT_PROOF + "[main/ERROR] native failure\n", + BOOT_PROOF + "java.lang.NoSuchMethodError: example\n"): + log.write_text(text) + with self.assertRaises(AssertionError): + native.require_native_boot(log) + + def test_explicit_source_pins_and_historical_failures_are_preserved(self): + args = self.source() + before = fingerprint(args.source) + definition, snapshot, verdict, actual, historical = native.verify_source(args) + self.assertEqual(definition, self.definition) + self.assertEqual(verdict["status"], "PASS") + self.assertEqual(historical["status"], "FAIL") + self.assertEqual(actual, before) + self.assertEqual(fingerprint(args.source), before) + + def test_changed_source_world_fails_before_staging(self): + args = self.source() + (args.source / WORLD / "level.dat").write_bytes(b"changed") + with patch.object(native, "prepare_target") as prepare, self.assertRaises(AssertionError): + native.run(args) + prepare.assert_not_called() + self.assertFalse(args.output.exists()) + + def test_changed_source_jar_or_wrong_profile_key_fails(self): + args = self.source() + args.source_key = "maintained110" + with self.assertRaises(ValueError): + native.verify_source(args) + args.source_key = "original110" + (args.source / "mods/old.jar").write_bytes(b"changed") + with self.assertRaises(AssertionError): + native.verify_source(args) + + def test_nonconnected_operation_fixture_cannot_be_reconstructed(self): + args = self.source() + (args.source / "connected-fixture.json").unlink() + with patch.object(native, "prepare_target") as prepare, self.assertRaises(FileNotFoundError): + native.run(args) + prepare.assert_not_called() + + def test_target_prepare_copies_only_fixture_and_has_no_build_authority(self): + args = self.source() + (args.source / "connected-build.allow").touch() + (args.source / "connected-built.json").write_text("{}") + before = fingerprint(args.source) + seed = self.root / "native-seed" + (seed / "libraries").mkdir(parents=True) + (seed / LAUNCHER).parent.mkdir(parents=True, exist_ok=True) + (seed / LAUNCHER).write_bytes(b"unit-test-launcher-not-executed") + jar = self.root / "target.jar" + jar.write_bytes(b"unit-test-target-not-executed") + runtime = self.root / "target" + pins = {"launcher_sha256": digest(seed / LAUNCHER), "jars": [{"path": str(jar), "sha256": digest(jar)}]} + self.assertEqual(native.prepare_target(runtime, (seed, LAUNCHER, [jar]), self.definition, 25576, args.source, pins), LAUNCHER) + self.assertEqual(fingerprint(runtime / WORLD), fingerprint(args.source / WORLD)) + self.assertFalse((runtime / "connected-build.allow").exists()) + self.assertFalse((runtime / "connected-built.json").exists()) + self.assertIn("server-port=25576", (runtime / "server.properties").read_text()) + self.assertNotIn("3;minecraft:bedrock", (runtime / "server.properties").read_text()) + self.assertEqual(fingerprint(args.source), before) + + def test_non_native_launcher_is_rejected_without_creating_target(self): + runtime = self.root / "target" + with self.assertRaises(ValueError): + native.prepare_target(runtime, (self.root, "forge-1.13.2.jar", []), self.definition, 25576, self.root, {}) + self.assertFalse(runtime.exists()) + + def target(self, args): + from native117_code import INVENTORY, code_files + seed = self.root / "seed" + (seed / "libraries").mkdir(parents=True) + (seed / LAUNCHER).parent.mkdir(parents=True, exist_ok=True) + (seed / LAUNCHER).write_bytes(b"unit-test-launcher-never-executed") + jars = [] + for identity in ("basepower", "basesteam", "baseelectric", "advantageworkstest", "orespawn"): + jar = seed / (identity + ".jar") + with zipfile.ZipFile(jar, "w") as archive: + archive.writestr("META-INF/mods.toml", f'[[mods]]\nmodId="{identity}"\nversion="1.0.0.117011"\n') + jars.append(dict(path=str(jar), id=identity, version="1.0.0.117011", sha256=digest(jar))) + server_code = {"files": code_files(seed)} + write_json(seed / INVENTORY, server_code) + write_json(args.target_manifest, {"target117": dict(seed=str(seed), launcher=LAUNCHER, + launcher_sha256=digest(seed / LAUNCHER), jars=jars, serverCode=server_code)}) + + def test_runtime_failure_is_retained_and_source_is_still_rechecked(self): + args = self.source() + self.target(args) + before = fingerprint(args.source) + with patch.object(native.socket, "socket"), patch.object(native, "prepare_target", side_effect=RuntimeError("retained native failure")), self.assertRaises(RuntimeError): + native.run(args) + report = native.read_json(args.output / "native117-connected-results.json") + self.assertEqual(report["status"], "FAIL") + self.assertEqual(report["failure"], "retained native failure") + self.assertEqual(report["sourceRuntimeAfter"], before) + self.assertEqual(report["sourcePreservation"]["status"], "PASS") + + def test_source_change_on_failure_is_reported_not_hidden(self): + args = self.source() + self.target(args) + def changed(*unused): + (args.source / WORLD / "level.dat").write_bytes(b"unexpected-source-change") + raise RuntimeError("native failure") + with patch.object(native.socket, "socket"), patch.object(native, "prepare_target", side_effect=changed), self.assertRaisesRegex(AssertionError, "Protected source"): + native.run(args) + report = native.read_json(args.output / "native117-connected-results.json") + self.assertEqual(report["status"], "FAIL") + self.assertEqual(report["sourcePreservation"]["status"], "FAIL") + + def test_candidate_change_after_preflight_cannot_replace_pinned_bytes(self): + args = self.source() + seed = self.root / "seed" + (seed / "libraries").mkdir(parents=True) + (seed / LAUNCHER).parent.mkdir(parents=True, exist_ok=True) + (seed / LAUNCHER).write_bytes(b"launcher-not-executed") + jar = self.root / "candidate.jar" + jar.write_bytes(b"pinned") + pins = {"launcher_sha256": digest(seed / LAUNCHER), "jars": [{"path": str(jar), "sha256": digest(jar)}]} + jar.write_bytes(b"evolving-after-preflight") + with self.assertRaises(AssertionError): + native.prepare_target(self.root / "target", (seed, LAUNCHER, [jar]), self.definition, 25576, args.source, pins) + + def test_continued_output_cannot_promote_idle_machinery(self): + self.assertEqual(native.continued_output(sample(), advanced(sample()))["status"], "PASS") + self.assertEqual(native.continued_output(sample(), sample())["status"], "FAIL") + + def launch_fixture(self, *, missing_debug=False): + runtime = self.root / "native launch with spaces" + logging_jar = runtime / "libraries/net/minecraftforge/fmlloader/1.17.1-37.1.1/fmlloader-1.17.1-37.1.1.jar" + logging_jar.parent.mkdir(parents=True) + logging_jar.write_bytes(b"unit logging input; no JVM is started") + launcher = runtime / LAUNCHER + launcher.parent.mkdir(parents=True, exist_ok=True) + launcher.write_bytes(b"unit argument file; no JVM is started") + write_json(runtime / native_code.INVENTORY, dict(format=1, minecraft="1.17.1", forge="37.1.1", + files=native_code.code_files(runtime))) + process = SimpleNamespace(stdout=io.StringIO(BOOT_PROOF + "Done (unit fixture)\n" + "ADVCONNECTED COMPLETE converted-loaded\nADVCONNECTED COMPLETE converted-worked\n"), + stdin=io.StringIO(), returncode=None, pid=117) + process.poll = lambda: process.returncode + def wait(timeout=None): + process.returncode = 0 + return 0 + process.wait = wait + process.kill = Mock(side_effect=AssertionError("Constructor fixture must stop cleanly")) + def spawn(*args, **kwargs): + logs = runtime / "logs" + logs.mkdir() + (logs / "latest.log").write_text(BOOT_PROOF) + if not missing_debug: + (logs / "debug.log").write_text(BOOT_PROOF) + return process + return runtime, logging_jar, process, spawn + + def test_constructor_uses_native_argument_file_and_keeps_connected_command_trace(self): + runtime, logging_jar, process, spawn = self.launch_fixture() + callback = Mock() + with patch.object(native_code.subprocess, "Popen", side_effect=spawn) as popen: + server = native.NativeConnectedServer(runtime, Path("unit-java16.exe"), LAUNCHER, "converted", callback) + try: + argv = popen.call_args.args[0] + self.assertIn("@" + LAUNCHER, argv) + self.assertNotIn("-jar", argv) + self.assertIn("--add-opens", argv) + self.assertIn("java.base/java.lang.invoke=ALL-UNNAMED", argv) + self.assertIn("-Dlog4j.configurationFile=jar:" + logging_jar.resolve().as_uri() + "!/log4j2.xml", argv) + self.assertEqual(runtime, popen.call_args.kwargs["cwd"]) + with self.assertRaisesRegex(AssertionError, "Preservation must pass"): + server.send("advconnected step 512 premature") + server.observe("snapshot", "converted-loaded") + callback.assert_called_once_with("converted-loaded") + server.observe("step", "converted-worked", 512) + server.send("save-all flush") + finally: + server.close() + expected = ["advconnected snapshot converted-loaded", "advconnected step 512 converted-worked", "save-all flush", "stop"] + self.assertEqual(expected, native.read_json(runtime / "console-converted.commands.json")) + self.assertEqual(expected, process.stdin.getvalue().splitlines()) + for kind in ("console", "latest", "debug"): + native.require_native_boot(runtime / (kind + "-converted.log")) + native_code.verify_code(runtime) + + def test_constructor_close_uses_native_three_log_gate(self): + runtime, _, _, spawn = self.launch_fixture(missing_debug=True) + with patch.object(native_code.subprocess, "Popen", side_effect=spawn): + server = native.NativeConnectedServer(runtime, Path("unit-java16.exe"), LAUNCHER, "converted", Mock()) + with self.assertRaisesRegex(AssertionError, "Missing native runtime log"): + server.close() + self.assertEqual(["stop"], native.read_json(runtime / "console-converted.commands.json")) + + def test_constructor_close_still_rejects_code_inventory_mutation(self): + runtime, logging_jar, _, spawn = self.launch_fixture() + with patch.object(native_code.subprocess, "Popen", side_effect=spawn): + server = native.NativeConnectedServer(runtime, Path("unit-java16.exe"), LAUNCHER, "converted", Mock()) + logging_jar.write_bytes(b"changed sealed input") + with self.assertRaisesRegex(AssertionError, "exact sealed inventory"): + server.close() + self.assertEqual(["stop"], native.read_json(runtime / "console-converted.commands.json")) + + +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 new file mode 100644 index 00000000..c60b46d5 --- /dev/null +++ b/test-rig/migration/test_native117_dimension_assessment.py @@ -0,0 +1,311 @@ +import copy +import json +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import MagicMock, patch + +import dimensions +from matrix import digest +import native117_actors +import native117_audit_actors +import native117_dimension_assessment as assessment + + +OWNER = "e036830e-303d-3ac3-bd23-062b72eed895" +PROOF = {"sha256": assessment.CONTROL_LOG_SHA256, "worldDataVersion": 2730, + "forgeVersion": "37.1.1", "scope": "NATIVE_TAMED_WOLF_LOAD_8_TO_20_NO_POWER"} + + +def states(): + before = {"dimension": -1, "file": {"Inventory": [{"id": "minecraft:stone", "Count": 3, + "Slot": 0, "tag": {"Private": "keep"}}]}, "relationships": {}, "entities": { + "passenger-wolf": {"id": "minecraft:wolf", "UUIDMost": -17, "UUIDLeast": 19, + "OwnerUUID": OWNER, "Health": 8.0, "Leashed": 0, + "Attributes": {"generic.maxHealth": {"Base": 20.0}, + "example:private": {"Base": 3.25, "Private": {"keep": [1, 2]}}}}}} + after = copy.deepcopy(before) + after["entities"]["passenger-wolf"]["Health"] = 20.0 + return before, after + + +def call(before, after, proof=Path("reviewed-control.log"), **kwargs): + return assessment.assess(before, after, dimensions.verdict(before, after), proof, **kwargs) + + +def checks_and_assessments(): + before, after = states() + checks, assessments = {}, {} + for key in assessment.SEMANTIC_CHECKS: + old = after if key == "idempotence" else before + checks[key] = dimensions.verdict(old, after) + assessments[key] = assessment.assess(old, after, checks[key], Path("control.log"), "survival") + for key in assessment.DIMENSION_CHECKS - assessment.SEMANTIC_CHECKS: + checks[key] = {"status": "PASS"} + checks["backup-stability"]["backups"] = {} + return checks, assessments + + +class NativeDifferenceAssessmentTest(unittest.TestCase): + def test_only_reviewed_tamed_wolf_change_has_separate_status_and_keeps_raw_fail(self): + before, after = states(); original = copy.deepcopy((before, after)) + with patch.object(assessment, "load_native_proof", return_value=PROOF) as proof: + result = call(before, after) + self.assertEqual("PASS_WITH_NATIVE_DIFFERENCE", result["status"]) + self.assertEqual("FAIL", result["rawStrictStatus"]) + self.assertEqual(dimensions.verdict(before, after), result["rawStrictResult"]) + self.assertEqual({"UUIDMost": -17, "UUIDLeast": 19}, result["nativeDifferences"][0]["entityUuid"]) + self.assertEqual(original, (before, after)) + proof.assert_called_once_with(Path("reviewed-control.log")) + + def test_missing_uuid_halves_cannot_be_classified(self): + for key in ("UUIDMost", "UUIDLeast"): + before, after = states() + del before["entities"]["passenger-wolf"][key]; del after["entities"]["passenger-wolf"][key] + self.assertEqual("FAIL", call(before, after)["status"]) + + def test_invalid_uuid_halves_nonbool_signed64_are_rejected(self): + for value in (True, False, 1.0, "17", None, 1 << 63, -(1 << 63) - 1): + for key in ("UUIDMost", "UUIDLeast"): + before, after = states() + before["entities"]["passenger-wolf"][key] = value + after["entities"]["passenger-wolf"][key] = value + self.assertEqual("FAIL", call(before, after)["status"]) + + def test_mismatching_entity_or_owner_uuid_cannot_be_classified(self): + for key, value in (("UUIDMost", 27), ("UUIDLeast", 29), ("OwnerUUID", "00000000-0000-0000-0000-000000000001")): + before, after = states(); after["entities"]["passenger-wolf"][key] = value + self.assertEqual("FAIL", call(before, after)["status"]) + + def test_owner_required_and_must_be_canonical_uuid(self): + for value in (None, "not-owner", True, ""): + before, after = states() + before["entities"]["passenger-wolf"]["OwnerUUID"] = value + after["entities"]["passenger-wolf"]["OwnerUUID"] = value + self.assertEqual("FAIL", call(before, after)["status"]) + + def test_other_entity_type_or_untamed_max_health_is_not_allowed(self): + for change in ("type", "maximum"): + before, after = states() + for state in (before, after): + if change == "type": state["entities"]["passenger-wolf"]["id"] = "minecraft:cat" + else: state["entities"]["passenger-wolf"]["Attributes"]["generic.maxHealth"]["Base"] = 8.0 + self.assertEqual("FAIL", call(before, after)["status"]) + + def test_no_generic_health_tolerance(self): + for old, new in ((7.0, 20.0), (8.0, 19.0), (8.0, 20.00001), (True, 20.0)): + before, after = states() + before["entities"]["passenger-wolf"]["Health"] = old + after["entities"]["passenger-wolf"]["Health"] = new + self.assertEqual("FAIL", call(before, after)["status"]) + + def test_other_raw_field_failures_cannot_be_relabelled(self): + before, after = states() + del after["entities"]["passenger-wolf"]["Leashed"] + self.assertEqual("FAIL", call(before, after)["status"]) + + def test_unknown_added_attributes_or_fields_are_not_silently_dropped(self): + for location in ("attribute", "payload"): + before, after = states() + if location == "attribute": after["entities"]["passenger-wolf"]["Attributes"]["example:extra"] = {"Base": 4.0} + else: after["entities"]["passenger-wolf"]["Attributes"]["generic.maxHealth"]["Extra"] = 1 + self.assertEqual("FAIL", call(before, after)["status"]) + + def test_item_quantity_and_unknown_item_payload_remain_exact(self): + for change in ("count", "payload"): + before, after = states() + if change == "count": after["file"]["Inventory"][0]["Count"] = 4 + else: after["file"]["Inventory"][0]["tag"]["Extra"] = True + self.assertEqual("FAIL", call(before, after)["status"]) + + def test_known_empty_modifiers_are_explicit_but_nonempty_changes_fail(self): + before, after = states() + after["entities"]["passenger-wolf"]["Attributes"]["generic.maxHealth"]["Modifiers"] = [] + with patch.object(assessment, "load_native_proof", return_value=PROOF): + self.assertEqual("PASS_WITH_NATIVE_DIFFERENCE", call(before, after)["status"]) + after["entities"]["passenger-wolf"]["Attributes"]["generic.maxHealth"]["Modifiers"] = [{"Amount": 1.0}] + self.assertEqual("FAIL", call(before, after)["status"]) + + def test_unknown_attribute_empty_modifier_fields_are_not_defaulted(self): + before, after = states() + after["entities"]["passenger-wolf"]["Attributes"]["example:private"]["Modifiers"] = [] + self.assertEqual("FAIL", call(before, after)["status"]) + + def test_reuses_only_the_existing_explicit_historical_survival_scope(self): + before, after = states() + after["entities"]["passenger-wolf"]["Pos"] = [10.0, 20.0, 30.0] + with patch.object(assessment, "load_native_proof", return_value=PROOF): + self.assertEqual("FAIL", call(before, after)["status"]) + self.assertEqual("PASS_WITH_NATIVE_DIFFERENCE", call(before, after, scope="survival")["status"]) + before["entities"]["passenger-wolf"]["Pos"] = [1.0, 2.0, 3.0] + self.assertEqual("FAIL", call(before, after, scope="survival")["status"]) + + def test_missing_proof_keeps_fail_and_changed_proof_is_rejected(self): + before, after = states() + self.assertEqual("FAIL", call(before, after, proof=None)["status"]) + with tempfile.TemporaryDirectory() as temp: + path = Path(temp) / "fake.log"; path.write_text("POWER_LEGACY_CLASS_ABSENT true\n") + with self.assertRaisesRegex(AssertionError, "reviewed control"): + call(before, after, proof=path) + + def test_raw_result_tampering_is_rejected(self): + before, after = states() + with self.assertRaises(AssertionError): assessment.assess(before, after, {"status": "PASS", "differences": []}) + + def test_strict_pass_is_not_reported_as_native_difference(self): + before, _ = states() + result = call(before, copy.deepcopy(before), proof=None) + self.assertEqual("PASS", result["status"]) + self.assertEqual([], result["nativeDifferences"]) + + def test_complete_gate_set_and_tick_backup_controls_are_mandatory(self): + with patch.object(assessment, "load_native_proof", return_value=PROOF): + checks, values = checks_and_assessments() + self.assertEqual("PASS_WITH_NATIVE_DIFFERENCE", assessment.summarize_checks(checks, values)["status"]) + self.assertEqual("FAIL", assessment.summarize_checks(checks, values)["rawStrictStatus"]) + for key in assessment.DIMENSION_CHECKS - assessment.SEMANTIC_CHECKS: + bad = copy.deepcopy(checks); bad[key]["status"] = "FAIL" + self.assertEqual("FAIL", assessment.summarize_checks(bad, values)["status"]) + incomplete = copy.deepcopy(checks); del incomplete["runtime-ticks"] + self.assertEqual("FAIL", assessment.summarize_checks(incomplete, values)["status"]) + extra = copy.deepcopy(checks); extra["unreviewed-gate"] = {"status": "FAIL"} + self.assertEqual("FAIL", assessment.summarize_checks(extra, values)["status"]) + + def test_forged_none_uuid_in_route_assessment_is_rejected(self): + with patch.object(assessment, "load_native_proof", return_value=PROOF): + checks, values = checks_and_assessments() + values["conversion"]["nativeDifferences"][0]["entityUuid"]["UUIDMost"] = None + self.assertEqual("FAIL", assessment.summarize_checks(checks, values)["status"]) + + def test_route_assessment_order_is_stable_for_independent_replay(self): + with patch.object(assessment, "load_native_proof", return_value=PROOF): + checks, values = checks_and_assessments() + self.assertEqual(assessment.summarize_checks(checks, values), + assessment.summarize_checks(dict(reversed(list(checks.items()))), values)) + + 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 self.assertRaises(AssertionError): native117_actors.run(SimpleNamespace(kind="dimensions", native_wolf_proof=proof)) + boot.assert_not_called() + + +class NativeRunnerAssessmentIntegrationTest(unittest.TestCase): + def test_route_writes_separate_assessment_without_changing_raw_checks(self): + before, after = states() + with tempfile.TemporaryDirectory() as temp: + root = Path(temp); source = root / "source"; source.mkdir(); runtime = root / "runtime" + (source / "dimension-fixture.json").write_text('{"dimension":-1}') + manifest = root / "manifest.json"; manifest.write_text("{}") + + def boot(runtime, target, java, port): + runtime.mkdir(); (runtime / "server.properties").write_text("allow-nether=false\n") + return "unused-no-process.jar" + + args = SimpleNamespace(kind="dimensions", java8=Path("unused"), port=25574, + target_manifest=manifest, native_wolf_proof=Path("reviewed.log")) + with patch("native117.boot", side_effect=boot), \ + patch.object(native117_actors, "copy_fixture"), \ + patch.object(native117_actors, "fingerprint", return_value={}), \ + patch.object(native117_actors, "baseline", return_value=(before, "source")), \ + patch.object(native117_actors, "Server") as server, \ + patch.object(native117_actors, "command", return_value=after), \ + patch.object(native117_actors, "release"), \ + patch.object(native117_actors, "raw", return_value={}), \ + patch.object(native117_actors, "ready_entities", return_value={'status': 'PASS'}), \ + patch.object(native117_actors, "audit_stopped", return_value=dict(status='PASS', + snapshotSha256='A' * 64, matchedEntities=5, auditedDimensions=1)), \ + patch.object(native117_actors, "require_clean_log"), \ + patch.object(native117_actors, "await_natural_ticks") as wait_ticks, \ + patch.object(native117_actors.actors, "backup_state", return_value={}), \ + patch.object(native117_actors.dimensions, "tick_result", return_value={"status": "PASS"}), \ + patch.object(assessment, "load_native_proof", return_value=PROOF): + checks = native117_actors.run_route(source, runtime, None, args) + self.assertEqual(3, server.call_count) + self.assertEqual(["conversion-release", "restart-load-release", "released", "restart-released"], + [call.args[-1] for call in wait_ticks.call_args_list]) + self.assertEqual("FAIL", checks["conversion"]["status"]) + sidecar = json.loads((runtime / "native117-platform-assessment.json").read_text()) + self.assertEqual("PASS_WITH_NATIVE_DIFFERENCE", sidecar["assessment"]["status"]) + self.assertEqual("FAIL", sidecar["assessment"]["rawStrictStatus"]) + self.assertEqual(checks, json.loads((runtime / "native117-comparison.json").read_text())) + + def test_raw_campaign_exit_stays_nonzero(self): + before, after = states() + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + args = SimpleNamespace(kind="dimensions", native_wolf_proof=None, output=root / "run", + fixtures=root / "fixtures", target_manifest=root / "manifest.json", routes=["original110"], port=25574) + with patch.object(native117_actors, "pinned_profile", return_value=(root / "seed", None, [])), \ + patch.object(native117_actors, "output_path", side_effect=lambda path: path), \ + patch.object(native117_actors.socket, "socket", return_value=MagicMock()), \ + patch.object(native117_actors, "run_route", return_value={"conversion": dimensions.verdict(before, after)}): + self.assertEqual(1, native117_actors.run(args)) + + +class NativeIndependentAssessmentReplayTest(unittest.TestCase): + def replay(self, root, tamper=False, bad_custody=False, omit_summary=False): + before, after = states() + route = root / "route"; route.mkdir() + source = root / "source"; source.mkdir() + manifest = root / "manifest.json"; manifest.write_text("{}") + checks, semantics = checks_and_assessments() + (root / "results.json").write_text(json.dumps({"route": checks})) + if not omit_summary: + (root / "summary.json").write_text(json.dumps({"kind": "dimensions", "routes": ["route"], "status": "FAIL"})) + (route / "native117-source-provenance.json").write_text(json.dumps({ + "source": str(source), "sourceFingerprint": {"changed": "not-empty"} if bad_custody else {}, + "targetManifest": str(manifest), "targetManifestSha256": digest(manifest)})) + (route / "input-jars.json").write_text("[]") + for label in ("converted", "adapter", "restarted", "runtime", "runtime-restart"): + (route / ("dimension-semantic-" + label + ".json")).write_text(json.dumps(after)) + platform = {"assessment": assessment.summarize_checks(checks, semantics), "semanticAssessments": semantics, + "proofPath": "control.log", "assessorSha256": digest(Path(assessment.__file__))} + if tamper: + platform["assessment"]["status"] = "PASS" + (route / "native117-platform-assessment.json").write_text(json.dumps(platform)) + with patch.object(native117_audit_actors, "fingerprint", return_value={}), \ + patch.object(native117_audit_actors, "validate_mod_inventory", return_value={"status": "PASS"}), \ + patch.object(native117_audit_actors, "pinned_profile"), \ + patch.object(native117_audit_actors, "baseline", return_value=(before, "source")), \ + patch.object(native117_audit_actors, "capture_dimensions", return_value=copy.deepcopy(after)), \ + patch.object(native117_audit_actors, "native_controls", return_value={ + key: checks[key] for key in assessment.NATIVE_CHECKS}), \ + patch.object(native117_audit_actors, "raw", return_value={}), \ + patch.object(native117_audit_actors, "require_clean_log"), \ + patch.object(native117_audit_actors.actors, "backup_state", return_value={}), \ + patch.object(native117_audit_actors.dimensions, "tick_result", return_value={"status": "PASS"}): + return native117_audit_actors.replay(root, "dimensions") + + def test_audit_replays_separate_assessment_but_keeps_all_five_raw_failures(self): + with tempfile.TemporaryDirectory() as temp, patch.object(assessment, "load_native_proof", return_value=PROOF): + result = self.replay(Path(temp)) + self.assertEqual("FAIL", result["status"]) + self.assertEqual(5, len(result["findings"])) + self.assertEqual("PASS_WITH_NATIVE_DIFFERENCE", result["nativePlatformAssessment"]["status"]) + self.assertEqual([], result["nativePlatformAssessment"]["otherFindings"]) + + def test_tampered_sidecar_cannot_relabel_raw_or_separate_qualification(self): + with tempfile.TemporaryDirectory() as temp, patch.object(assessment, "load_native_proof", return_value=PROOF): + result = self.replay(Path(temp), tamper=True) + self.assertEqual("FAIL", result["status"]) + self.assertEqual("FAIL", result["nativePlatformAssessment"]["status"]) + self.assertTrue(any("independent replay" in value for value in result["findings"])) + + def test_source_custody_failure_blocks_native_difference_qualification(self): + with tempfile.TemporaryDirectory() as temp, patch.object(assessment, "load_native_proof", return_value=PROOF): + result = self.replay(Path(temp), bad_custody=True) + self.assertEqual("FAIL", result["nativePlatformAssessment"]["status"]) + self.assertTrue(any("source fingerprint" in value for value in result["findings"])) + + def test_partial_campaign_does_not_gain_completed_native_qualification(self): + with tempfile.TemporaryDirectory() as temp, patch.object(assessment, "load_native_proof", return_value=PROOF): + result = self.replay(Path(temp), omit_summary=True) + self.assertEqual("FAIL", result["nativePlatformAssessment"]["status"]) + self.assertFalse(result["nativePlatformAssessment"]["completeCampaign"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_dimension_io.py b/test-rig/migration/test_native117_dimension_io.py new file mode 100644 index 00000000..f20ac3b9 --- /dev/null +++ b/test-rig/migration/test_native117_dimension_io.py @@ -0,0 +1,259 @@ +import base64 +import copy +import gzip +import io +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import nbtlib +import native117_actor_io +import dimensions +from matrix import WORLD +from native117_dimension_io import (ATTRIBUTE_NAMES, FORGE_ATTRIBUTES, VANILLA_ATTRIBUTES, attribute_list, entity_uuid, + observed_context, parse_dimension_nbt, project_entity, validate_record) + + +ROOT = [1, 2, 3, 4] +CHILD = [5, 6, 7, 8] +NETHER = "minecraft:the_nether" +END = "minecraft:the_end" + + +def typed(value, key=None): + if isinstance(value, dict): + return nbtlib.Compound({name: typed(child, name) for name, child in value.items()}) + if isinstance(value, list): + if key == "UUID": + return nbtlib.IntArray(value) + if not value: + return nbtlib.List[nbtlib.Compound]() + values = [typed(child) for child in value] + return nbtlib.List[type(values[0])](values) + if isinstance(value, str): + return nbtlib.String(value) + if isinstance(value, float): + return nbtlib.Double(value) + return nbtlib.Int(value) + + +def entry(payload, key=NETHER): + buffer = io.BytesIO() + nbtlib.File(typed(payload)).write(buffer) + worlds = {entity_uuid(payload): key} + for passenger in payload.get("Passengers", []): + worlds.update(entry(passenger, key)["entityWorlds"]) + return {"uuid": entity_uuid(payload), "witness": "boat", "worldDimensionKey": key, + "sameWorld": True, "entityWorlds": worlds, + "nbtBase64": base64.b64encode(gzip.compress(buffer.getvalue())).decode("ascii")} + + +def record(payload=None, key=NETHER): + return {"nativeEntityContextVersion": 1, "worldDimensionKey": key, + "dimension": -1 if key == NETHER else 1, + "entities": [entry(payload or {"id": "minecraft:boat", "UUID": ROOT}, key)], + "definition": {"dimension": 999}} + + +class AttributeProjectionTest(unittest.TestCase): + def test_exact_eight_vanilla_names_are_bijective_and_preserve_payloads(self): + self.assertEqual(8, len(VANILLA_ATTRIBUTES)) + self.assertEqual(8, len(set(VANILLA_ATTRIBUTES.values()))) + for native, legacy in VANILLA_ATTRIBUTES.items(): + source = [{"Name": native, "Base": 2.25, "Private": {"Name": native}, + "Modifiers": [{"UUID": ROOT, "Amount": 0.75, "Operation": 0, "Private": "keep"}]}] + before = copy.deepcopy(source) + expected = copy.deepcopy(source) + expected[0]["Name"] = legacy + self.assertEqual(expected, attribute_list(source)) + self.assertEqual(before, source) + + def test_only_the_four_source_verified_forge_identities_are_projected(self): + self.assertEqual({"forge:swim_speed": "forge.swimSpeed", + "forge:nametag_distance": "forge.nameTagDistance", + "forge:entity_gravity": "forge.entity_gravity", + "forge:reach_distance": "generic.reachDistance"}, FORGE_ATTRIBUTES) + for native, legacy in FORGE_ATTRIBUTES.items(): + source = [{"Name": native, "Base": 12.5, "Private": {"Name": native}, + "Modifiers": [{"UUID": ROOT, "Amount": 0.125, "Operation": 0, "Private": "keep"}]}] + before = copy.deepcopy(source) + expected = copy.deepcopy(source); expected[0]["Name"] = legacy + self.assertEqual(expected, attribute_list(source)) + self.assertEqual(before, source) + for name in ("forge:other", "forge:swimSpeed", "example:swim_speed", "generic.max_health"): + source = [{"Name": name, "Base": 2.0, "Unknown": [1, 2]}] + self.assertEqual(source, attribute_list(source)) + + def test_absent_and_empty_known_modifiers_are_explicit_without_losing_nonempty(self): + for value in ({"Name": "minecraft:generic.armor", "Base": 0.0}, + {"Name": "minecraft:generic.armor", "Base": 0.0, "Modifiers": []}): + self.assertEqual([], attribute_list([value])[0]["Modifiers"]) + modifier = {"UUID": ROOT, "Amount": 3.0, "Operation": 2, "Unrecognized": {"keep": 7}} + self.assertEqual([modifier], attribute_list([{"Name": "minecraft:generic.armor", "Modifiers": [modifier]}])[0]["Modifiers"]) + + def test_rejects_native_legacy_name_collisions(self): + for native, legacy in ATTRIBUTE_NAMES.items(): + with self.assertRaisesRegex(ValueError, "Ambiguous"): + attribute_list([{"Name": native, "Base": 2}, {"Name": legacy, "Base": 3}]) + + def test_rejects_duplicate_unknown_attributes_instead_of_dict_overwrite(self): + with self.assertRaisesRegex(ValueError, "Ambiguous"): + attribute_list([{"Name": "example:custom"}, {"Name": "example:custom"}]) + + def test_rejects_malformed_attribute_and_modifier_containers(self): + for value in ({}, [7], [{"Base": 2}], [{"Name": 3}], + [{"Name": "minecraft:generic.armor", "Modifiers": {}}], + [{"Name": "minecraft:generic.armor", "Modifiers": ["not-compound"]}]): + with self.assertRaises(ValueError): + attribute_list(value) + + def test_idempotent_projection_preserves_unknown_attribute_keys(self): + source = [{"Name": "minecraft:generic.max_health", "Base": 20.0, "Private": {"attributes": [1]}}] + once = attribute_list(source) + self.assertEqual(once, attribute_list(once)) + self.assertEqual(source[0]["Private"], once[0]["Private"]) + + +class ActualWorldContextTest(unittest.TestCase): + def test_actual_registry_world_is_used_not_expected_definition(self): + payload = {"id": "minecraft:boat", "UUID": ROOT} + raw = record(payload, END) + raw["definition"]["dimension"] = -1 + self.assertEqual(1, project_entity(payload, observed_context(raw))["Dimension"]) + + def test_actual_passenger_context_is_required_and_projected(self): + child = {"id": "minecraft:wolf", "UUID": CHILD, "Health": 20.0} + payload = {"id": "minecraft:boat", "UUID": ROOT, "Passengers": [child]} + raw = record(payload) + before = copy.deepcopy(raw) + actual = project_entity(payload, observed_context(raw)) + self.assertEqual(-1, actual["Dimension"]) + self.assertEqual(-1, actual["Passengers"][0]["Dimension"]) + self.assertEqual(20.0, actual["Passengers"][0]["Health"]) + self.assertEqual(before, raw) + + def test_missing_context_is_never_inferred(self): + for key in ("nativeEntityContextVersion", "worldDimensionKey"): + raw = record(); del raw[key] + with self.assertRaises(ValueError): observed_context(raw) + raw = record(); del raw["entities"][0]["worldDimensionKey"] + with self.assertRaises(ValueError): observed_context(raw) + + def test_same_world_requires_a_real_boolean_true(self): + for value in (False, 1, "true", None): + raw = record(); raw["entities"][0]["sameWorld"] = value + with self.assertRaises(AssertionError): observed_context(raw) + + def test_rejects_root_key_and_observed_dimension_mismatch(self): + raw = record(); raw["dimension"] = 1 + with self.assertRaises(AssertionError): observed_context(raw) + + def test_rejects_existing_nbt_dimension_mismatch(self): + payload = {"id": "minecraft:boat", "UUID": ROOT, "Dimension": 1} + with self.assertRaises(AssertionError): project_entity(payload, {entity_uuid(payload): NETHER}) + payload["Dimension"] = NETHER + self.assertEqual(-1, project_entity(payload, {entity_uuid(payload): NETHER})["Dimension"]) + + def test_rejects_missing_extra_and_wrong_passenger_world_observations(self): + payload = {"id": "minecraft:boat", "UUID": ROOT, + "Passengers": [{"id": "minecraft:wolf", "UUID": CHILD}]} + raw = record(payload) + missing = copy.deepcopy(raw); del missing["entities"][0]["entityWorlds"][entity_uuid(payload["Passengers"][0])] + extra = copy.deepcopy(raw); extra["entities"][0]["entityWorlds"]["extra"] = NETHER + wrong = copy.deepcopy(raw); wrong["entities"][0]["entityWorlds"][entity_uuid(payload["Passengers"][0])] = END + for bad in (missing, extra, wrong): + with self.assertRaises(AssertionError): observed_context(bad) + + def test_rejects_duplicate_root_uuid_and_mismatching_uuid_attestation(self): + raw = record(); raw["entities"].append(copy.deepcopy(raw["entities"][0])) + with self.assertRaises(AssertionError): observed_context(raw) + raw = record(); raw["entities"][0]["uuid"] = "wrong" + with self.assertRaises(AssertionError): observed_context(raw) + + def test_rejects_duplicate_serialized_passenger_uuid(self): + payload = {"id": "minecraft:boat", "UUID": ROOT, + "Passengers": [{"id": "minecraft:wolf", "UUID": ROOT}]} + with self.assertRaises(AssertionError): observed_context(record(payload)) + + def test_rejects_unknown_world_keys_and_missing_entity_observations(self): + payload = {"id": "minecraft:boat", "UUID": ROOT} + for contexts in ({}, {entity_uuid(payload): "example:unknown"}): + with self.assertRaises(ValueError): project_entity(payload, contexts) + + def test_does_not_walk_inventory_private_compounds_or_change_health(self): + private = {"Attributes": [{"Name": "minecraft:generic.max_health", "Base": 17.0}]} + payload = {"id": "minecraft:wolf", "UUID": ROOT, "Health": 20.0, + "Attributes": [{"Name": "minecraft:generic.max_health", "Base": 20.0}], + "PrivateData": private, "Inventory": [private]} + before = copy.deepcopy(payload) + actual = project_entity(payload, {entity_uuid(payload): NETHER}) + self.assertEqual(20.0, actual["Health"]) + self.assertEqual(private, actual["PrivateData"]) + self.assertEqual([private], actual["Inventory"]) + self.assertEqual(before, payload) + + +class CaptureBindingTest(unittest.TestCase): + def test_real_definition_entity_references_are_not_serialized_nbt(self): + definition = {"dimension": -1, "entities": [ + {"uuid": "382ee065-9b64-3e5d-ba95-28d25fe76f39", "witness": "boat"}, + {"uuid": "8fcfc8b9-ad2c-3649-8bbc-9985c0044121", "witness": "horse"}, + {"uuid": "56a3698a-8a5b-3f03-9088-7283f466f0e2", "witness": "hydroturbine"}, + {"uuid": "5e0e0ed0-34bb-3d03-8b5a-b262336b07e4", "witness": "leash-knot"}, + {"uuid": "b1a56f21-95c0-3e04-a77d-b160826c9c7f", "witness": "passenger-wolf"}], + "generator": {"name": "electricadvantage:hydroelectric_generator", "x": 176, "y": 101, "z": 8}, + "geometry": {"minX": 144, "maxX": 176, "y": 100, "z": 8}, "nativeOnLoad": True, + "playerName": "AdvWorksDimensionPlayer", "playerUuid": "e036830e-303d-3ac3-bd23-062b72eed895", + "universalBucket": False} + with patch("native117_dimension_io.parse_nbt", side_effect=AssertionError("Definition is not an NBT snapshot")) as parse: + self.assertEqual(definition, native117_actor_io.DimensionJson.loads(json.dumps(definition))) + parse.assert_not_called() + + def test_explicit_native_observations_without_payload_still_fail_closed(self): + raw = record(); del raw["entities"][0]["nbtBase64"] + with self.assertRaisesRegex(KeyError, "nbtBase64"): + native117_actor_io.DimensionJson.loads(json.dumps(raw)) + for marker, value in (("worldDimensionKey", NETHER), ("sameWorld", True), ("entityWorlds", {})): + explicit = {marker: value, "entities": [{"uuid": "00000000-0000-0001-0000-000000000002", "witness": "boat"}]} + with self.assertRaisesRegex(ValueError, "versioned"): + native117_actor_io.DimensionJson.loads(json.dumps(explicit)) + contextual = copy.deepcopy(raw); contextual.pop("nativeEntityContextVersion"); contextual.pop("worldDimensionKey") + with self.assertRaisesRegex(ValueError, "versioned"): + native117_actor_io.DimensionJson.loads(json.dumps(contextual)) + + def test_legacy_source_capture_keeps_existing_boundary(self): + payload = {"id": "Wolf", "UUIDMost": 1, "UUIDLeast": 2, "Dimension": -1, + "Attributes": [{"Name": "generic.maxHealth", "Base": 20.0}]} + old = entry(payload); old.pop("worldDimensionKey"); old.pop("sameWorld"); old.pop("entityWorlds") + self.assertEqual(payload, parse_dimension_nbt(old)) + self.assertIs(native117_actor_io.capture_dimensions.__code__, dimensions.capture.__code__) + + def test_sealed_old_native_data_without_context_cannot_be_relabelled(self): + raw = record() + raw.pop("nativeEntityContextVersion"); raw.pop("worldDimensionKey") + for key in ("worldDimensionKey", "sameWorld", "entityWorlds"): + raw["entities"][0].pop(key) + with self.assertRaises(ValueError): validate_record(raw) + + def test_native_capture_uses_contextual_projection_in_historical_function_copy(self): + payload = {"id": "minecraft:wolf", "UUID": ROOT, "Health": 20.0, + "Attributes": [{"Name": "minecraft:generic.max_health", "Base": 20.0}]} + + with tempfile.TemporaryDirectory() as temp: + root = Path(temp); world = root / WORLD; (world / "playerdata").mkdir(parents=True) + nbtlib.File(typed({"Data": {"DataVersion": 2730}})).save(world / "level.dat", gzipped=True) + player = {"UUID": ROOT, "Dimension": NETHER, "Inventory": [], "EnderItems": []} + nbtlib.File(typed(player)).save(world / "playerdata" / (entity_uuid(player) + ".dat"), gzipped=True) + (root / "dimension-fixture.json").write_text(json.dumps({"playerUuid": entity_uuid(player)})) + raw = record(payload); raw["playerAdapter"] = False; raw["generator"] = {"actualName": "minecraft:stone"} + (root / "dimensions-native.json").write_text(json.dumps(raw)) + actual = native117_actor_io.capture_dimensions(root, "native") + self.assertEqual(-1, actual["entities"]["boat"]["Dimension"]) + self.assertEqual(20.0, actual["entities"]["boat"]["Attributes"]["generic.maxHealth"]["Base"]) + self.assertEqual(20.0, actual["entities"]["boat"]["Health"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_dimension_readiness_context.py b/test-rig/migration/test_native117_dimension_readiness_context.py new file mode 100644 index 00000000..5e46f21e --- /dev/null +++ b/test-rig/migration/test_native117_dimension_readiness_context.py @@ -0,0 +1,64 @@ +"""Explicit dimension context without weakening readiness receipt semantics.""" + +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import Mock, patch + +import native117_actors as live +import native117_audit_actors as audit +from native117_readiness import require_ready + +BOUNDS = (6, -2, 13, 2) + + +def ready(key, label): + rows = [dict(x=x, z=z, blockAndEntitiesLoaded=True, entityTicking=True) + for x in range(6, 14) for z in range(-2, 3)] + return dict(protocol="native117-chunk-readiness-v1", status="PASS", label="entities-" + label, + minecraft="1.17.1", dimension=key, bounds=dict(zip(("minX", "minZ", "maxX", "maxZ"), BOUNDS)), + checks=1, elapsedMillis=1, requestedWorldTime=10, completedWorldTime=11, + maximumChecks=1200, maximumWaitMillis=120000, chunks=rows, + transitions=[dict(check=1, worldTime=11, changes=rows)]) + + +class DimensionReadinessContextTest(unittest.TestCase): + def test_live_calls_pass_actual_nether_and_end_context(self): + for identity, key in ((-1, "minecraft:the_nether"), (1, "minecraft:the_end")): + with self.subTest(key=key), tempfile.TemporaryDirectory() as temp: + root = Path(temp) + (root / "dimension-fixture.json").write_text(json.dumps(dict(dimension=identity))) + server = Mock() + with patch.object(live, "await_ready", return_value=ready(key, "conversion")) as wait: + live.ready_entities(server, root, "dimensions", "conversion") + self.assertEqual((root, BOUNDS, "entities-conversion", key), wait.call_args.args[1:]) + wait.call_args.args[0].send("probe") + server.send.assert_called_with("execute in " + key + " run probe") + + def test_replay_passes_same_explicit_dimension_and_keeps_raw_gate(self): + for identity, key in ((-1, "minecraft:the_nether"), (1, "minecraft:the_end")): + with self.subTest(key=key), tempfile.TemporaryDirectory() as temp: + root = Path(temp) + (root / "dimension-fixture.json").write_text(json.dumps(dict(dimension=identity))) + folder = root / "advantage-works-results"; folder.mkdir() + recorded = {} + for phase in ("conversion", "restart", "runtime-restart"): + (folder / ("native-readiness-entities-" + phase + ".json")).write_text(json.dumps(ready(key, phase))) + recorded["saved-entities-" + phase] = dict(snapshotSha256="A" * 64) + with patch.object(audit, "raw", return_value=dict(gameTime=12)), \ + patch.object(audit, "replay_entity_storage", return_value=dict(status="PASS", matchedEntities=4, + snapshotSha256="A" * 64, auditedDimensions=1)): + result = audit.native_controls(root, "dimensions", recorded) + self.assertEqual(6, len(result)) + + def test_wrong_dimension_still_fails(self): + document = ready("minecraft:the_nether", "conversion") + self.assertIs(document, require_ready(document, BOUNDS, "entities-conversion", "minecraft:the_nether")) + for key in ("minecraft:overworld", "minecraft:the_end"): + with self.subTest(key=key), self.assertRaises(AssertionError): + require_ready(document, BOUNDS, "entities-conversion", key) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_endurance.py b/test-rig/migration/test_native117_endurance.py new file mode 100644 index 00000000..ab259edb --- /dev/null +++ b/test-rig/migration/test_native117_endurance.py @@ -0,0 +1,195 @@ +"""Native transport guards; historical material/energy oracles stay inherited.""" + +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import nbtlib +import endurance +from matrix import WORLD, fingerprint, write_json +from native117_endurance import (FRESH_FIXTURE_SEED, NativeEnduranceRig, audit, + configure_new_fresh_fixture, prove_fresh_fixture_before_build, + verify_fresh_fixture_proof, world_info) + + +class NativeEnduranceTests(unittest.TestCase): + def test_batch_and_conservation_oracles_are_inherited(self): + self.assertIsNot(NativeEnduranceRig.batch, endurance.Rig.batch) + self.assertIs(NativeEnduranceRig.invariants, endurance.Rig.invariants) + self.assertIs(NativeEnduranceRig.inventory, endurance.Rig.inventory) + self.assertIs(NativeEnduranceRig.seed, endurance.Rig.seed) + self.assertIs(NativeEnduranceRig.structure, endurance.Rig.structure) + + def test_independent_failure_is_retained_and_cannot_reach_a_passing_ledger(self): + with tempfile.TemporaryDirectory() as directory: + rig = NativeEnduranceRig("test", Path(directory), None, None, {}) + rig.documents = {station: {} for station in ("S-04", "E-03", "W-04", "W-03")} + with patch("semantic_machine_audit.endurance_batch", return_value={"status": "FAIL", "failures": ["lost filter"]}), \ + patch.object(endurance.Rig, "batch") as historical: + with self.assertRaisesRegex(AssertionError, "Independent batch"): + rig.batch("cycle-001") + historical.assert_not_called() + self.assertEqual("FAIL", __import__("json").loads((Path(directory) / "semantic-batch-cycle-001.json").read_text())["status"]) + + def test_independent_pass_keeps_the_historical_material_checks(self): + with tempfile.TemporaryDirectory() as directory: + rig = NativeEnduranceRig("test", Path(directory), None, None, {}) + rig.documents = {station: {} for station in ("S-04", "E-03", "W-04", "W-03")} + with patch("semantic_machine_audit.endurance_batch", return_value={"status": "PASS", "failures": []}), \ + patch.object(endurance.Rig, "batch", return_value={"retained": True}) as historical: + self.assertEqual({"retained": True}, rig.batch("cycle-001")) + historical.assert_called_once_with("cycle-001", power="basepower") + + def test_restart_selects_first_tick_without_output_or_inventory_inspection(self): + with tempfile.TemporaryDirectory() as directory: + definitions = {s: dict(origin=[0, 0, 0], boundsMin=[0, 0, 0], boundsMax=[31, 20, 31]) + for s in endurance.CHECKED} + rig = NativeEnduranceRig("test", Path(directory), None, None, definitions) + ticks = iter([200, 200, 201]) + observations, commands = [], [] + def checkpoint(label): + observations.append(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"): + rig.boot("endurance-restart") + self.assertEqual(len(observations), 3) + self.assertEqual(len(commands), len(endurance.CHECKED)) + self.assertTrue(all(c == "forceload add 0 0 31 31" for c in commands)) + self.assertTrue((Path(directory) / "native-restart-readiness.json").is_file()) + + def test_restart_without_measured_tick_fails(self): + with tempfile.TemporaryDirectory() as directory: + definitions = {s: dict(origin=[0, 0, 0], boundsMin=[0, 0, 0], boundsMax=[31, 20, 31]) + for s in endurance.CHECKED} + 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 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: + rig.boot("endurance") + server.assert_called_once_with(rig.runtime, rig.java, rig.launcher, 'endurance') + + def test_smoke_result_never_qualifies_as_endurance(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + write_json(root / "native117-endurance-results.json", dict(status="SMOKE_PASS_NOT_ENDURANCE")) + with self.assertRaisesRegex(AssertionError, "No completed native endurance"): + audit(root) + + def test_missing_native_profile_fails(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + write_json(root / "native117-endurance-results.json", dict( + status="PASS", duration_seconds=1800, interval_seconds=60, + source_fixture_unchanged=True, helpers_unchanged=True, profiles={})) + with self.assertRaisesRegex(AssertionError, "Missing native environment"): + audit(root) + + +class FreshFixtureIsolationTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.runtime = Path(self.temporary.name) + (self.runtime / "server.properties").write_text( + "max-tick-time=60000\nserver-port=25590\nlevel-seed=old\n" + "generate-structures=true\ngenerate-structures=true\n", encoding="utf-8") + + def level(self, features=0, seed=FRESH_FIXTURE_SEED): + path = self.runtime / WORLD / "level.dat" + path.parent.mkdir(exist_ok=True) + dimensions = nbtlib.Compound({key: nbtlib.Compound({"type": nbtlib.String(key), + "generator": nbtlib.Compound({"type": nbtlib.String("minecraft:flat"), + "settings": nbtlib.Compound()})}) + for key in ("minecraft:overworld", "minecraft:the_nether", "minecraft:the_end")}) + nbtlib.File({"Data": nbtlib.Compound({"WorldGenSettings": nbtlib.Compound({ + "seed": nbtlib.Long(seed), "generate_features": nbtlib.Byte(features), + "bonus_chest": nbtlib.Byte(0), "dimensions": dimensions})})}, + gzipped=True).save(path) + return path + + def rig(self, block="minecraft:air"): + rig = NativeEnduranceRig("test", self.runtime, None, None, {}) + def snapshot(label): + write_json(self.runtime / f"migration-{label}.json", [dict( + id="poi-target", x=-126, y=5, z=27, actualName=block, properties={})]) + rig.server = type("FakeServer", (), {"snapshot": staticmethod(snapshot)})() + rig.command = lambda command, response=None: self.assertEqual((command, response), + ("save-all flush", "Saved the game")) + return rig + + def test_new_world_only_changes_two_creation_settings_and_keeps_watchdog(self): + proof = configure_new_fresh_fixture(self.runtime) + text = (self.runtime / "server.properties").read_text() + self.assertTrue(proof["existing_world_absent"]) + self.assertEqual(text.count("generate-structures="), 1) + self.assertEqual(text.count("level-seed="), 1) + self.assertIn("generate-structures=false\n", text) + self.assertIn(f"level-seed={FRESH_FIXTURE_SEED}\n", text) + self.assertIn("max-tick-time=60000\nserver-port=25590\n", text) + + def test_existing_imported_world_is_refused_before_any_write(self): + self.level(features=1, seed=42) + before = fingerprint(self.runtime) + with self.assertRaisesRegex(AssertionError, "existing/imported world"): + configure_new_fresh_fixture(self.runtime) + self.assertEqual(fingerprint(self.runtime), before) + self.assertEqual(world_info(self.runtime / WORLD / "level.dat")["MapFeatures"], 1) + + def test_actual_seed_mapfeatures_and_air_proof_pass_and_are_replayable(self): + configuration = configure_new_fresh_fixture(self.runtime) + self.level() + proof = prove_fresh_fixture_before_build(self.rig(), configuration) + self.assertEqual(proof["status"], "PASS") + self.assertEqual(verify_fresh_fixture_proof(self.runtime, proof), + dict(MapFeatures=0, RandomSeed=FRESH_FIXTURE_SEED, generatorName="flat")) + self.assertEqual(proof["critical_block"]["actualName"], "minecraft:air") + + def test_property_false_does_not_substitute_for_actual_worldinfo_zero(self): + configuration = configure_new_fresh_fixture(self.runtime) + self.level(features=1) + with self.assertRaisesRegex(AssertionError, "MapFeatures is not zero"): + prove_fresh_fixture_before_build(self.rig(), configuration) + + def test_a_different_seed_is_rejected_even_with_structures_disabled(self): + configuration = configure_new_fresh_fixture(self.runtime) + self.level(seed=123) + with self.assertRaisesRegex(AssertionError, "seed changed"): + prove_fresh_fixture_before_build(self.rig(), configuration) + + def test_composter_is_rejected_even_with_correct_seed_and_worldinfo(self): + configuration = configure_new_fresh_fixture(self.runtime) + self.level() + with self.assertRaisesRegex(AssertionError, "not AIR"): + prove_fresh_fixture_before_build(self.rig("minecraft:composter"), configuration) + + def test_missing_guard_and_tampered_raw_evidence_fail(self): + configuration = configure_new_fresh_fixture(self.runtime) + self.level() + proof = prove_fresh_fixture_before_build(self.rig(), configuration) + with self.assertRaisesRegex(AssertionError, "new-world isolation guard"): + verify_fresh_fixture_proof(self.runtime, dict(proof, existing_world_absent=False)) + (self.runtime / "migration-fresh-isolation-before-build.json").write_text("[]") + with self.assertRaisesRegex(AssertionError, "evidence changed"): + verify_fresh_fixture_proof(self.runtime, proof) + + def test_no_new_run_can_qualify_without_fresh_isolation_proof(self): + write_json(self.runtime / "native117-endurance-results.json", dict( + status="PASS", duration_seconds=1800, interval_seconds=60, + source_fixture_unchanged=True, helpers_unchanged=True, + profiles={"fresh117": {}, "retained-upgrade-native117": {}})) + with self.assertRaisesRegex(AssertionError, "Missing fresh isolation proof"): + audit(self.runtime) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_endurance_archive.py b/test-rig/migration/test_native117_endurance_archive.py new file mode 100644 index 00000000..d0f471b1 --- /dev/null +++ b/test-rig/migration/test_native117_endurance_archive.py @@ -0,0 +1,42 @@ +from pathlib import Path +import tempfile +import unittest + +from audit_native117_endurance import retained_protocol +from matrix import digest + + +class EnduranceProtocolArchive(unittest.TestCase): + def setUp(self): + temp = tempfile.TemporaryDirectory() + self.addCleanup(temp.cleanup) + self.root = Path(temp.name) + self.folder = self.root / 'protocol-sources' + self.folder.mkdir() + self.path = self.folder / 'native117_commands.py' + self.path.write_bytes(b'original command protocol\n') + self.hashes = {str(self.root / 'working-copy/native117_commands.py'): digest(self.path)} + + def test_archived_bytes_remain_auditable_when_working_copy_is_absent(self): + self.assertEqual({'native117_commands.py': self.path}, retained_protocol(self.root, self.hashes)) + + def test_changed_archive_cannot_be_replaced_by_a_summary_claim(self): + self.path.write_bytes(b'changed protocol') + with self.assertRaisesRegex(AssertionError, 'Archived protocol source changed'): + retained_protocol(self.root, self.hashes) + + def test_missing_extra_or_ambiguous_inputs_fail(self): + self.path.unlink() + with self.assertRaises(AssertionError): retained_protocol(self.root, self.hashes) + self.path.write_bytes(b'original command protocol\n') + (self.folder / 'unlisted.py').write_bytes(b'additional') + with self.assertRaisesRegex(AssertionError, 'inventory differs'): + retained_protocol(self.root, self.hashes) + (self.folder / 'unlisted.py').unlink() + self.hashes[str(self.root / 'other/native117_commands.py')] = digest(self.path) + with self.assertRaisesRegex(AssertionError, 'Ambiguous'): + retained_protocol(self.root, self.hashes) + + +if __name__ == '__main__': + unittest.main() diff --git a/test-rig/migration/test_native117_entity_storage.py b/test-rig/migration/test_native117_entity_storage.py new file mode 100644 index 00000000..07c3fac1 --- /dev/null +++ b/test-rig/migration/test_native117_entity_storage.py @@ -0,0 +1,407 @@ +"""Synthetic strict-reader controls only; these never claim native runtime qualification.""" + +import base64 +import copy +import gzip +import hashlib +import io +import json +from pathlib import Path +import struct +import tempfile +import unittest +from unittest.mock import patch +import zlib + +import nbtlib as n + +import native117_entity_storage as storage + + +def entity(words=(1, 2, 3, 4), pos=(8.5, 80.0, 8.5), identity="minecraft:chest_minecart"): + return n.Compound(id=n.String(identity), UUID=n.IntArray(words), Pos=n.List[n.Double](pos), + Motion=n.List[n.Double]([0, 0, 0]), Rotation=n.List[n.Float]([0, 0]), + Items=n.List[n.Compound]([n.Compound(id=n.String("minecraft:dirt"), Count=n.Byte(7), Slot=n.Byte(2), + tag=n.Compound(Private=n.String("retain exactly"), Damage=n.Int(17)))]), + ForgeData=n.Compound(AdvWorksActor=n.String("cart"), Private=n.List[n.Int]([9, 8, 7]))) + + +def encoded(value): + stream = io.BytesIO() + n.File(value).write(stream) + return stream.getvalue() + + +def entry(value, **extra): + return dict(nbtBase64=base64.b64encode(gzip.compress(encoded(value))).decode("ascii"), **extra) + + +def root(coordinates, entities): + return n.Compound(DataVersion=n.Int(2730), Position=n.IntArray(coordinates), Entities=n.List[n.Compound](entities)) + + +def payload(value, compression=2): + raw = encoded(value) + data = gzip.compress(raw) if compression == 1 else zlib.compress(raw) if compression == 2 else raw + return struct.pack(">I", len(data) + 1) + bytes([compression]) + data + + +def put(world, coordinates, entities=None, value=None, compression=2, raw=None): + cx, cz = coordinates + directory = world / "entities" + directory.mkdir(parents=True, exist_ok=True) + path = directory / ("r.%d.%d.mca" % (cx // 32, cz // 32)) + existing = {} + if path.exists(): + existing = dict(storage._payloads(path.read_bytes(), cx // 32, cz // 32)) + existing[coordinates] = raw if raw is not None else payload(value if value is not None else root(coordinates, entities), compression) + header, body = bytearray(8192), bytearray() + offset = 2 + for (x, z), data in sorted(existing.items()): + slot = (x & 31) + (z & 31) * 32 + count = (len(data) + 4095) // 4096 + header[slot * 4:slot * 4 + 4] = ((offset << 8) | count).to_bytes(4, "big") + body += data + bytes(count * 4096 - len(data)) + offset += count + path.write_bytes(header + body) + return path, existing[coordinates] + + +class NativeEntityStorageTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.base = Path(self.temp.name) + self.world = self.base / "World" + self.world.mkdir() + (self.world / "level.dat").write_bytes(b"unrelated protected world data") + self.actor = entity() + self.record = {"entities": [entry(self.actor)], "frozen": True} + self.index = 0 + + def audit(self, record=None, world=None): + self.index += 1 + self.evidence = self.base / ("evidence-" + str(self.index)) + return storage.audit(record if record is not None else self.record, world or self.world, self.evidence) + + def test_exact_payloads_unknown_nbt_and_world_bytes_are_retained(self): + path, raw = put(self.world, (0, 0), [self.actor]) + before = {p.relative_to(self.world).as_posix(): p.read_bytes() for p in self.world.rglob("*") if p.is_file()} + record = copy.deepcopy(self.record) + receipt = self.audit() + self.assertEqual("PASS", receipt["status"]) + self.assertEqual("EXACT_STOPPED_ENTITY_NBT_ONLY", receipt["scope"]) + self.assertEqual(1, receipt["matchedEntities"]) + self.assertEqual(raw, (self.evidence / "chunks/0.0.chunk").read_bytes()) + self.assertEqual(path.read_bytes(), (self.evidence / "regions/r.0.0.mca").read_bytes()) + for name, checksum in receipt["files"].items(): + self.assertEqual(hashlib.sha256((self.evidence / name).read_bytes()).hexdigest().upper(), checksum) + self.assertEqual(before, {p.relative_to(self.world).as_posix(): p.read_bytes() for p in self.world.rglob("*") if p.is_file()}) + self.assertEqual(record, self.record) + + def test_actual_negative_saved_pos_not_fixture_coordinates_selects_chunk(self): + self.actor["Pos"] = n.List[n.Double]([-0.25, 80, -512.5]) + self.record = {"entities": [entry(self.actor)], "definition": {"x": 1000, "z": 1000}} + put(self.world, (-1, -33), [self.actor]) + receipt = self.audit() + self.assertEqual([[-1, -33]], [row["chunk"] for row in receipt["matched"].values()]) + self.assertIn("chunks/-1.-33.chunk", receipt["files"]) + + 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 + put(world, (0, 0), [self.actor]) + record = dict(entities=[entry(self.actor)], worldDimensionKey=key, dimension=identity) + with self.subTest(dimension=key): + self.assertEqual(identity, self.audit(record, world)["dimension"]) + + def test_dimension_record_cannot_be_checked_against_the_wrong_world(self): + put(self.world, (0, 0), [self.actor]) + with self.assertRaisesRegex(AssertionError, "explicit dimension"): + self.audit(dict(entities=[entry(self.actor)], worldDimensionKey="minecraft:the_nether")) + + def test_recursive_passenger_and_separate_child_record_match_the_stored_root(self): + child = entity((5, 6, 7, 8), pos=(16.25, 80, 8.5), identity="minecraft:pig") + self.actor["Passengers"] = n.List[n.Compound]([child]) + put(self.world, (0, 0), [self.actor]) + receipt = self.audit(dict(entities=[entry(self.actor), entry(child)])) + self.assertEqual(2, receipt["matchedEntities"]) + self.assertEqual(2, receipt["snapshotRecords"]) + self.assertEqual({(0, 0)}, {tuple(row["chunk"]) for row in receipt["matched"].values()}) + + def test_overlapping_child_observations_must_have_identical_full_nbt(self): + child = entity((5, 6, 7, 8), identity="minecraft:pig") + self.actor["Passengers"] = n.List[n.Compound]([child]) + put(self.world, (0, 0), [self.actor]) + wrong = copy.deepcopy(child) + wrong["Items"][0]["Count"] = n.Byte(6) + with self.assertRaisesRegex(AssertionError, "overlapping snapshot"): + self.audit(dict(entities=[entry(self.actor), entry(wrong)])) + + def test_duplicate_raw_uuid_in_a_chunk_or_another_region_fails(self): + for other_region in (False, True): + put(self.world, (0, 0), [self.actor] if other_region else [self.actor, copy.deepcopy(self.actor)]) + if other_region: + other = copy.deepcopy(self.actor) + other["Pos"] = n.List[n.Double]([520, 80, 8]) + put(self.world, (32, 0), [other]) + with self.subTest(other_region=other_region), self.assertRaisesRegex(AssertionError, "Duplicate UUID"): + self.audit() + + def test_duplicate_passenger_uuid_fails(self): + raw = copy.deepcopy(self.actor) + raw["Passengers"] = n.List[n.Compound]([copy.deepcopy(self.actor)]) + put(self.world, (0, 0), [raw]) + with self.assertRaisesRegex(AssertionError, "Duplicate UUID"): + self.audit() + + def test_unrelated_entities_are_counted_not_claimed_as_witnesses(self): + extra = entity((5, 6, 7, 8), identity="minecraft:pig") + put(self.world, (0, 0), [self.actor, extra]) + receipt = self.audit() + self.assertEqual((1, 2, 1), (receipt["matchedEntities"], receipt["rawEntities"], receipt["unrelatedEntities"])) + + def test_inventory_quantity_and_unknown_keys_are_not_ignored(self): + mutations = [lambda value: value["Items"][0].update(Count=n.Byte(6)), + lambda value: value["Items"][0]["tag"].update(Private=n.String("changed")), + lambda value: value["ForgeData"].update(Unknown=n.Int(99)), + lambda value: value.pop("ForgeData"), + lambda value: value["Items"].clear()] + for mutate in mutations: + raw = copy.deepcopy(self.actor) + mutate(raw) + put(self.world, (0, 0), [raw]) + with self.subTest(mutate=mutate), self.assertRaisesRegex(AssertionError, "Complete saved entity NBT"): + self.audit() + self.assertEqual("FAIL", json.loads((self.evidence / "receipt.json").read_text())["status"]) + + def test_missing_passengers_and_changed_passenger_inventory_fail(self): + child = entity((5, 6, 7, 8), identity="minecraft:pig") + self.actor["Passengers"] = n.List[n.Compound]([child]) + self.record = dict(entities=[entry(self.actor)]) + for remove in (True, False): + raw = copy.deepcopy(self.actor) + if remove: + raw.pop("Passengers") + else: + raw["Passengers"][0]["Items"][0]["Count"] = n.Byte(6) + put(self.world, (0, 0), [raw]) + with self.subTest(remove=remove), self.assertRaisesRegex(AssertionError, "Complete saved entity NBT"): + self.audit() + + def test_nested_nbt_numeric_tag_types_remain_exact(self): + raw = copy.deepcopy(self.actor) + raw["Items"][0]["Count"] = n.Short(7) + put(self.world, (0, 0), [raw]) + with self.assertRaisesRegex(AssertionError, "Complete saved entity NBT"): + self.audit() + + def test_only_existing_finite_dimension_and_owner_encodings_can_differ(self): + raw = copy.deepcopy(self.actor) + raw["Dimension"] = n.String("minecraft:overworld") + raw["Owner"] = n.IntArray([5, 6, 7, 8]) + snapshot = copy.deepcopy(raw) + snapshot["Dimension"] = n.Int(0) + most, least = storage.uuid_longs([5, 6, 7, 8]) + snapshot.pop("Owner") + snapshot["OwnerUUID"] = n.String(str(storage.uuid.UUID(int=((most & ((1 << 64) - 1)) << 64) | (least & ((1 << 64) - 1))))) + put(self.world, (0, 0), [raw]) + self.assertEqual("PASS", self.audit(dict(entities=[entry(snapshot)]))["status"]) + + def test_native_uuid_shape_and_entity_namespaced_ids_are_required(self): + for key, value in (("UUID", n.IntArray([1, 2, 3])), ("UUID", n.List[n.Int]([1, 2, 3, 4])), + ("id", n.String("Chest")), ("id", n.String("Minecraft:pig"))): + raw = copy.deepcopy(self.actor) + raw[key] = value + put(self.world, (0, 0), [raw]) + with self.subTest(key=key, value=value), self.assertRaises(ValueError): + self.audit() + + def test_old_uuid_longs_are_not_native_storage(self): + raw = copy.deepcopy(self.actor) + raw.pop("UUID") + raw.update(UUIDMost=n.Long(1), UUIDLeast=n.Long(2)) + put(self.world, (0, 0), [raw]) + with self.assertRaisesRegex(ValueError, "native int"): + self.audit() + + def test_data_version_position_types_coordinates_and_unknown_root_keys_fail(self): + cases = [dict(DataVersion=n.Int(2724)), dict(DataVersion=n.Long(2730)), + dict(Position=n.IntArray([0])), dict(Position=n.List[n.Int]([0, 0])), + dict(Position=n.IntArray([1, 0])), dict(Unknown=n.Int(3)), dict(Level=n.Compound())] + for replacement in cases: + value = root((0, 0), [self.actor]) + value.update(replacement) + put(self.world, (0, 0), value=value) + with self.subTest(replacement=replacement), self.assertRaises(ValueError): + self.audit() + + def test_entities_and_passengers_must_be_compound_lists(self): + for value in (n.String("none"), n.List[n.String](["not an entity"])): + put(self.world, (0, 0), value=n.Compound(DataVersion=n.Int(2730), Position=n.IntArray([0, 0]), Entities=value)) + with self.assertRaisesRegex(ValueError, "Entities must"): + self.audit() + raw = copy.deepcopy(self.actor) + raw["Passengers"] = n.String("none") + put(self.world, (0, 0), [raw]) + with self.assertRaisesRegex(ValueError, "Passengers must"): + self.audit() + + def test_nonfinite_and_malformed_saved_positions_fail(self): + for value in (n.List[n.Double]([float("nan"), 80, 0]), n.List[n.Double]([0, 80, float("inf")]), + n.List[n.Float]([0, 80, 0]), n.List[n.Double]([0, 80])): + raw = copy.deepcopy(self.actor) + raw["Pos"] = value + put(self.world, (0, 0), [raw]) + with self.subTest(value=value), self.assertRaisesRegex(ValueError, "finite native doubles"): + self.audit() + + def test_root_saved_pos_must_match_the_actual_region_slot(self): + raw = copy.deepcopy(self.actor) + raw["Pos"] = n.List[n.Double]([32, 80, 8]) + put(self.world, (0, 0), [raw]) + with self.assertRaisesRegex(AssertionError, "saved Pos differs"): + self.audit() + + def test_empty_storage_and_moved_snapshot_cannot_pass_vacuously(self): + put(self.world, (0, 0), []) + with self.assertRaisesRegex(AssertionError, "absent from the chunk"): + self.audit() + put(self.world, (0, 0), [self.actor]) + moved = copy.deepcopy(self.actor) + moved["Pos"] = n.List[n.Double]([520, 80, 8]) + with self.assertRaisesRegex(AssertionError, "actual saved Pos"): + self.audit(dict(entities=[entry(moved)])) + + def test_actual_snapshot_required_not_expected_projection(self): + put(self.world, (0, 0), [self.actor]) + for value in ({"entities": []}, {"entities": [{"UUIDMost": 1}]}, {"entities": {"cart": {}}}): + with self.subTest(value=value), self.assertRaises(ValueError): + self.audit(value) + + def test_snapshot_uuid_metadata_duplicates_and_missing_vehicle_root_fail(self): + put(self.world, (0, 0), [self.actor]) + with self.assertRaisesRegex(AssertionError, "attestation"): + self.audit(dict(entities=[entry(self.actor, uuid="wrong")])) + with self.assertRaisesRegex(AssertionError, "Duplicate snapshot UUID"): + self.audit(dict(entities=[entry(self.actor), entry(self.actor)])) + with self.assertRaisesRegex(ValueError, "vehicle root"): + self.audit(dict(entities=[entry(self.actor, vehicleUuid="missing-vehicle")])) + + def test_all_native_region_compression_forms(self): + for compression in (1, 2, 3): + put(self.world, (0, 0), [self.actor], compression=compression) + with self.subTest(compression=compression): + self.assertEqual("PASS", self.audit()["status"]) + + def test_corrupt_compression_trailing_nbt_and_external_chunks_fail(self): + for raw in (struct.pack(">I", 5) + b"\x82data", struct.pack(">I", 5) + b"\x02data", + struct.pack(">I", len(encoded(root((0, 0), [self.actor]))) + 2) + b"\x03" + encoded(root((0, 0), [self.actor])) + b"x"): + put(self.world, (0, 0), raw=raw) + with self.subTest(raw=raw[:6]), self.assertRaises((ValueError, zlib.error)): + self.audit() + + def test_decompression_limit_fails_closed(self): + put(self.world, (0, 0), [self.actor]) + with patch.object(storage, "MAX_NBT_BYTES", 32), self.assertRaises(ValueError): + self.audit() + + def test_empty_native_lists_in_unrelated_chunks_are_supported(self): + put(self.world, (0, 0), [self.actor]) + for entities in (n.List[n.Compound](), n.List[n.End]()): + put(self.world, (1, 0), value=n.Compound(DataVersion=n.Int(2730), Position=n.IntArray([1, 0]), Entities=entities)) + self.assertEqual("PASS", self.audit()["status"]) + + def test_native_zero_byte_read_probe_regions_are_retained_as_empty(self): + put(self.world, (0, 0), [self.actor]) + empty = self.world / "entities/r.-1.-1.mca" + empty.write_bytes(b"") + receipt = self.audit() + self.assertEqual("PASS", receipt["status"]) + self.assertEqual(1, receipt["matchedEntities"]) + self.assertEqual(hashlib.sha256(b"").hexdigest().upper(), receipt["sourceFiles"][empty.name]) + self.assertEqual(b"", (self.evidence / "regions" / empty.name).read_bytes()) + self.assertEqual(["chunks/0.0.chunk"], list(receipt["chunks"])) + + def test_empty_read_probe_cannot_satisfy_an_entity_witness(self): + directory = self.world / "entities" + directory.mkdir() + (directory / "r.0.0.mca").write_bytes(b"") + with self.assertRaisesRegex(AssertionError, "absent from the chunk"): + self.audit() + receipt = json.loads((self.evidence / "receipt.json").read_text()) + self.assertEqual("FAIL", receipt["status"]) + self.assertEqual({}, receipt["matched"]) + self.assertEqual({}, receipt["chunks"]) + + def test_nonempty_short_headers_and_unpadded_regions_remain_invalid(self): + for data in (b"\x00", bytes(4096), bytes(8191), bytes(8193)): + with self.subTest(length=len(data)), self.assertRaisesRegex(ValueError, "region length"): + list(storage._payloads(data, 0, 0)) + + def test_duplicate_nbt_keys_cannot_be_silently_overwritten(self): + data = encoded(root((0, 0), [self.actor])) + duplicate = b"\x03\x00\x0bDataVersion" + struct.pack(">i", 2730) + malformed = data[:-1] + duplicate + b"\x00" + raw = struct.pack(">I", len(malformed) + 1) + b"\x03" + malformed + put(self.world, (0, 0), raw=raw) + with self.assertRaisesRegex(ValueError, "round-trip exactly"): + self.audit() + + def test_overlapping_out_of_bounds_and_header_sector_allocations_fail(self): + for location in ((1 << 8) | 1, (99 << 8) | 1, 1): + path, _ = put(self.world, (0, 0), [self.actor]) + data = bytearray(path.read_bytes()) + data[:4] = location.to_bytes(4, "big") + path.write_bytes(data) + with self.assertRaisesRegex(ValueError, "sector allocation"): + self.audit() + path.unlink() + path, _ = put(self.world, (0, 0), [self.actor]) + data = bytearray(path.read_bytes()) + data[4:8] = data[:4] + path.write_bytes(data) + with self.assertRaisesRegex(ValueError, "Overlapping"): + self.audit() + + def test_truncated_region_and_invalid_chunk_length_fail(self): + path, _ = put(self.world, (0, 0), [self.actor]) + data = path.read_bytes() + path.write_bytes(data[:-1]) + with self.assertRaisesRegex(ValueError, "region length"): + self.audit() + broken = bytearray(data) + broken[8192:8196] = (99999).to_bytes(4, "big") + path.write_bytes(broken) + with self.assertRaisesRegex(ValueError, "payload length"): + self.audit() + + def test_source_changes_during_audit_are_not_accepted_as_stopped_evidence(self): + path, _ = put(self.world, (0, 0), [self.actor]) + original = storage._semantic + calls = 0 + + def concurrent_write(value): + nonlocal calls + calls += 1 + if calls == 2: + path.write_bytes(path.read_bytes() + bytes(4096)) + return original(value) + + with patch.object(storage, "_semantic", side_effect=concurrent_write), \ + self.assertRaisesRegex(AssertionError, "changed during"): + self.audit() + + def test_evidence_cannot_be_in_world_or_overwrite_an_earlier_run(self): + put(self.world, (0, 0), [self.actor]) + with self.assertRaisesRegex(ValueError, "disjoint"): + storage.audit(self.record, self.world, self.world / "evidence") + self.audit() + receipt = (self.evidence / "receipt.json").read_bytes() + with self.assertRaises(FileExistsError): + storage.audit(self.record, self.world, self.evidence) + self.assertEqual(receipt, (self.evidence / "receipt.json").read_bytes()) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_entity_storage_replay.py b/test-rig/migration/test_native117_entity_storage_replay.py new file mode 100644 index 00000000..bde7f8bd --- /dev/null +++ b/test-rig/migration/test_native117_entity_storage_replay.py @@ -0,0 +1,312 @@ +"""Synthetic replay/tamper controls only. No native server or source fixture is used.""" + +import copy +import hashlib +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import nbtlib as n + +from matrix import WORLD +import native117_entity_storage as native +import native117_stopped_entities as stopped +import native117_entity_storage_replay as reader +from test_native117_entity_storage import entity, entry, put, root + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest().upper() + + +def read(path): + return json.loads(path.read_text()) + + +def write(path, value): + path.write_text(json.dumps(value, sort_keys=True, indent=2) + "\n") + + +class RetainedEntityReplayTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.base = Path(self.temp.name) + self.runtime = self.base / "runtime" + self.world = self.runtime / WORLD + self.world.mkdir(parents=True) + (self.runtime / "advantage-migration.allow").write_text("synthetic fixture only") + self.evidence = self.base / "retained" + self.actor = entity() + self.document = dict(format=1, status="CAPTURED", phase="FMLServerStoppingEvent", minecraftVersion="1.17.1", + dataVersion=2730, worldName=WORLD, worldDirectory=str(self.world), bootId="12345678-1234-1234-1234-123456789abc", + enumeration="ServerLevel.getAllEntities", entityCount=0, dimensions={}) + for key, (dimension, _) in stopped.DIMENSIONS.items(): + self.document["dimensions"][key] = dict(worldDimensionKey=key, dimension=dimension, gameTime=20, entityCount=0, entities=[]) + + def stage(self, key="minecraft:overworld", actor=None): + actor = actor if actor is not None else self.actor + row = entry(actor, uuid=native._identity(actor), id=str(actor["id"]), worldDimensionKey=key) + self.document["dimensions"][key]["entities"].append(row) + self.document["dimensions"][key]["entityCount"] += 1 + self.document["entityCount"] += 1 + put(self.world / stopped.DIMENSIONS[key][1], native._chunk(actor), [actor]) + + def retain(self): + write(self.runtime / stopped.SNAPSHOT, self.document) + return stopped.audit_stopped(self.runtime, self.evidence) + + def reseal(self, prefix="overworld", key="minecraft:overworld"): + """Forge coherent claimed hashes without repairing semantic truth.""" + folder = self.evidence / prefix + child = read(folder / "receipt.json") + child["files"] = {p.relative_to(folder).as_posix(): sha(p) for p in folder.rglob("*") if p.is_file() and p.name != "receipt.json"} + child["sourceFiles"] = {p.name: sha(p) for p in (folder / "regions").glob("*.mca")} + child["chunks"] = {} + for path in (folder / "regions").glob("*.mca"): + rx, rz = map(int, path.name.split(".")[1:3]) + for coordinates, payload in native._payloads(path.read_bytes(), rx, rz): + name = "chunks/%d.%d.chunk" % coordinates + if (folder / name).exists(): + child["chunks"][name] = dict(position=list(coordinates), source=path.name, sha256=sha(folder / name)) + write(folder / "receipt.json", child) + parent = read(self.evidence / "receipt.json") + parent["dimensions"][key] = child + parent["snapshotSha256"] = sha(self.evidence / stopped.SNAPSHOT) + write(self.evidence / "receipt.json", parent) + + def replace_raw(self, entities, prefix="overworld", key="minecraft:overworld", version=2730): + scratch = self.base / ("synthetic-replacement-" + prefix) + value = root((0, 0), entities) + value["DataVersion"] = n.Int(version) + path, _ = put(scratch, (0, 0), value=value) + folder = self.evidence / prefix + (folder / "regions" / path.name).write_bytes(path.read_bytes()) + for coordinates, payload in native._payloads(path.read_bytes(), 0, 0): + (folder / ("chunks/%d.%d.chunk" % coordinates)).write_bytes(payload) + self.reseal(prefix, key) + + def test_replays_complete_semantics_and_hashes_every_file_without_writing(self): + self.stage() + proof = self.retain() + before = {p.relative_to(self.evidence).as_posix(): p.read_bytes() for p in self.evidence.rglob("*") if p.is_file()} + with patch.object(native, "audit", side_effect=AssertionError("Replay must never restage/write a world")), \ + patch.object(native, "_entity_chunk", wraps=native._entity_chunk) as parse: + result = reader.replay(self.evidence, expected_snapshot_sha256=proof["snapshotSha256"], + expected_receipt_sha256=sha(self.evidence / "receipt.json")) + self.assertEqual("PASS", result["status"]) + self.assertEqual(1, parse.call_count) + self.assertEqual((1, 1, 1), (result["auditedDimensions"], result["snapshotRecords"], result["matchedEntities"])) + self.assertEqual(set(before), set(result["files"])) + self.assertIn("receipt.json", result["files"]) + self.assertIn("overworld/receipt.json", result["files"]) + self.assertEqual(before, {p.relative_to(self.evidence).as_posix(): p.read_bytes() for p in self.evidence.rglob("*") if p.is_file()}) + + def test_does_not_read_or_require_the_original_runtime(self): + self.stage() + self.retain() + destination = self.base / "original-runtime-no-longer-at-recorded-path" + assert self.runtime.resolve().is_relative_to(self.base.resolve()) + assert destination.resolve().is_relative_to(self.base.resolve()) + self.runtime.rename(destination) + self.assertEqual("PASS", reader.replay(self.evidence)["status"]) + + def test_retained_native_zero_byte_regions_are_not_missing_witness_proof(self): + self.stage() + (self.world / "entities/r.-1.-1.mca").write_bytes(b"") + self.retain() + result = reader.replay(self.evidence) + self.assertIn("overworld/regions/r.-1.-1.mca", result["files"]) + self.assertEqual(1, result["matchedEntities"]) + self.replace_raw([]) + with self.assertRaisesRegex(AssertionError, "absent from its actual saved Pos"): + reader.replay(self.evidence) + + def test_all_empty_remains_nonqualifying(self): + self.retain() + self.assertEqual("NO_NONEMPTY_DIMENSIONS", reader.replay(self.evidence)["status"]) + + def test_all_three_dimensions_are_rederived(self): + for index, key in enumerate(stopped.DIMENSIONS): + self.stage(key, entity((index + 1, 2, 3, 4))) + self.retain() + self.assertEqual(3, reader.replay(self.evidence)["auditedDimensions"]) + + def test_cross_dimension_uuid_duplicates_fail_after_local_metadata_is_resigned(self): + self.stage() + self.stage("minecraft:the_nether", entity((5, 6, 7, 8))) + self.retain() + document = read(self.evidence / stopped.SNAPSHOT) + key = "minecraft:the_nether" + identity = native._identity(self.actor) + document["dimensions"][key]["entities"] = [entry(self.actor, uuid=identity, + id=str(self.actor["id"]), worldDimensionKey=key)] + write(self.evidence / stopped.SNAPSHOT, document) + write(self.evidence / "DIM-1/snapshot.json", document["dimensions"][key]) + self.replace_raw([self.actor], "DIM-1", key) + path = self.evidence / "DIM-1/receipt.json" + child = read(path) + child["matched"] = {identity: dict(rootUuid=identity, chunk=[0, 0], + payload="chunks/0.0.chunk", separatelyObserved=True)} + write(path, child) + self.reseal("DIM-1", key) + with self.assertRaisesRegex(AssertionError, "across dimensions"): + reader.replay(self.evidence) + + def test_forged_root_counts_fail_even_with_pass_status(self): + self.stage() + self.retain() + path = self.evidence / "receipt.json" + value = read(path) + value["matchedEntities"] = 99 + write(path, value) + with self.assertRaisesRegex(AssertionError, "root receipt"): + reader.replay(self.evidence) + + def test_boolean_or_forged_child_counters_are_not_accepted_as_integers(self): + self.stage() + self.retain() + for value in (True, 99): + child_path = self.evidence / "overworld/receipt.json" + child = read(child_path) + child["snapshotRecords"] = value + write(child_path, child) + self.reseal() + with self.subTest(value=value), self.assertRaisesRegex(AssertionError, "overworld/receipt"): + reader.replay(self.evidence) + + def test_forged_uuid_match_metadata_is_recomputed(self): + self.stage() + self.retain() + path = self.evidence / "overworld/receipt.json" + child = read(path) + next(iter(child["matched"].values()))["chunk"] = [99, 99] + write(path, child) + self.reseal() + with self.assertRaisesRegex(AssertionError, "overworld/receipt"): + reader.replay(self.evidence) + + def test_changed_inventory_nbt_fails_after_all_claimed_hashes_are_resigned(self): + self.stage() + self.retain() + changed = copy.deepcopy(self.actor) + changed["Items"][0]["Count"] = n.Byte(6) + self.replace_raw([changed]) + with self.assertRaisesRegex(AssertionError, "Complete retained entity NBT/type"): + reader.replay(self.evidence) + + def test_motion_drift_is_not_normalized_away(self): + self.stage() + self.retain() + changed = copy.deepcopy(self.actor) + changed["Motion"][1] = n.Double(-0.4) + self.replace_raw([changed]) + with self.assertRaisesRegex(AssertionError, "Complete retained entity NBT/type"): + reader.replay(self.evidence) + + def test_changed_unknown_keys_or_tag_types_cannot_hide_behind_semantic_projection(self): + self.stage() + self.retain() + for count in (n.Short(7), n.Byte(7)): + changed = copy.deepcopy(self.actor) + changed["Items"][0]["Count"] = count + if isinstance(count, n.Byte): + changed["ForgeData"]["Unrecognized"] = n.String("must not be ignored") + self.replace_raw([changed]) + with self.subTest(count=count), self.assertRaisesRegex(AssertionError, "Complete retained entity NBT/type"): + reader.replay(self.evidence) + + def test_passenger_loss_is_rejected(self): + self.actor["Passengers"] = n.List[n.Compound]([entity((5, 6, 7, 8), identity="minecraft:pig")]) + self.stage() + self.retain() + changed = copy.deepcopy(self.actor) + changed.pop("Passengers") + self.replace_raw([changed]) + with self.assertRaisesRegex(AssertionError, "Complete retained entity NBT/type"): + reader.replay(self.evidence) + + def test_duplicate_raw_uuid_fails_even_with_coherent_archive_hashes(self): + self.stage() + self.retain() + self.replace_raw([self.actor, copy.deepcopy(self.actor)]) + with self.assertRaisesRegex(AssertionError, "Duplicate UUID"): + reader.replay(self.evidence) + + def test_native_data_version_is_parsed_not_trusted_from_receipt(self): + self.stage() + self.retain() + self.replace_raw([self.actor], version=2724) + with self.assertRaisesRegex(ValueError, "DataVersion 2730"): + reader.replay(self.evidence) + + def test_serialized_dimension_must_agree_with_storage_dimension(self): + self.stage() + self.retain() + wrong = copy.deepcopy(self.actor) + wrong["Dimension"] = n.String("minecraft:the_nether") + self.replace_raw([wrong]) + with self.assertRaisesRegex(AssertionError, "NBT dimension differs"): + reader.replay(self.evidence) + + def test_missing_payload_is_detected_from_region_even_if_removed_from_manifests(self): + self.stage() + self.retain() + (self.evidence / "overworld/chunks/0.0.chunk").unlink() + self.reseal() + with self.assertRaisesRegex(AssertionError, "Missing retained payload"): + reader.replay(self.evidence) + + def test_payload_cannot_disagree_with_region_even_if_each_has_a_valid_hash(self): + self.stage() + self.retain() + path = self.evidence / "overworld/chunks/0.0.chunk" + path.write_bytes(path.read_bytes() + b"trailing") + self.reseal() + with self.assertRaisesRegex(AssertionError, "byte-for-byte"): + reader.replay(self.evidence) + + def test_nested_snapshot_must_equal_actual_stopping_dimension_record(self): + self.stage() + self.retain() + path = self.evidence / "overworld/snapshot.json" + value = read(path) + value["gameTime"] += 1 + write(path, value) + self.reseal() + with self.assertRaisesRegex(AssertionError, "overworld/snapshot"): + reader.replay(self.evidence) + + def test_external_frozen_pins_are_checked_as_well_as_semantic_truth(self): + self.stage() + self.retain() + for kwargs in (dict(expected_snapshot_sha256="0" * 64), dict(expected_receipt_sha256="0" * 64)): + with self.subTest(kwargs=kwargs), self.assertRaisesRegex(AssertionError, "External frozen evidence pin"): + reader.replay(self.evidence, **kwargs) + + def test_unlisted_files_and_empty_directories_are_not_silently_ignored(self): + self.stage() + self.retain() + extra = self.evidence / "unexpected.txt" + extra.write_text("not part of retained proof") + with self.assertRaisesRegex(AssertionError, "retained files"): + reader.replay(self.evidence) + extra.unlink() + (self.evidence / "unexpected-empty").mkdir() + with self.assertRaisesRegex(AssertionError, "retained directories"): + reader.replay(self.evidence) + + def test_duplicate_receipt_keys_cannot_hide_a_forged_counter(self): + self.stage() + self.retain() + path = self.evidence / "receipt.json" + data = path.read_text() + path.write_text('{"matchedEntities":999,' + data[1:]) + with self.assertRaisesRegex(ValueError, "Duplicate retained JSON key"): + reader.replay(self.evidence) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_format.py b/test-rig/migration/test_native117_format.py new file mode 100644 index 00000000..fa484289 --- /dev/null +++ b/test-rig/migration/test_native117_format.py @@ -0,0 +1,73 @@ +import copy +import unittest + +from native117_format import actor_nbt, decode_palette, dimension_id, uuid_longs, world_info, worldgen_settings + + +class Native117FormatTest(unittest.TestCase): + def test_native_dimensions_are_exact_and_unknown_keys_fail(self): + for identity, key in ((-1, "minecraft:the_nether"), (0, "minecraft:overworld"), (1, "minecraft:the_end")): + self.assertEqual(identity, dimension_id(key)) + for value in (True, "0", "minecraft:nether", "custom:moon", 2): + with self.assertRaises(ValueError): + dimension_id(value) + + def test_uuid_words_retain_every_signed_bit(self): + self.assertEqual((-1, -(1 << 63)), uuid_longs([-1, -1, -(1 << 31), 0])) + for value in ([1, 2, 3], [1, 2, 3, 1 << 31], [True, 0, 0, 0]): + with self.assertRaises(ValueError): + uuid_longs(value) + + def test_actor_adapter_retains_raw_input_and_inventory(self): + value = {"UUID": [0, 1, 0, 2], "Dimension": "minecraft:the_end", "Inventory": [{"id": "poweradvantage:test", "Count": 7}]} + original = copy.deepcopy(value) + actual = actor_nbt(value) + self.assertEqual(original, value) + self.assertEqual((1, 2, 1), (actual["UUIDMost"], actual["UUIDLeast"], actual["Dimension"])) + self.assertEqual(value["Inventory"], actual["Inventory"]) + + def settings(self): + return {"WorldGenSettings": {"seed": 5919679243107101036, "generate_features": False, "bonus_chest": False, + "dimensions": {key: {"type": key, "generator": {"type": "minecraft:flat", "settings": {"layers": []}}} + for key in ("minecraft:overworld", "minecraft:the_nether", "minecraft:the_end")}}} + + def test_worldgen_reads_native_fields_not_stale_legacy_fields(self): + data = self.settings() + data.update(MapFeatures=1, RandomSeed=0, generatorName="wrong") + self.assertEqual({"MapFeatures": 0, "RandomSeed": 5919679243107101036, "generatorName": "flat"}, world_info(data)) + self.assertEqual(data["WorldGenSettings"], worldgen_settings(data)) + + def test_legacy_fields_are_not_native_proof(self): + data = dict(MapFeatures=0, RandomSeed=4, generatorName="flat") + with self.assertRaises(ValueError): + world_info(data) + self.assertEqual(data, world_info(data, allow_legacy=True)) + + def test_missing_native_dimension_fails(self): + data = self.settings() + del data["WorldGenSettings"]["dimensions"]["minecraft:the_end"] + with self.assertRaises(ValueError): + world_info(data) + + def test_padded_palette_boundaries_at_five_seven_and_thirteen_bits(self): + for size, count in ((17, 4096), (65, 4096), (4097, 8192)): + palette = list(range(size)) + bits = max(4, (size - 1).bit_length()) + per_long = 64 // bits + values = [index % size for index in range(count)] + words = [0] * ((count + per_long - 1) // per_long) + for index, value in enumerate(values): + words[index // per_long] |= value << ((index % per_long) * bits) + self.assertEqual(values, decode_palette(palette, words, count)) + + def test_old_cross_long_packing_is_rejected(self): + with self.assertRaises(ValueError): + decode_palette(list(range(17)), [0] * 320) + + def test_missing_palette_index_is_not_clamped(self): + with self.assertRaises(ValueError): + decode_palette(["air", "stone"], [15] * 256) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_geometry.py b/test-rig/migration/test_native117_geometry.py new file mode 100644 index 00000000..9ca00055 --- /dev/null +++ b/test-rig/migration/test_native117_geometry.py @@ -0,0 +1,357 @@ +"""Engine geometry and independent historical-oracle preservation contracts.""" + +import ast +import copy +import hashlib +import json +from pathlib import Path +import unittest +import tempfile +import zipfile + +from historical_test_inputs import (ACTIVE_ADAPTERS, ADVANTAGE117, BASE117_ADAPTERS, + RIG, git_blob, oracle_bytes) +from native117_geometry import (ENGINE_SOURCES, FORGE_VERSION, MCP_VERSION, MINECRAFT_VERSION, + SOURCE_ARCHIVE_SHA256, SOURCE_SHA256, require_geometry, + verify_engine_sources) +from native117_campaign import ACCEPTED_POWER114, REQUIRED, require_routes, require_source114_receipt +from native117_harvest import EXPECTED, require_checks, mark_runtime + + +class GeometryTests(unittest.TestCase): + def test_native_engine_source_pin_is_not_the_historical_engine_relabelled(self): + self.assertEqual((MINECRAFT_VERSION, FORGE_VERSION, MCP_VERSION), + ("1.17.1", "37.1.1", "20210706.113038")) + self.assertEqual(SOURCE_ARCHIVE_SHA256, + "ACB95ACEBB65CE2EE034E887F324F6CC18C7C628D0CF97358F4D2F64572AAA0B") + self.assertEqual(SOURCE_SHA256, + "5BB6FDF6E625CB86C5A7975F686DB89306729C0517D39350BFD88BD77C5EBBD4") + self.assertEqual(ENGINE_SOURCES["net/minecraft/server/level/DistanceManager.java"], SOURCE_SHA256) + self.assertEqual(len(ENGINE_SOURCES), 6) + self.assertEqual(hashlib.sha256(json.dumps(ENGINE_SOURCES, sort_keys=True, + separators=(",", ":")).encode()).hexdigest().upper(), + "EC4EC6A1DAE0006C11EA42311B046E8AF169172343350311871ECBC73FA80E45") + self.assertNotEqual(SOURCE_SHA256, + "A1C8999617D792843C879C04E49F8658C299E5A423B4F44468F8F70961A68104") + + def test_unverified_source_archive_cannot_become_native_proof_by_renaming(self): + with tempfile.TemporaryDirectory() as folder: + archive = Path(folder) / "forge-1.17.1-37.1.1-injected-sources.jar" + with zipfile.ZipFile(archive, "w") as sources: + for name in ENGINE_SOURCES: + sources.writestr(name, "unverified or historical source") + with self.assertRaisesRegex(AssertionError, "source archive differs"): + verify_engine_sources(archive) + + def sample(self): + return dict(ticketGeometry=dict(fullLevel=33, entityTickingLevel=31, maxDistance=11, + lastHolderLevel=44, holderRadius=13, observedForcedLevels=[31])) + + def test_real_forced_ticket_level_and_generation_halo_are_both_required(self): + require_geometry(self.sample(), forced=True) + for key in ("fullLevel", "entityTickingLevel", "maxDistance", "lastHolderLevel", "holderRadius"): + value = self.sample() + value["ticketGeometry"][key] += 1 + with self.subTest(key=key), self.assertRaises(AssertionError): + require_geometry(value) + + def test_empty_tickets_only_allowed_after_release(self): + value = self.sample() + value["ticketGeometry"]["observedForcedLevels"] = [] + require_geometry(value) + with self.assertRaises(AssertionError): + require_geometry(value, forced=True) + + def test_missing_or_historical_assumptions_cannot_count_as_live_geometry(self): + with self.assertRaises(AssertionError): + require_geometry({}) + value = self.sample() + value["ticketGeometry"]["observedForcedLevels"] = [30, 31] + with self.assertRaises(AssertionError): + require_geometry(value) + + +class HarvestTests(unittest.TestCase): + def test_disposable_harvest_runtime_has_both_required_markers(self): + with tempfile.TemporaryDirectory() as folder: + mark_runtime(Path(folder)) + self.assertEqual({p.name for p in Path(folder).iterdir()}, + {"advantage-native-harvest.allow", "advantage-works.allow"}) + + def sample(self): + rows = [] + for name, (level, allowed, silk, minimum, maximum, trials) in EXPECTED.items(): + rows.append(dict(id=name, status="PASS", historicalHarvestLevel=level, harvestAllowed=allowed, + silkTouch=silk, trials=trials, actualDropCounts=[minimum] * trials, + actualSolid=name.startswith(("sulfur/", "lithium/", "wire/")), + fullCollision=not name.startswith("wire/"), + connectionMask=int(name.split("/")[1]) if name.startswith(("wire/", "scaffold/")) else -1)) + rows.append(dict(id="solar/native-solidity", status="PASS", actualSolid=True)) + return dict(label="exercise", minecraft="1.17.1", status="PASS", checks=rows) + + def test_complete_native_survival_witness_set(self): + require_checks(self.sample(), "exercise") + self.assertEqual(len(EXPECTED), 270) + + def test_json_presence_or_pass_label_without_real_samples_is_not_enough(self): + value = self.sample() + value["checks"][0]["actualDropCounts"] = [] + with self.assertRaises(AssertionError): + require_checks(value, "exercise") + + def test_failure_tool_drop_silk_drop_quantity_and_level_remain_strict(self): + for name, field, bad in (("lithium/wood-fails", "harvestAllowed", True), + ("lithium/stone", "historicalHarvestLevel", 0), + ("sulfur/silk", "actualDropCounts", [2]), + ("sulfur/wood", "actualDropCounts", [5] * 8), + ("sulfur/hand-fails", "actualDropCounts", [1])): + value = self.sample() + next(row for row in value["checks"] if row["id"] == name)[field] = bad + with self.subTest(name=name), self.assertRaises(AssertionError): + require_checks(value, "exercise") + + def test_missing_or_duplicated_witnesses_fail(self): + value = self.sample() + value["checks"][-1] = copy.deepcopy(value["checks"][0]) + with self.assertRaises(AssertionError): + require_checks(value, "exercise") + + def test_all_wire_scaffold_states_and_legacy_solar_solidness_are_strict(self): + for name, field, bad in (("wire/63/hand", "connectionMask", 0), + ("wire/0/hand", "actualSolid", False), + ("wire/31/silk", "fullCollision", True), + ("scaffold/31/silk", "actualSolid", True), + ("scaffold/0/hand", "fullCollision", False), + ("solar/native-solidity", "actualSolid", False)): + value = self.sample() + next(row for row in value["checks"] if row["id"] == name)[field] = bad + with self.subTest(name=name), self.assertRaises(AssertionError): + require_checks(value, "exercise") + + +class PortContracts(unittest.TestCase): + def validate_ledger(self, ledger): + self.assertEqual(ledger["format"], 1) + self.assertEqual(ledger["sourceCommit"], "c310d0ca63ad9a8408dd4bba75172afafa5c71b7") + self.assertEqual(ledger["sourceTree"], "5d3893fc5316a5af20d2456d51cc020a7741f5b0") + hashes = ledger["hashes"] + excluded = ledger["excludedNativeAdaptations"] + self.assertEqual(len(hashes), 298) + self.assertEqual(len(excluded), 94) + self.assertFalse(set(hashes) & set(excluded)) + self.assertEqual(ledger["baselineFileCount"], 392) + inventory = dict(hashes) + inventory.update({name: record["historicalSha256"] for name, record in excluded.items()}) + self.assertEqual(len(inventory), 392) + canonical = lambda value: hashlib.sha256(json.dumps(value, sort_keys=True, + separators=(",", ":")).encode()).hexdigest().upper() + self.assertEqual(ledger["baselineInventorySha256"], + "C606235169C963A2CEF5445E87A645A73228AE05A35D2EFB89D22ABD6E88B559") + self.assertEqual(canonical(inventory), ledger["baselineInventorySha256"]) + self.assertEqual(ledger["excludedNativePathsSha256"], + "B0B86A9CFD2CF5065C155A44381AFE240CD5625C003A063E916CDE87918279BC") + self.assertEqual(canonical(sorted(excluded)), ledger["excludedNativePathsSha256"]) + clients = {name: {key: record[key] for key in ( + "historicalSha256", "auditedNativeSha256", "portPreservationGuards", "preservationEvidence")} + for name, record in excluded.items() if record["kind"] == "native-client-adaptation"} + self.assertEqual(len(clients), 8) + self.assertEqual(ledger["nativeClientAdaptationsSha256"], + "B7D9EC6E6146AE08A0838D187E52E353D836B327919B705026B84E9FB57098B7") + self.assertEqual(canonical(clients), ledger["nativeClientAdaptationsSha256"]) + revisions = ledger['documentationRevisions'] + self.assertEqual({'client-resources/README.md'}, set(revisions)) + revision = revisions['client-resources/README.md'] + self.assertEqual(clients['client-resources/README.md']['auditedNativeSha256'], revision['previousAuditedSha256']) + self.assertEqual('FA7900D9190957B07A915597404AA63027B2E0C950BA26C3ED0F2424FA651595', revision['nativeSha256']) + self.assertIn('Human instructions only', revision['scope']) + evidence = ledger["nativeClientPreservation"] + self.assertEqual(evidence["baselineCommit"], ledger["sourceCommit"]) + self.assertEqual(evidence["receipt"], "sidecar-summary-20261031-050041-c7861af6.json") + self.assertEqual(evidence["receiptSha256"], + "AEB5F04F1B9F0FF55C4BFA8F669630DEEB1844E046DF11039F9B6CD6F8C2CF7A") + self.assertEqual(evidence["status"], "STATIC_SIDECAR_PASS") + self.assertEqual((evidence["preservationChecks"], evidence["geometryContractChecks"], + evidence["mutationControlsRejected"]), (57, 78, 28)) + self.assertEqual(evidence["negativeControlReceipt"], + "sidecar-negative-controls/20261031-045932-0e28b9e5ba234e728a49c4537475dd59/results.json") + self.assertEqual(evidence["negativeControlReceiptSha256"], + "2D530DE914D9534B15E70878867623AECA6A69ACD1203DE1BB0388F95AF98FE4") + metadata = {"build.gradle", "gradle.properties", "src/main/resources/META-INF/mods.toml", + "src/main/resources/pack.mcmeta"} + for name, record in excluded.items(): + self.assertGreater(len(record["reason"]), 30, name) + if name in metadata: + self.assertEqual(record["kind"], "native-target-metadata") + elif name in clients: + self.assertEqual(record["preservationEvidence"], "nativeClientPreservation") + self.assertTrue(record["portPreservationGuards"], name) + for guard in record["portPreservationGuards"]: + self.assertIn(guard, clients) + self.assertTrue(guard.endswith(".ps1"), guard) + else: + self.assertEqual(record["kind"], "native-java-adaptation") + self.assertTrue(name.endswith(".java"), name) + self.assertTrue(name.startswith(("src/main/java/", "src/test/java/", "src/runner/java/", + "client-resources/java/")), name) + self.assertIn("migration/native116-historical-inputs.json", hashes) + self.assertEqual(hashes["src/test/java/zone/moddev/mc/advantageworks/NativeHarvestIsolationTest.java"], + "94BF6761278EDA6A60F9A593652EC7A42D1F65A8186A4E104807EFDDB7559E65") + self.assertEqual(hashes["client-resources/resources/gui-label-expectations.json"], + "EC0B66B926053E90C216D856B5F33C19D5B7E92DBC4C9E2E25812B401AEBAC0E") + + def test_native_client_adaptations_match_the_approved_preservation_inputs(self): + ledger = json.loads((RIG / "migration/native117-historical-inputs.json").read_text()) + self.validate_ledger(ledger) + for name, record in ledger["excludedNativeAdaptations"].items(): + if record["kind"] == "native-client-adaptation": + 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") + 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) + continue + original = git_blob(ADVANTAGE117, name).replace(b"\r\n", b"\n") + # The original Windows review hashed three text files with CRLF. + self.assertIn(expected, {hashlib.sha256(data).hexdigest().upper() + for data in (original, original.replace(b"\n", b"\r\n"))}) + for old, new in ((b"poweradvantage", b"basepower"), (b"steamadvantage", b"basesteam"), + (b"electricadvantage", b"baseelectric"), (b"PowerAdvantage-", b"basePower-"), + (b"2.4.8.117011", b"2.4.9.117011"), (b"2.2.9.117011", b"2.2.10.117011"), + (rb"2\.4\.8\.117011", rb"2\.4\.9\.117011")): + original = original.replace(old, new) + if name == "client-resources/test-probe-contract.ps1": + # This guard now applies the rebrand after constructing + # the 1.16 oracle, while keeping cyano API bindings literal. + self.assertEqual("72CD0A5BE3B9446655BB2DDA5FC74A11DBBBF3EF60B899E8834CA09F75D4382E", + hashlib.sha256(active).hexdigest().upper()) + else: + self.assertEqual(original, active) + + def test_active_port_adapters_are_separately_guarded(self): + for name in sorted(ACTIVE_ADAPTERS): + with self.subTest(path=name): + self.assertEqual(git_blob(BASE117_ADAPTERS, name).replace(b"\r\n", b"\n"), + (RIG / name).read_bytes().replace(b"\r\n", b"\n")) + + def test_ledger_cannot_refingerprint_or_exempt_historical_oracles(self): + ledger = json.loads((RIG / "migration/native117-historical-inputs.json").read_text()) + self.validate_ledger(ledger) + path = "migration/extended_lifecycle.py" + changed = copy.deepcopy(ledger) + changed["hashes"][path] = "0" * 64 + with self.assertRaises(AssertionError): + self.validate_ledger(changed) + changed = copy.deepcopy(ledger) + changed["hashes"].pop(path) + with self.assertRaises(AssertionError): + self.validate_ledger(changed) + for field, value in (("auditedNativeSha256", "0" * 64), ("portPreservationGuards", [])): + changed = copy.deepcopy(ledger) + changed["excludedNativeAdaptations"]["client-resources/test-probe-contract.ps1"][field] = value + with self.subTest(unapproved_client_change=field), self.assertRaises(AssertionError): + self.validate_ledger(changed) + for protected in (path, "profiles/dev-core.json", + "src/main/resources/assets/advantageworkstest/works/works-1.16.5.json", + "client-resources/resources/gui-label-expectations.json", + "src/test/java/zone/moddev/mc/advantageworks/NativeHarvestIsolationTest.java"): + changed = copy.deepcopy(ledger) + checksum = changed["hashes"].pop(protected) + native_path = next(name for name in changed["excludedNativeAdaptations"] if name.endswith(".java")) + native = changed["excludedNativeAdaptations"].pop(native_path) + changed["hashes"][native_path] = native["historicalSha256"] + changed["excludedNativeAdaptations"][protected] = dict( + historicalSha256=checksum, kind="native-java-adaptation", reason="unapproved new exemption") + self.assertEqual(len(changed["hashes"]), len(ledger["hashes"])) + self.assertEqual(len(changed["excludedNativeAdaptations"]), len(ledger["excludedNativeAdaptations"])) + with self.subTest(path=protected), self.assertRaises(AssertionError): + self.validate_ledger(changed) + + def test_historical_inputs_match_accepted_baseline_with_original_correction_guards(self): + ledger = json.loads((RIG / "migration/native117-historical-inputs.json").read_text()) + self.validate_ledger(ledger) + historical = json.loads((RIG / "migration/native116-historical-inputs.json").read_text()) + self.assertEqual(historical["sourceCommit"], "5b8e73a78391960643420a09798a1ef8eb06f518") + approval = json.loads((RIG / "migration/audited-semantic-corrections.json").read_text()) + self.assertEqual(approval["id"], "EXTENDED-ABSENCE-001") + records = approval["corrections"] + self.assertIn("migration/extended_lifecycle.py", records) + self.assertLessEqual(set(records), {"migration/extended_lifecycle.py", "migration/test_native115_geometry.py"}) + + def verify(data, expected, record): + self.assertEqual(record["correctedSha256"], expected) + self.assertEqual(hashlib.sha256(data).hexdigest().upper(), expected) + tree = ast.parse(data.decode("utf-8-sig")) + if record["kind"] == "semantic-oracle-correction": + names = {"partial_result", "continuous_absence"} + functions = {n.name: hashlib.sha256(ast.dump(n).encode()).hexdigest().upper() + for n in tree.body if isinstance(n, ast.FunctionDef) and n.name in names} + self.assertEqual(functions, record["correctedFunctionAstSha256"]) + tree.body = [n for n in tree.body if not (isinstance(n, ast.FunctionDef) and n.name in names)] + else: + self.assertEqual(record["kind"], "historical-guard-reporting-correction") + for node in tree.body: + if isinstance(node, ast.ClassDef) and node.name == "PortContracts": + node.body = [n for n in node.body if not (isinstance(n, ast.FunctionDef) and n.name in ( + "test_historical_adapters_profiles_and_semantic_oracles_are_byte_identical", + "test_historical_inputs_remain_exact_except_audited_semantic_corrections"))] + tree.body = [n for n in tree.body if not (isinstance(n, ast.Import) + and all(a.name in ("ast", "hashlib") for a in n.names))] + self.assertEqual(hashlib.sha256(ast.dump(tree).encode()).hexdigest().upper(), + record["unchangedHistoricalAstSha256"]) + + for relative, checksum in ledger["hashes"].items(): + with self.subTest(path=relative): + data = oracle_bytes(ledger["sourceCommit"], relative) + self.assertEqual(hashlib.sha256(data).hexdigest().upper(), checksum) + if relative in records: + self.assertEqual(records[relative]["historicalSha256"], historical["hashes"][relative]) + verify(data, checksum, records[relative]) + + # Updating only a byte hash must not bless a lower threshold or unrelated oracle edit. + source = (RIG / "migration/extended_lifecycle.py").read_bytes() + for old, new in ((b">= 40", b">= 39"), + (b"four consecutive moves were not witnessed", b"three consecutive moves were not witnessed")): + changed = source.replace(old, new, 1) + self.assertNotEqual(changed, source) + record = copy.deepcopy(records["migration/extended_lifecycle.py"]) + record["correctedSha256"] = hashlib.sha256(changed).hexdigest().upper() + with self.subTest(unapproved_change=new), self.assertRaises(AssertionError): + verify(changed, record["correctedSha256"], record) + + def test_all_thirteen_old_routes_and_comprehensive_native114_source_are_required(self): + self.assertEqual(len(REQUIRED["frozen"]), 16) + self.assertEqual(len(REQUIRED["connected"]), 20) + self.assertIn("native115/full", REQUIRED["frozen"]) + self.assertIn("native116/full", REQUIRED["frozen"]) + self.assertEqual(5, len([identity for identity in REQUIRED["connected"] if identity.startswith("native115/")])) + self.assertEqual(5, len([identity for identity in REQUIRED["connected"] if identity.startswith("native116/")])) + document = dict(format=1, acceptedPower114=ACCEPTED_POWER114, + routes=[dict(kind=kind, id=name) for kind, names in REQUIRED.items() for name in names]) + require_routes(document) + for kind in REQUIRED: + value = copy.deepcopy(document) + index = next(i for i, row in enumerate(value["routes"]) if row["kind"] == kind) + value["routes"].pop(index) + with self.subTest(kind=kind), self.assertRaises(AssertionError): + require_routes(value) + + def test_accepted114_cannot_be_replaced_by_another_passing_legacy_case(self): + document = dict(format=1, acceptedPower114=ACCEPTED_POWER114, + routes=[dict(kind=kind, id=name) for kind, names in REQUIRED.items() for name in names]) + victim = next(row for row in document["routes"] if row["kind"] == "connected" and row["id"].startswith("native114/")) + victim["id"] = "legacy/original110" + with self.assertRaises(AssertionError): + require_routes(document) + + def test_freshly_fingerprinting_an_unverified_old_destination_is_not_approval(self): + with self.assertRaisesRegex(AssertionError, "not approval"): + require_source114_receipt(dict(id="native114/full", kind="frozen", sourceStatus="FRESH_HASH_ONLY")) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_hammer_audit.py b/test-rig/migration/test_native117_hammer_audit.py new file mode 100644 index 00000000..e31908dd --- /dev/null +++ b/test-rig/migration/test_native117_hammer_audit.py @@ -0,0 +1,91 @@ +from copy import deepcopy +import unittest +from audit_native117_hammer import console_label, validate + + +def case(): + source = '{Count:1b,id:"electricadvantage:li_ingot"}' + result = '{Count:1b,id:"electricadvantage:li_powder"}' + identity = '77102ccd-9ef5-4a98-b1ce-4c759d3c6b17' + hammer = 'zone.moddev.mc.basemetals.content.CrackhammerItem' + before = dict(protocol='native117-hammer-query-v1', recipe='electricadvantage:crushing_lithium_ingot', + sourceUuid=identity, accepted=True, sourceMatchesInput=True, inputNbt=source, expectedNbt=result, + before=dict(sourceRemoved=False, sourceInAabb=True, sourceIntersectsArea=True, uuidLookupIsSource=True, + clientSide=False, creative=False, shift=False, sourceNbt=source, + hammerClass=hammer, hammerDamage=0, hammerNbt='{Count:1b,id:"basemetals:iron_crackhammer",tag:{Damage:0}}', + dimension='minecraft:overworld', gameTime=20, + items=[dict(uuid=identity, isSource=True, removed=False, stackNbt=source)]), + baseSelection=dict(status='MATCH', matched=True, requestedIdPresent=True, + inputNbt=source, id='electricadvantage:crushing_lithium_ingot', + resultNbt=result, **{'class':'zone.moddev.mc.basemetals.recipe.CrushingRecipe'})) + after = deepcopy(before) + after.update(action='SUCCESS', success=True, sourceConsumed=True, damageIsOne=True, + after=dict(dimension='minecraft:overworld', gameTime=20, sourceNbt='{}', hammerClass=hammer, + hammerDamage=1, hammerNbt='{Count:1b,id:"basemetals:iron_crackhammer",tag:{Damage:1}}', + items=[dict(isSource=False, stackNbt=result)])) + return before, after + + +class HammerAuditControls(unittest.TestCase): + def test_initial_reload_shares_original_console_not_initial_initial(self): + for mode in ('normal', 'tech_progression', 'apocalyptic'): + for rubber in ('rubber', 'no-rubber'): + prefix = mode + '-' + rubber + self.assertEqual(console_label('progression-' + prefix + '-initial.json'), prefix + '-initial') + self.assertEqual(console_label('progression-' + prefix + '-initial-reload.json'), prefix + '-initial') + self.assertEqual(console_label('progression-' + prefix + '-restart.json'), prefix + '-restart') + for bad in ('progression-normal-rubber-reload.json', 'progression-normal-rubber-initial-initial.json', 'other.json'): + with self.assertRaises(AssertionError): console_label(bad) + + def test_complete_positive(self): + self.assertEqual(validate(*case()), ('electricadvantage:crushing_lithium_ingot', 1, 1)) + + def test_nine_setup_and_selector_negative_controls(self): + changes = { + 'addition_rejected': lambda b: b.update(accepted=False), + 'input_altered': lambda b: b.update(sourceMatchesInput=False), + 'query_invisible': lambda b: b['before'].update(sourceInAabb=False), + 'uuid_invisible': lambda b: b['before'].update(uuidLookupIsSource=False), + 'decoy': lambda b: b['before']['items'][0].update(uuid='57e677cb-2bfb-41d1-a314-dd3b2ed72105'), + 'extra_item': lambda b: b['before']['items'].append(deepcopy(b['before']['items'][0])), + 'typed_count_changed': lambda b: b['before'].update(sourceNbt='{Count:1s,id:"electricadvantage:li_ingot"}'), + 'no_match': lambda b: b['baseSelection'].update(status='NO_MATCH', matched=False), + 'observer_error': lambda b: b['baseSelection'].update(status='OBSERVER_ERROR'), + } + for name, change in changes.items(): + with self.subTest(name=name): + before, after = case() + change(before) + for key, value in before.items(): after[key] = deepcopy(value) + with self.assertRaises((AssertionError, KeyError, ValueError)): + validate(before, after) + + def test_eight_independent_callback_and_output_failures(self): + changes = { + 'pass_action': lambda a: a.update(action='PASS'), + 'false_success': lambda a: a.update(success=False), + 'source_not_consumed': lambda a: a.update(sourceConsumed=False), + 'source_raw_remaining': lambda a: a['after'].update(sourceNbt='{Count:1b,id:"electricadvantage:li_ingot"}'), + 'damage_flag_false': lambda a: a.update(damageIsOne=False), + 'raw_damage_two': lambda a: a['after'].update(hammerNbt='{Count:1b,id:"basemetals:iron_crackhammer",tag:{Damage:2}}'), + 'foreign_output': lambda a: a['after']['items'][0].update(stackNbt='{Count:1b,id:"minecraft:dirt"}'), + 'extra_output': lambda a: a['after']['items'][0].update(stackNbt='{Count:2b,id:"electricadvantage:li_powder"}'), + } + for name, change in changes.items(): + with self.subTest(name=name): + before, after = case() + change(after) + with self.assertRaises((AssertionError, KeyError, ValueError)): + validate(before, after) + + def test_two_custody_failures(self): + before, after = case() + after['sourceUuid'] = '57e677cb-2bfb-41d1-a314-dd3b2ed72105' + with self.assertRaises(AssertionError): validate(before, after) + before, after = case() + after['after']['gameTime'] += 1 + with self.assertRaises(AssertionError): validate(before, after) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/test-rig/migration/test_native117_harvest.py b/test-rig/migration/test_native117_harvest.py new file mode 100644 index 00000000..15687033 --- /dev/null +++ b/test-rig/migration/test_native117_harvest.py @@ -0,0 +1,145 @@ +"""Harvest fixture isolation and unchanged-oracle tests; never start a server.""" + +from copy import deepcopy +import hashlib +import json +from pathlib import Path +import unittest +from uuid import UUID + +import native117_harvest as native + + +def sample(label="exercise"): + rows = [] + identity = 1 + for name, (level, allowed, silk, minimum, maximum, trials) in native.EXPECTED.items(): + evidence = [] + for trial in range(trials): + drops = [] + if minimum: + drops.append(dict(entityId=identity, uuid=str(UUID(int=identity)), + item="unit:actual-drop", count=minimum, removed=False)) + identity += 1 + evidence.append(dict(trial=trial, worldTime=1, removedBeforeCleanup=trial, + liveBeforeCleanup=int(trial > 0), liveAfterCleanup=0, actualDrops=drops)) + rows.append(dict(id=name, status="PASS", historicalHarvestLevel=level, harvestAllowed=allowed, + silkTouch=silk, trials=trials, actualDropCounts=[minimum] * trials, + connectionMask=int(name.split("/")[1]) if name.startswith(("wire/", "scaffold/")) else -1, + actualSolid=name.startswith(("sulfur/", "lithium/", "wire/")), + fullCollision=not name.startswith("wire/"), dropEvidence=evidence)) + rows.append(dict(id="solar/native-solidity", status="PASS", actualSolid=True)) + return dict(label=label, minecraft="1.17.1", status="PASS", checks=rows, + fixtureIsolation=native.ISOLATION_PROTOCOL) + + +def row(document, name="sulfur/wood"): + return next(entry for entry in document["checks"] if entry["id"] == name) + + +class HarvestIsolationTests(unittest.TestCase): + def test_original_270_harvest_oracles_are_exactly_preserved(self): + checksum = hashlib.sha256(json.dumps(native.EXPECTED, sort_keys=True, separators=(",", ":")).encode()).hexdigest().upper() + self.assertEqual("A43F316D748DFCCB4DA9B72AE771851FFBD30661C56E5609A0D04E0AC0050D5B", checksum) + self.assertEqual(270, len(native.EXPECTED)) + + def test_java_witness_inputs_masks_trial_counts_and_thresholds_are_unchanged(self): + source = (Path(__file__).resolve().parents[1] / + "src/main/java/zone/moddev/mc/advantageworks/NativeHarvestProbe.java").read_text() + region = source.split(" String sulfur =", 1)[1].split(" JsonObject solar =", 1)[0] + self.assertIn("baseelectric:sulfur_ore", region) + self.assertNotIn("electricadvantage:", region) + region = region.replace("baseelectric:", "electricadvantage:").replace("basepower:", "poweradvantage:") + self.assertEqual("3FF0AC4C13CA1B737679F67128CE2F41C5A0873E89258D456628BA8A309BAA9A", + hashlib.sha256(region.encode()).hexdigest().upper()) + self.assertIn("Actual drop quantity changed: ", source) + self.assertIn('require(count >= minimum && count <= maximum,', source) + self.assertIn('equals(dropId), "Wrong actual drop: "', source) + self.assertIn('bounds, entity -> !entity.isRemoved())', source) + + def test_new_protocol_accepts_all_original_cases_in_each_restart_phase(self): + for label in ("exercise", "restored"): + document = sample(label) + native.require_checks(document, label) + native.require_isolation(document) + + def test_old_or_missing_fixture_protocol_is_rejected(self): + for marker in (None, "native114", "native117-default-entity-query"): + document = sample() + document["fixtureIsolation"] = marker + with self.subTest(marker=marker), self.assertRaisesRegex(AssertionError, "isolation protocol"): + native.require_isolation(document) + + def test_incomplete_trial_audits_cannot_qualify(self): + document = sample() + row(document)["dropEvidence"].pop() + with self.assertRaisesRegex(AssertionError, "per-trial"): + native.require_isolation(document) + + def test_live_remainder_bad_sequence_or_noninteger_cleanup_is_rejected(self): + for field, value in (("liveAfterCleanup", 1), ("liveAfterCleanup", False), + ("removedBeforeCleanup", -1), ("liveBeforeCleanup", -1), + ("trial", 3), ("worldTime", -1)): + document = sample() + row(document)["dropEvidence"][0][field] = value + with self.subTest(field=field, value=value), self.assertRaisesRegex(AssertionError, "not isolated"): + native.require_isolation(document) + + def test_removed_entities_missing_identity_and_count_lies_are_rejected(self): + for field, value in (("removed", True), ("removed", None), ("entityId", "1"), + ("uuid", ""), ("item", ""), ("count", 0), ("count", True), ("count", 2)): + document = sample() + row(document)["dropEvidence"][0]["actualDrops"][0][field] = value + with self.subTest(field=field, value=value), self.assertRaises(AssertionError): + native.require_isolation(document) + + def test_same_entity_cannot_be_counted_in_two_trials_or_two_rows(self): + for across_rows in (False, True): + for field in ("entityId", "uuid"): + document = sample() + first = row(document)["dropEvidence"][0]["actualDrops"][0] + later = row(document, "sulfur/stone")["dropEvidence"][0] if across_rows else row(document)["dropEvidence"][1] + later["actualDrops"][0][field] = first[field] + with self.subTest(across_rows=across_rows, field=field), self.assertRaisesRegex(AssertionError, "repeated"): + native.require_isolation(document) + + def test_no_drop_case_cannot_hide_an_unexpected_live_entity(self): + document = sample() + row(document, "lithium/wood-fails")["dropEvidence"][0]["actualDrops"] = [ + dict(entityId=99999, uuid=str(UUID(int=99999)), item="electricadvantage:sulfur_powder", count=4, removed=False)] + with self.assertRaisesRegex(AssertionError, "disagree"): + native.require_isolation(document) + + def test_failed_identity_witness_reports_actual_failure_not_missing_inventory(self): + document = sample() + document["status"] = "FAIL" + failed = row(document, "sulfur/silk") + failed.update(status="FAIL", failure="Wrong actual drop: 4 sulfur_powder") + with self.assertRaisesRegex(AssertionError, "sulfur/silk: Wrong actual drop: 4 sulfur_powder"): + native.require_checks(document, "exercise") + + def test_complete_inventory_with_failed_aggregate_status_still_fails(self): + document = sample() + document["status"] = "FAIL" + with self.assertRaisesRegex(AssertionError, "witnesses failed"): + native.require_checks(document, "exercise") + + def test_original_drop_bounds_wrong_tools_masks_shapes_and_solar_remain_strict(self): + for name, field, value in (("sulfur/wood", "actualDropCounts", [5] * 8), + ("sulfur/silk", "actualDropCounts", [4]), + ("lithium/wood-fails", "actualDropCounts", [1]), + ("lithium/stone", "historicalHarvestLevel", 0), + ("wire/63/hand", "connectionMask", 0), + ("wire/0/hand", "actualSolid", False), + ("wire/0/hand", "fullCollision", True), + ("scaffold/0/hand", "actualSolid", True), + ("scaffold/0/hand", "fullCollision", False), + ("solar/native-solidity", "actualSolid", False)): + document = sample() + row(document, name)[field] = deepcopy(value) + with self.subTest(name=name, field=field), self.assertRaises(AssertionError): + native.require_checks(document, "exercise") + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_helper_contract.py b/test-rig/migration/test_native117_helper_contract.py new file mode 100644 index 00000000..af15caf2 --- /dev/null +++ b/test-rig/migration/test_native117_helper_contract.py @@ -0,0 +1,138 @@ +"""Synthetic partition/tamper controls, not a claim of runtime migration coverage.""" + +import copy +import json +from pathlib import Path +import unittest + +import nbtlib as n +import native117_entity_storage as native +import native117_entity_storage_replay as replay +import native117_helper_contract as policy +import native117_stopped_entities as stopped +from test_native117_entity_storage import entity, entry, put +import test_native117_entity_storage_replay as fixtures + + +class NativeHelperContractTests(unittest.TestCase): + def setUp(self): + self.fixture = fixtures.RetainedEntityReplayTests() + self.fixture.setUp() + self.addCleanup(self.fixture.doCleanups) + f = self.fixture + self.holder = entity(identity='minecraft:horse') + self.holder['Leash'] = n.Compound(X=n.Int(8), Y=n.Int(80), Z=n.Int(8)) + f.stage(actor=self.holder) + self.knot = entity((5, 6, 7, 8), pos=(8.5, 80.375, 8.5), identity=policy.ID) + self.identity = native._identity(self.knot) + self.helper = entry(self.knot, uuid=self.identity, id=policy.ID, nativeClass=policy.CLASS, + worldDimensionKey='minecraft:overworld', canSerialize=False, removed=False, passenger=False, + passengerCount=0, policy=policy.POLICY, attachmentPosition=[8, 80, 8]) + f.document.update(format=2, helperPolicy=policy.POLICY, helperContractSha256=policy.CONTRACT_SHA, + nonpersistentHelperCount=1, observedEntityCount=2) + for record in f.document['dimensions'].values(): + record.update(nonpersistentHelpers=[], nonpersistentHelperCount=0, observedEntityCount=record['entityCount']) + f.document['dimensions']['minecraft:overworld'].update(nonpersistentHelpers=[self.helper], + nonpersistentHelperCount=1, observedEntityCount=2) + self.lifecycle = dict(worldDimensionKey='minecraft:overworld', gameTime=19, nativeOnLoad=True, + nativeEntityContextVersion=1, entities=[dict(uuid=native._identity(self.holder), + worldDimensionKey='minecraft:overworld', sameWorld=True, leashHolderUuid=self.identity, + leashHolderType=policy.ID, leashHolderPosition=[8, 80, 8])]) + self.contract = (Path(__file__).resolve().parents[1] / + 'src/main/resources/assets/advantageworkstest/native117-helper-contract.json').read_bytes() + (f.runtime / policy.CONTRACT).write_bytes(self.contract) + fixtures.write(f.runtime / 'dimensions-runtime.json', self.lifecycle) + + def validate(self): + return policy.validate(self.fixture.document, self.contract, self.lifecycle) + + def test_exact_helper_partition_has_separate_native_lifecycle_not_persistence_credit(self): + result = self.validate() + self.assertEqual('PASS_NATIVE_HELPER_LIFECYCLE', result['status']) + self.assertEqual([native._identity(self.holder)], result['helpers'][self.identity]['holderUuids']) + proof = self.fixture.retain() + self.assertEqual((1, 1), (proof['snapshotRecords'], proof['matchedEntities'])) + result = replay.replay(self.fixture.evidence) + self.assertEqual('PASS', result['status']) + self.assertIn(policy.CONTRACT, result['files']) + self.assertIn(policy.LIFECYCLE, result['files']) + + def test_unknown_nosave_type_and_wrong_class_fail_closed(self): + for name, value in [('id', 'minecraft:lightning_bolt'), ('nativeClass', policy.CLASS + 'Subclass'), + ('canSerialize', True), ('removed', True), ('passenger', True), ('policy', 'unproven')]: + with self.subTest(name=name): + before = self.helper[name] + self.helper[name] = value + with self.assertRaises(AssertionError): self.validate() + self.helper[name] = before + + def test_duplicate_uuid_cannot_hide_persistent_entity(self): + self.helper['uuid'] = native._identity(self.holder) + with self.assertRaisesRegex(AssertionError, 'Duplicate'): self.validate() + + def test_forged_counts_and_boolean_counts_fail(self): + for target, key in [(self.fixture.document, 'entityCount'), (self.fixture.document, 'observedEntityCount'), + (self.fixture.document['dimensions']['minecraft:overworld'], 'nonpersistentHelperCount')]: + previous = target[key] + for value in (True, 999): + target[key] = value + with self.assertRaises(AssertionError): self.validate() + target[key] = previous + + def test_complete_helper_nbt_identity_and_attachment_must_match(self): + self.knot['Pos'][1] = n.Double(79.375) + self.helper['nbtBase64'] = entry(self.knot)['nbtBase64'] + with self.assertRaisesRegex(AssertionError, 'attachment'): self.validate() + + def test_hanging_entity_tile_coordinates_cannot_substitute_for_native_knot_pos(self): + self.knot['TileY'] = n.Int(80) + self.helper['nbtBase64'] = entry(self.knot)['nbtBase64'] + with self.assertRaisesRegex(AssertionError, 'attachment'): self.validate() + + def test_missing_persistent_holder_is_not_accepted_as_transient(self): + record = self.fixture.document['dimensions']['minecraft:overworld'] + record.update(entities=[], entityCount=0, observedEntityCount=1) + self.fixture.document.update(entityCount=0, observedEntityCount=1) + with self.assertRaisesRegex(AssertionError, 'persistent holder'): self.validate() + + def test_wrong_dimension_future_or_missing_relationship_fails(self): + for key, value in [('worldDimensionKey', 'minecraft:the_nether'), ('gameTime', 21), ('entities', [])]: + previous = self.lifecycle[key] + self.lifecycle[key] = value + with self.assertRaises(AssertionError): self.validate() + self.lifecycle[key] = previous + + def test_changed_native_contract_is_rejected(self): + self.contract += b' ' + with self.assertRaisesRegex(AssertionError, 'pin differs'): self.validate() + + def test_saved_helper_is_rejected_even_if_native_payload_is_well_formed(self): + put(self.fixture.world, (0, 0), [self.holder, self.knot]) + with self.assertRaisesRegex(AssertionError, 'unexpectedly appears'): + self.fixture.retain() + + def test_missing_saved_persistent_entity_still_fails(self): + put(self.fixture.world, (0, 0), []) + with self.assertRaisesRegex(AssertionError, 'absent'): + self.fixture.retain() + + def test_replay_checks_helper_data_not_only_claimed_pass(self): + self.fixture.retain() + path = self.fixture.evidence / policy.LIFECYCLE + data = fixtures.read(path) + data['entities'][0]['leashHolderPosition'][1] = 79 + fixtures.write(path, data) + with self.assertRaisesRegex(AssertionError, 'persistent holder'): + replay.replay(self.fixture.evidence) + + def test_replay_rejects_claimed_helper_counts(self): + self.fixture.retain() + path = self.fixture.evidence / 'receipt.json' + data = fixtures.read(path) + data['helperLifecycle']['helpers'] = {} + fixtures.write(path, data) + with self.assertRaisesRegex(AssertionError, 'metadata differs'): + replay.replay(self.fixture.evidence) + + +if __name__ == '__main__': unittest.main() diff --git a/test-rig/migration/test_native117_holder_networks.py b/test-rig/migration/test_native117_holder_networks.py new file mode 100644 index 00000000..b453d564 --- /dev/null +++ b/test-rig/migration/test_native117_holder_networks.py @@ -0,0 +1,61 @@ +"""The full native holder halo is wider than the FULL-status halo.""" + +import unittest +from native117_holder_networks import (FORCED_LEVEL, HOLDER_RADIUS, LAST_HOLDER_LEVEL, + bridge_fixtures, extended_fixtures, linear_fixtures) +from native117_ticket_networks import fill_commands +import extended_lifecycle + + +class HolderNetworkTests(unittest.TestCase): + def test_exact_native_holder_threshold_is_not_confused_with_full_threshold(self): + self.assertEqual(FORCED_LEVEL, 31) + self.assertEqual(LAST_HOLDER_LEVEL, 33 + 11) + self.assertEqual(HOLDER_RADIUS, 13) + self.assertGreater(FORCED_LEVEL + HOLDER_RADIUS + 1, LAST_HOLDER_LEVEL) + + def test_every_retained_ticket_is_outside_full_holder_dependency_radius(self): + for definition in (linear_fixtures(), bridge_fixtures()): + for f in definition: + all_chunks = {(x, z) for x in range(f["min"][0] >> 4, (f["max"][0] >> 4) + 1) + for z in range(f["min"][2] >> 4, (f["max"][2] >> 4) + 1)} + for group in ("bridge",) if "bridge" in f["groups"] else ("supply", "machine"): + retained = all_chunks - set(map(tuple, f["groups"][group + "halo"])) + self.assertTrue(retained) + self.assertTrue(all(min(max(abs(x-a), abs(z-b)) for a, b in f["groups"][group]) > 13 for x, z in retained)) + + def test_long_paths_and_all_witnesses_stay_inside_separate_bays(self): + for definition in (linear_fixtures(), bridge_fixtures(), extended_fixtures()): + for index, f in enumerate(definition): + for p in f["witnesses"]: + self.assertTrue(all(a <= n <= b for a, n, b in zip(f["min"], p, f["max"]))) + for other in definition[index + 1:]: + self.assertTrue(any(f["max"][a] < other["min"][a] or other["max"][a] < f["min"][a] for a in range(3))) + + def test_paths_are_connected(self): + for f in linear_fixtures() + bridge_fixtures(): + sink = f["witnesses"][1] if "bridge" in f["groups"] else f["witnesses"][4] + path = [f["witnesses"][0], *f["pipes"], sink] + self.assertTrue(all(sum(abs(x-y) for x, y in zip(a,b)) == 1 for a,b in zip(path,path[1:]))) + self.assertEqual(len(path), len(set(map(tuple,path)))) + + def test_drill_geometry_is_not_changed_to_accept_network_results(self): + blocks = {"steamadvantage:steam_drill": "basesteam:steam_drill", + "electricadvantage:electric_drill": "baseelectric:electric_drill"} + self.assertEqual(extended_fixtures()[:18], [dict(f, block=blocks[f["block"]]) + for f in extended_lifecycle.fixtures() if "route" in f]) + + def test_native_fill_is_still_bounded_below_vanilla_volume_limit(self): + for f in linear_fixtures() + bridge_fixtures(): + commands = fill_commands([f], "fill " + " ".join(map(str,f["min"]+f["max"])) + " minecraft:air replace") + self.assertEqual(len(commands), 68) + for command in commands: + values = list(map(int, command.split()[1:7])) + volume = 1 + for a,b in zip(values[:3],values[3:]): + volume *= b-a+1 + self.assertLessEqual(volume,32768) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_identity.py b/test-rig/migration/test_native117_identity.py new file mode 100644 index 00000000..7aaa1239 --- /dev/null +++ b/test-rig/migration/test_native117_identity.py @@ -0,0 +1,41 @@ +import unittest + +from native117 import native_normalized, source_normalized +from native117_identity import command_ids, fixture_ids, target_id + + +class NamespaceUpgradeTest(unittest.TestCase): + def test_maps_expected_nested_items_and_fluids_without_changing_amounts(self): + old = { + "id": "minecraft:poweradvantage.tileentity.fluid_metal_tank", + "Tank": {"FluidName": "poweradvantage:crude_oil", "Amount": 1730}, + "Items": [{"id": "steamadvantage:musket", "Count": 1, + "tag": {"display": {"Name": "steamadvantage:musket"}, + "Enchantments": [{"id": "steamadvantage:rapid_reload", "lvl": 2}]}}], + } + actual = source_normalized(old) + self.assertEqual("basepower:tileentity.fluid_metal_tank", actual["id"]) + self.assertEqual({"FluidName": "basepower:crude_oil", "Amount": 1730}, actual["Tank"]) + item = actual["Items"][0] + self.assertEqual(("basesteam:musket", 1), (item["id"], item["Count"])) + self.assertEqual("steamadvantage:musket", item["tag"]["display"]["Name"]) + self.assertEqual([{"id": "basesteam:rapid_reload", "lvl": 2}], item["tag"]["Enchantments"]) + + def test_does_not_hide_an_unmigrated_target(self): + old = {"id": "electricadvantage:electric_drill"} + self.assertNotEqual(source_normalized(old), native_normalized(old)) + + def test_keeps_base_family_and_other_mods_unchanged(self): + for identity in ("basepower:crude_oil", "basesteam:steam_drill", "minecraft:water", + "mineralogy:crude_oil", "other:poweradvantage"): + self.assertEqual(identity, target_id(identity)) + + def test_retargets_only_native_command_and_fixture_identifiers(self): + self.assertEqual("setblock 0 0 0 basesteam:steam_drill[facing=west]", + command_ids("setblock 0 0 0 steamadvantage:steam_drill[facing=west]")) + self.assertEqual({"basepower:infinite_steam": ["baseelectric:electric_drill", 4]}, + fixture_ids({"poweradvantage:infinite_steam": ["electricadvantage:electric_drill", 4]})) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_interop.py b/test-rig/migration/test_native117_interop.py new file mode 100644 index 00000000..8c78903e --- /dev/null +++ b/test-rig/migration/test_native117_interop.py @@ -0,0 +1,132 @@ +import unittest +from pathlib import Path +import tempfile + +from native117_interop import require_checks, require_probe_order, mark_runtime + + +class NativeInteropOracleTests(unittest.TestCase): + def test_runtime_marks_both_the_probe_and_read_only_readiness_command(self): + with tempfile.TemporaryDirectory() as temporary: + runtime = Path(temporary) + mark_runtime(runtime) + self.assertEqual({'advantage-native-interop.allow', 'advantage-works.allow'}, + {path.name for path in runtime.iterdir()}) + + def test_probe_cannot_run_before_actual_readiness(self): + require_probe_order(dict(worldTime=43), dict(completedWorldTime=43)) + require_probe_order(dict(worldTime=44), dict(completedWorldTime=43)) + for time in (42, None, True, '44'): + with self.subTest(time=time), self.assertRaisesRegex(AssertionError, 'preceded'): + require_probe_order(dict(worldTime=time), dict(completedWorldTime=43)) + + ORIGINAL_EXERCISE_IDS = ( + "converter_rf_steam/capability", + "converter_rf_steam/forge-to-advantage", + "converter_rf_steam/advantage-to-forge", + "converter_rf_steam/invalidation", + "converter_rf_electricity/capability", + "converter_rf_electricity/forge-to-advantage", + "converter_rf_electricity/advantage-to-forge", + "converter_rf_electricity/invalidation", + "converter_rf_quantum/capability", + "converter_rf_quantum/forge-to-advantage", + "converter_rf_quantum/advantage-to-forge", + "converter_rf_quantum/invalidation", + "converter_tr_electricity/capability", + "converter_tr_electricity/forge-to-advantage", + "converter_tr_electricity/advantage-to-forge", + "converter_tr_electricity/invalidation", + "fluid/simulation-conservation-and-identity", + "portable-tank/native-break-drop-and-replace", + "locked-chest/conveyor-refuses-extraction", + "unlocked-chest/conveyor-transfers-exactly", + ) + CALLBACK_IDS = ( + "capability/native-chunk-unloaded-callback", + "capability/native-clear-removed-and-revive", + ) + RESTORED_IDS = ( + "portable-tank/full-process-restart", + "conveyor/full-process-restart", + ) + + def document(self, phase="exercise"): + ids = self.ORIGINAL_EXERCISE_IDS + self.CALLBACK_IDS if phase == "exercise" else self.RESTORED_IDS + return dict(phase=phase, status="PASS", checks=[dict(id=id, status="PASS") for id in ids]) + + def test_complete_witness_sets_pass(self): + for phase in ("exercise", "restored"): + require_checks(self.document(phase), phase) + + def test_single_conversion_failure_is_not_hidden_by_overall_status(self): + document = self.document() + document["checks"][2].update(status="FAIL", failure="energy disappeared") + with self.assertRaisesRegex(AssertionError, "energy disappeared"): + require_checks(document, "exercise") + + def test_missing_and_duplicate_witnesses_fail(self): + document = self.document() + document["checks"].pop() + with self.assertRaisesRegex(AssertionError, "Incomplete"): + require_checks(document, "exercise") + document = self.document() + document["checks"][1]["id"] = document["checks"][0]["id"] + with self.assertRaisesRegex(AssertionError, "Duplicate"): + require_checks(document, "exercise") + + def test_original_twenty_are_not_native_callback_proof(self): + document = self.document() + document["checks"] = document["checks"][:20] + with self.assertRaisesRegex(AssertionError, "Incomplete"): + require_checks(document, "exercise") + + def test_each_callback_is_required_and_cannot_be_relabelled(self): + for id in self.CALLBACK_IDS: + with self.subTest(id=id): + document = self.document() + next(row for row in document["checks"] if row["id"] == id)["id"] = "unrelated/PASS" + with self.assertRaisesRegex(AssertionError, "Unexpected"): + require_checks(document, "exercise") + + def test_callback_failures_are_not_hidden_by_overall_pass(self): + for id in self.CALLBACK_IDS: + with self.subTest(id=id): + document = self.document() + next(row for row in document["checks"] if row["id"] == id).update( + status="FAIL", failure="invalid callback lifecycle") + with self.assertRaisesRegex(AssertionError, "invalid callback lifecycle"): + require_checks(document, "exercise") + + def test_original_and_restoration_names_remain_required(self): + for phase, ids in (("exercise", self.ORIGINAL_EXERCISE_IDS), ("restored", self.RESTORED_IDS)): + for id in ids: + with self.subTest(phase=phase, id=id): + document = self.document(phase) + next(row for row in document["checks"] if row["id"] == id)["id"] = "unrelated/PASS" + with self.assertRaisesRegex(AssertionError, "Unexpected"): + require_checks(document, phase) + + def test_unknown_phase_is_not_a_restoration_witness(self): + document = self.document("restored") + document["phase"] = "not-a-phase" + with self.assertRaisesRegex(AssertionError, "Unknown"): + require_checks(document, "not-a-phase") + + def test_overall_failure_is_not_hidden_by_passing_rows(self): + document = self.document() + document["status"] = "FAIL" + with self.assertRaisesRegex(AssertionError, "defects"): + require_checks(document, "exercise") + + def test_extra_exercise_or_restoration_rows_fail(self): + for phase in ("exercise", "restored"): + with self.subTest(phase=phase): + document = self.document(phase) + document["checks"].append(dict(id="unrelated/PASS", status="PASS")) + with self.assertRaisesRegex(AssertionError, "Incomplete"): + require_checks(document, phase) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_legacy_snbt.py b/test-rig/migration/test_native117_legacy_snbt.py new file mode 100644 index 00000000..bf32227a --- /dev/null +++ b/test-rig/migration/test_native117_legacy_snbt.py @@ -0,0 +1,32 @@ +import unittest +import nbtlib +from native117_legacy_snbt import parse_legacy_snbt + + +class LegacyDiagnosticSnbtTests(unittest.TestCase): + def test_summary_uses_actual_nonzero_binary_bytes(self): + binary = nbtlib.parse_nbt('{growth:[B;4b,-2b,7b],prog:[I;0,1,2]}') + parsed = parse_legacy_snbt('{growth:[3 bytes],prog:[0,1,2,]}', binary) + self.assertEqual([4, -2, 7], parsed["growth"].unpack().tolist()) + self.assertEqual([0, 1, 2], parsed["prog"].unpack()) + + def test_summary_without_binary_or_wrong_shape_fails(self): + for peer in (None, nbtlib.parse_nbt('{growth:[B;1b,2b]}'), nbtlib.parse_nbt('{growth:[I;1,2,3]}')): + with self.assertRaises(ValueError): parse_legacy_snbt('{growth:[3 bytes]}', peer) + + def test_nested_summary_and_quoted_text_are_distinct(self): + binary = nbtlib.parse_nbt('{Items:[{id:"minecraft:dirt",Count:3b,tag:{bytes:[B;8b],Name:"[3 bytes]"}}]}') + parsed = parse_legacy_snbt('{Items:[0:{id:"minecraft:dirt",Count:3b,tag:{bytes:[1 bytes],Name:"[3 bytes]"}}]}', binary) + self.assertEqual(8, int(parsed["Items"][0]["tag"]["bytes"][0])) + self.assertEqual("[3 bytes]", str(parsed["Items"][0]["tag"]["Name"])) + + def test_plain_and_indexed_lists_remain_supported(self): + self.assertEqual([0, 1], parse_legacy_snbt('[0,1,]').unpack()) + self.assertEqual(["a", "b"], parse_legacy_snbt('[0:"a",1:"b"]').unpack()) + + def test_malformed_legacy_forms_fail(self): + for text in ('[1:"a"]', '[0:"a",0:"b"]', '[0:1,2]', '[3 bytes,1]', '[3 bytes] extra'): + with self.assertRaises(ValueError): parse_legacy_snbt(text) + + +if __name__ == "__main__": unittest.main() diff --git a/test-rig/migration/test_native117_lifecycle.py b/test-rig/migration/test_native117_lifecycle.py new file mode 100644 index 00000000..8bd907a7 --- /dev/null +++ b/test-rig/migration/test_native117_lifecycle.py @@ -0,0 +1,466 @@ +"""Native lifecycle adapter regressions; no Minecraft process or retained world writes.""" + +import argparse +import copy +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch +from unittest.mock import Mock + +import lifecycle +import native117_lifecycle as native +from matrix import WORLD, digest, fingerprint, write_json +from native117 import RIG + + +class NativeLifecycleTests(unittest.TestCase): + def setUp(self): + root = RIG / "build/port-evidence" + root.mkdir(parents=True, exist_ok=True) + self.temp = tempfile.TemporaryDirectory(dir=root) + self.root = Path(self.temp.name) + + def tearDown(self): + self.temp.cleanup() + + def source_args(self): + source = self.root / "source" + world = source / WORLD + world.mkdir(parents=True) + (world / "level.dat").write_bytes(b"unit-test-world-hash-only") + mods = source / "mods" + mods.mkdir() + jar = mods / "archived-1.13.2.jar" + jar.write_bytes(b"unit-test-pin-not-a-runtime") + write_json(source / "input-jars.json", [{"file": jar.name, "sha256": digest(jar)}]) + history = {key: {"status": "PASS"} for key in ("conversion", "restart", "idempotence")} + write_json(source / "state-comparison.json", history) + write_json(source / "migration-fixture.json", []) + write_json(source / "migration-restarted.json", []) + manifest = self.root / "archived.json" + write_json(manifest, {"target113": {"seed": str(source), "launcher": "forge-1.13.2-25.0.223.jar", + "jars": [{"path": str(jar), "sha256": digest(jar)}]}}) + hashes = self.root / "source-world-hashes.json" + write_json(hashes, fingerprint(world)) + java = self.root / "java.exe" + java.write_bytes(b"not-launched") + target = self.root / "target.json" + write_json(target, {"target117": {"jars": [{"id": key} for key in + ("basesteam", "baseelectric", "basemetals")]}}) + return argparse.Namespace(source=source, source_manifest=manifest, source_key="target113", + source_hashes=hashes, target_manifest=target, java8=java, + output=self.root / "output", port=25580) + + def test_native_subclass_inherits_original_operations_fixtures_and_oracles(self): + self.assertIs(native.lifecycle.LifecycleRun, lifecycle.LifecycleRun) + self.assertIs(native.lifecycle.fixtures, lifecycle.fixtures) + for method in ("build", "midblock", "save_read_guards", "mining_results"): + self.assertIs(getattr(native.NativeLifecycleRun, method), getattr(lifecycle.LifecycleRun, method)) + definition = native.lifecycle.fixtures() + self.assertEqual(len(definition), 15) + drills = [entry for entry in definition if "from" in entry] + self.assertEqual({entry["id"] for entry in drills}, + {prefix + facing for prefix in ("S-MOVE-", "E-MOVE-") for facing in lifecycle.FACINGS}) + for entry in drills: + self.assertNotEqual((entry["from"][0] >> 4, entry["from"][2] >> 4), + (entry["to"][0] >> 4, entry["to"][2] >> 4)) + + def test_full_seven_groups_cannot_pass_missing_or_failed_drills(self): + definition = lifecycle.fixtures() + group = {entry["id"]: {"status": "PASS"} for entry in definition if "from" in entry} + groups = [copy.deepcopy(group) for _ in range(7)] + self.assertTrue(native.require_groups(definition, groups)) + self.assertFalse(native.require_groups(definition, [])) + groups[0].pop("S-MOVE-down") + self.assertFalse(native.require_groups(definition, groups)) + groups[0] = copy.deepcopy(group) + groups[6]["E-MOVE-up"]["status"] = "FAIL" + self.assertFalse(native.require_groups(definition, groups)) + + 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) + runner.boot.assert_called_once_with("lifecycle") + runner.probe.assert_called_once_with("trace-loads", "tracing-loads") + self.assertEqual([call[0] for call in runner.mock_calls], ["boot", "probe", "build"]) + + def test_source_preflight_is_read_only(self): + args = self.source_args() + before = fingerprint(args.source) + observed, history = native.verify_source(args) + self.assertEqual(before, observed) + self.assertEqual(before, fingerprint(args.source)) + self.assertEqual(history["restart"]["status"], "PASS") + + def test_changed_source_world_is_rejected(self): + args = self.source_args() + (args.source / WORLD / "level.dat").write_bytes(b"tampered") + with self.assertRaisesRegex(AssertionError, "fingerprint mismatch"): + native.verify_source(args) + + def test_source_mods_must_match_archived_manifest_and_recorded_inputs(self): + args = self.source_args() + (args.source / "mods" / "unexpected.jar").write_bytes(b"extra") + with self.assertRaisesRegex(AssertionError, "source jars differ"): + native.verify_source(args) + + def test_failed_source_state_is_not_reclassified(self): + args = self.source_args() + history = native.read_json(args.source / "state-comparison.json") + history["restart"]["status"] = "FAIL" + write_json(args.source / "state-comparison.json", history) + with self.assertRaisesRegex(AssertionError, "state gate did not pass"): + native.verify_source(args) + + def test_world_hash_keys_are_portable_without_ignoring_files(self): + native.require_equal_hashes({"region/r.0.0.mca": "abc"}, {"region\\r.0.0.mca": "ABC"}, "mismatch") + for actual in ({}, {"region/r.0.0.mca": "DEF"}, {"region/r.0.0.mca": "ABC", "extra": "ABC"}): + with self.assertRaisesRegex(AssertionError, "mismatch"): + native.require_equal_hashes(actual, {"region/r.0.0.mca": "ABC"}, "mismatch") + + def test_wrong_runtime_and_errors_fail_native_boot_check(self): + log = self.root / "console.log" + good = ("Starting minecraft server version 1.17.1\n" + "Forge mod loading, version 37.1.1, for MC 1.17.1 with MCP 20210706.113038\nDone\n") + log.write_text(good) + native.require_native_boot(log) + notice = "[Thread-1/DEBUG] [ne.mi.co.ForgeConfig/CORE]: Forge config just got changed on the file system!\n" + log.write_text(good + notice) + native.require_native_boot(log) + for text in (good.replace("1.17.1", "1.13.2"), good + "[Server thread/ERROR] failure\n", + good + "[Server thread/FATAL] failure\n", good + notice.replace("/DEBUG]", "/FATAL]"), + good + notice.replace("/DEBUG]", "/FATAL]").replace("ForgeConfig/CORE", "Other/CORE")): + log.write_text(text) + with self.assertRaises(AssertionError): + native.require_native_boot(log) + + def test_staged_extra_or_changed_jars_fail(self): + runtime = self.root / "runtime" + (runtime / "mods").mkdir(parents=True) + seed = self.root / "seed" + seed.mkdir() + (seed / native.LAUNCHER).parent.mkdir(parents=True, exist_ok=True) + (seed / native.LAUNCHER).write_bytes(b"launcher") + (runtime / native.LAUNCHER).parent.mkdir(parents=True, exist_ok=True) + (runtime / native.LAUNCHER).write_bytes(b"launcher") + jar = self.root / "candidate.jar" + jar.write_bytes(b"candidate") + (runtime / "mods" / jar.name).write_bytes(jar.read_bytes()) + write_json(runtime / "input-jars.json", [{"file": jar.name, "sha256": digest(jar)}]) + profile = (seed, native.LAUNCHER, [jar]) + native.verify_staged(runtime, profile) + (runtime / "mods" / "extra.jar").write_bytes(b"unexpected") + with self.assertRaisesRegex(AssertionError, "jar set changed"): + native.verify_staged(runtime, profile) + + def test_failures_are_retained_both_profiles_attempted_and_source_rechecked(self): + args = self.source_args() + source_profile = native.pinned_profile(args.source_manifest, args.source_key) + native_profile = (self.root / "native-seed", native.LAUNCHER, []) + native_profile[0].mkdir() + before = fingerprint(args.source) + + def profile(path, key): + return native_profile if key == "target117" else source_profile + + with patch.object(native, "pinned_profile", side_effect=profile), \ + patch.object(native.socket, "socket"), \ + patch.object(native, "prepare", side_effect=RuntimeError("deliberate unit-test failure")) as prepare: + self.assertEqual(native.run(args), 1) + result = native.read_json(args.output / "native117-lifecycle-results.json") + self.assertEqual(prepare.call_count, 2) + self.assertEqual(result["status"], "FAIL") + self.assertTrue(result["source_fixture_unchanged"]) + self.assertTrue(result["helpers_unchanged"]) + self.assertEqual(result["sourceRuntimeBefore"], result["sourceRuntimeAfter"]) + self.assertEqual(fingerprint(args.source), before) + for row in result["profiles"].values(): + self.assertIn("deliberate unit-test failure", row["failure"]) + + +class NativeReadinessTests(unittest.TestCase): + def setUp(self): + root = RIG / "build/port-evidence" + root.mkdir(parents=True, exist_ok=True) + self.temp = tempfile.TemporaryDirectory(dir=root) + self.root = Path(self.temp.name) + self.definition = [dict(id="drill", min=[0, 0, 0], max=[15, 100, 15], + witnesses=[[1, 80, 1]], to=[1, 80, 1], block="basesteam:steam_drill")] + + def tearDown(self): + self.temp.cleanup() + + def snapshot(self, tick=100, loaded=True, loads=1, nbt="{}"): + return dict(worldTime=tick, chunks=[dict(pos=[0, 0], loaded=loaded, loads=loads, unloads=0)], + chunkEvents=[dict(operation="load", pos=[0, 0], worldTime=99)] if loads else [], + cases={"drill": [dict(pos=[1, 80, 1], loaded=loaded, nbt=nbt)]}) + + def runner(self, snapshots): + runner = native.NativeLifecycleRun(self.root, None, None, self.definition) + pending = iter(snapshots) + + def probe(operation, label): + value = copy.deepcopy(next(pending)) + path = self.root / "advantage-works-results" / f"lifecycle-{label}.json" + self.assertFalse(path.exists()) + write_json(path, value) + return value + + runner._raw_probe = Mock(side_effect=probe) + runner._round_trip_ready = Mock(return_value=["fixture-chunks-ready"]) + return runner + + def test_readiness_failure_does_not_run_the_save_read_mutation(self): + runner = self.runner([]) + runner._round_trip_ready.side_effect = TimeoutError("entity sections not ticking") + with self.assertRaisesRegex(TimeoutError, "entity sections not ticking"): + runner.probe("legacy-read", "first") + runner._raw_probe.assert_not_called() + self.assertEqual("FAIL", runner.observations[-1]["status"]) + + def test_round_trip_readiness_watches_exact_fixture_chunks(self): + runner = native.NativeLifecycleRun(self.root, None, None, self.definition) + 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") + runner.server.send.assert_not_called() + + def test_readiness_does_not_inspect_machine_nbt_block_or_progress(self): + first, ready = self.snapshot(), self.snapshot(101, nbt="not even parsed") + runner = self.runner([ready]) + with patch.object(native.time, "sleep"): + selected = runner._wait_ready("test", first) + self.assertEqual(selected, ready) + self.assertEqual(runner.observations[-1]["first_ready"]["world_time"], 101) + self.assertEqual(native.loaded_issues(ready, self.definition), []) + + def unloaded(self, tick=101, callbacks=0, events=False): + value = self.snapshot(tick, loaded=False) + value["chunks"][0]["unloads"] = callbacks + if events: + value["chunkEvents"].append(dict(operation="unload", pos=[0, 0], worldTime=tick)) + return value + + def unload_runner(self, snapshots, deadline=45): + runner = self.runner(snapshots) + runner._unload_context = dict(name="cycle", before=self.snapshot(), deadline=deadline) + return runner + + def test_absent_holder_waits_for_queued_real_unload_callback(self): + pending, complete = self.unloaded(), self.unloaded(102, callbacks=1, events=True) + runner = self.unload_runner([pending, complete]) + with patch.object(native.time, "sleep"), patch.object(native.time, "monotonic", return_value=0): + selected = runner.probe("snapshot", "cycle-absent-00") + self.assertEqual(selected, complete) + self.assertEqual(runner._raw_probe.call_count, 2) + lifecycle.require_unloaded(self.snapshot(), selected) + self.assertEqual(native.read_json(self.root / "advantage-works-results/lifecycle-cycle-absent-00.json"), complete) + self.assertTrue((self.root / "advantage-works-results/lifecycle-cycle-absent-00-native-initial.json").exists()) + + def test_queued_callback_wait_never_extends_original_unload_budget(self): + pending = self.unloaded() + runner = self.unload_runner([pending], deadline=1) + with patch.object(native.time, "sleep") as sleep, patch.object(native.time, "monotonic", return_value=1): + selected = runner.probe("snapshot", "cycle-absent-44") + self.assertEqual(selected, pending) + sleep.assert_not_called() + with self.assertRaisesRegex(AssertionError, "No real unload event"): + lifecycle.require_unloaded(self.snapshot(), selected) + + def test_still_loaded_snapshot_returns_to_inherited_polling(self): + loaded = self.snapshot(101) + runner = self.unload_runner([loaded]) + with patch.object(native.time, "sleep") as sleep: + selected = runner.probe("snapshot", "cycle-absent-00") + self.assertEqual(selected, loaded) + sleep.assert_not_called() + + def test_unload_counter_without_an_actual_event_is_not_ready(self): + pending = self.unloaded(callbacks=1) + runner = self.unload_runner([pending], deadline=0) + with patch.object(native.time, "monotonic", return_value=0): + runner.probe("snapshot", "cycle-absent-00") + self.assertEqual(runner.observations[-1]["status"], "PENDING") + self.assertTrue(native.unload_event_issues(self.snapshot(), pending)) + + def test_unload_context_cannot_leak_to_another_cycle_after_failure(self): + runner = self.runner([]) + with patch.object(lifecycle.LifecycleRun, "unload_reload", side_effect=RuntimeError("probe failure")): + with self.assertRaisesRegex(RuntimeError, "probe failure"): + runner.unload_reload("cycle") + self.assertIsNone(runner._unload_context) + + def test_unload_wrapper_delegates_the_original_protocol_exactly_once(self): + runner = self.runner([]) + result = object() + + def original(instance, name): + self.assertIs(instance, runner) + self.assertEqual(name, "cycle") + self.assertEqual(instance._unload_context, dict(name="cycle", before=None, deadline=None)) + return result + + with patch.object(lifecycle.LifecycleRun, "unload_reload", autospec=True, side_effect=original) as call: + self.assertIs(runner.unload_reload("cycle"), result) + call.assert_called_once_with(runner, "cycle") + self.assertIsNone(runner._unload_context) + + def test_unload_budget_is_anchored_to_release_and_not_extended_by_samples(self): + runner = self.unload_runner([self.unloaded(), self.unloaded()]) + with patch.object(native.time, "monotonic", return_value=10): + runner.probe("release", "cycle-released") + self.assertEqual(runner._unload_context["deadline"], 55) + with patch.object(native.time, "monotonic", return_value=55): + runner.probe("snapshot", "cycle-absent-44") + self.assertEqual(runner._unload_context["deadline"], 55) + + def test_reload_waits_for_genuine_load_then_first_tick_without_reading_values(self): + absent = self.unloaded(callbacks=1, events=True) + initial, ready = self.snapshot(102, loads=2), self.snapshot(103, loads=2, nbt="{progress:0s}") + runner = self.unload_runner([initial, ready]) + runner._unload_context["absent"] = absent + with patch.object(native.time, "sleep"): + selected = runner.probe("load", "cycle-loaded") + self.assertEqual(selected, ready) + self.assertEqual(runner._raw_probe.call_args_list[0].args[0], "load") + self.assertEqual(native.read_json(self.root / "advantage-works-results/lifecycle-cycle-loaded.json"), ready) + + def test_load_wait_uses_first_clock_tick_even_when_desired_values_arrive_later(self): + immediate = self.snapshot(nbt="{progress:12s}") + same_tick = self.snapshot(nbt="{progress:12s}") + first_tick = self.snapshot(101, nbt="{progress:0s}") + later_pass = self.snapshot(102, nbt="{progress:13s}") + runner = self.runner([same_tick, first_tick, later_pass]) + with patch.object(native.time, "sleep"), \ + patch.object(lifecycle, "progress_results", side_effect=AssertionError("oracle inside wait")), \ + patch.object(lifecycle, "changed_target_results", side_effect=AssertionError("oracle inside wait")): + selected = runner._wait_ready("load", immediate) + self.assertEqual(selected, first_tick) + self.assertEqual(runner._raw_probe.call_count, 2) + self.assertNotEqual(selected, later_pass) + record = runner.observations[-1] + source = self.root / "advantage-works-results" / ("lifecycle-" + record["selected_label"] + ".json") + self.assertEqual(native.read_json(source), first_tick) + self.assertEqual([sample["world_time"] for sample in record["samples"]], [100, 100, 101]) + + def round_trip(self, snapshot, operation, label): + row = snapshot["cases"]["drill"][0] + row["tileIdentity"] = 42 + snapshot["roundTrip"] = dict(protocol=native.ROUND_TRIP_PROTOCOL, status="PASS", operation=operation, + label=label, dimension="minecraft:overworld", startPhase="START", endPhase="END", mutationCount=1, + startWorldTime=snapshot["worldTime"] - 1, endWorldTime=snapshot["worldTime"], + prerequisites={"drill": dict(tileIdentity=42, flags={key: True for key in native.ROUND_TRIP_FLAGS})}, + callbacks={"drill": dict(calls=1, completed=1, valid=True, nativeStackValidated=True, + prerequisiteFlags={key: True for key in native.CALLBACK_FLAGS}, + worldTime=snapshot["worldTime"], before=copy.deepcopy(row), after=copy.deepcopy(row), + nativeStack=["net.minecraft.world.level.chunk.LevelChunk$BoundTickingBlockEntity.tick(LevelChunk.java:844)", + "net.minecraft.world.level.Level.tickBlockEntities(Level.java:488)"])}) + return snapshot + + def test_changed_read_is_not_reissued_or_retried_until_target_clears(self): + retargeted = self.round_trip(self.snapshot(101, nbt="{progress:1s,targetX:2,targetY:80,targetZ:1}"), + "changed-target-read", "changed") + runner = self.runner([retargeted, self.snapshot(108, nbt="{progress:0s}")]) + with patch.object(native.time, "sleep"): + self.assertEqual(runner.probe("changed-target-read", "changed"), retargeted) + self.assertEqual([call.args[0] for call in runner._raw_probe.call_args_list], + ["changed-target-read"]) + + def guard_samples(self, changed_items="[]"): + old = '{progress:12s,targetX:2,targetY:80,targetZ:1,Items:[],TargetState:{Name:"minecraft:stone"}}' + advanced = old.replace("12s", "13s") + cleared = "{progress:0s,Items:" + changed_items + "}" + return [self.snapshot(nbt=old), + self.round_trip(self.snapshot(101, nbt=advanced), "legacy-read", "save-read-legacy"), + self.round_trip(self.snapshot(102, nbt=cleared), "changed-target-read", "save-read-changed")] + + def test_inherited_guard_calls_each_unchanged_oracle_once_after_selection(self): + runner = self.runner(self.guard_samples()) + with patch.object(native.time, "sleep"), \ + patch.object(lifecycle, "progress_results", wraps=lifecycle.progress_results) as progress, \ + patch.object(lifecycle, "changed_target_results", wraps=lifecycle.changed_target_results) as changed: + results = runner.save_read_guards() + self.assertEqual(progress.call_count, 1) + self.assertEqual(changed.call_count, 1) + self.assertEqual(results["legacy_read"]["drill"]["status"], "PASS") + self.assertEqual(results["changed_target_read"]["drill"]["status"], "PASS") + + def test_inherited_unchanged_inventory_guard_still_fails(self): + runner = self.runner(self.guard_samples('[{id:"minecraft:dirt",Count:1b}]')) + with patch.object(native.time, "sleep"): + with self.assertRaisesRegex(AssertionError, "changed drill inventory"): + runner.save_read_guards() + + def test_loaded_chunks_need_actual_load_events_and_counter_increase(self): + before = self.snapshot(loaded=False, loads=1) + loaded = self.snapshot(101, loads=1) + self.assertTrue(native.load_event_issues(before, loaded)) + loaded["chunks"][0]["loads"] = 2 + self.assertEqual(native.load_event_issues(before, loaded), []) + loaded["chunkEvents"] = [] + self.assertTrue(native.load_event_issues(before, loaded)) + + def test_tick_gate_starts_only_once_all_witnesses_are_loaded(self): + first = self.snapshot(loaded=False, loads=0) + loaded, ready = self.snapshot(101), self.snapshot(102) + runner = self.runner([loaded, ready]) + with patch.object(native.time, "sleep"): + self.assertEqual(runner._wait_ready("test", first, reload_before=first), ready) + samples = runner.observations[-1]["samples"] + self.assertIsNone(samples[0]["loaded_since_world_time"]) + self.assertEqual(samples[1]["loaded_since_world_time"], 101) + + def test_missing_witness_and_missing_chunk_are_never_ready(self): + for key in ("cases", "chunks"): + value = self.snapshot() + value[key] = {} if key == "cases" else [] + self.assertTrue(native.loaded_issues(value, self.definition)) + + def test_stalled_native_time_fails_bounded_without_synthetic_advancement(self): + runner = self.runner([self.snapshot()] * 4) + with patch.object(native, "READINESS_SAMPLES", 3), patch.object(native.time, "sleep"): + with self.assertRaisesRegex(TimeoutError, "actual ticks"): + runner._wait_ready("test", self.snapshot()) + self.assertEqual(runner.observations[-1]["status"], "FAIL") + self.assertTrue(all(row["world_time"] == 100 for row in runner.observations[-1]["samples"])) + + def test_wall_guard_cannot_be_passed_by_late_tick(self): + runner = self.runner([self.snapshot(101)]) + with patch.object(native.time, "monotonic", side_effect=[0, 0, 21]), patch.object(native.time, "sleep"): + with self.assertRaises(TimeoutError): + runner._wait_ready("test", self.snapshot()) + + 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"), \ + patch.object(native.time, "sleep"): + runner.boot("midblock-restart") + boot.assert_called_once_with(runner.runtime, runner.java, runner.launcher, "midblock-restart") + 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") + + def test_initial_and_frozen_boot_never_rebuild_or_load_lifecycle_fixture(self): + runner = self.runner([]) + with patch("native117_code.Server"): + runner.boot("lifecycle") + runner.boot("retained13-import") + runner._raw_probe.assert_not_called() + + def test_raw_snapshot_overwrite_rejected(self): + runner = native.NativeLifecycleRun(self.root, None, None, self.definition) + write_json(self.root / "advantage-works-results/lifecycle-original.json", self.snapshot()) + with patch.object(lifecycle.LifecycleRun, "probe") as original: + with self.assertRaises(FileExistsError): + runner._raw_probe("snapshot", "original") + original.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_lifecycle_roundtrip.py b/test-rig/migration/test_native117_lifecycle_roundtrip.py new file mode 100644 index 00000000..6cfb9b84 --- /dev/null +++ b/test-rig/migration/test_native117_lifecycle_roundtrip.py @@ -0,0 +1,160 @@ +"""First native callback receipts, independent of eventual machine outcomes.""" + +import copy +from pathlib import Path +import tempfile +import unittest +from unittest.mock import Mock + +import lifecycle +import native117_lifecycle as native +from matrix import write_json + + +class FirstCallbackTests(unittest.TestCase): + definition = [{"id": "drill", "to": [1, 80, 1]}] + + def runner(self, folder): + runner = native.NativeLifecycleRun(Path(folder), None, None, self.definition) + runner._round_trip_ready = Mock(return_value=["fixture-chunks-ready"]) + return runner + + def document(self, nbt='{progress:12s,targetX:2,targetY:80,targetZ:1,TargetState:{Name:"minecraft:stone"}}'): + row = dict(pos=[1, 80, 1], loaded=True, tileIdentity=42, nbt=nbt) + flags = {name: True for name in native.ROUND_TRIP_FLAGS} + return dict(worldTime=101, cases={"drill": [row]}, roundTrip=dict( + protocol=native.ROUND_TRIP_PROTOCOL, operation="changed-target-read", label="changed", + status="PASS", dimension="minecraft:overworld", startPhase="START", endPhase="END", + startWorldTime=100, endWorldTime=101, mutationCount=1, + prerequisites={"drill": dict(flags=flags, tileIdentity=42)}, + callbacks={"drill": dict(calls=1, completed=1, valid=True, worldTime=101, + prerequisiteFlags={key: True for key in native.CALLBACK_FLAGS}, + nativeStack=["net.minecraft.world.level.chunk.LevelChunk$BoundTickingBlockEntity.tick(LevelChunk.java:844)", + "net.minecraft.world.level.Level.tickBlockEntities(Level.java:488)"], + nativeStackValidated=True, before=copy.deepcopy(row), after=row)})) + + def check(self, doc): + return native.require_round_trip(doc, self.definition, "changed-target-read", "changed") + + def test_complete_natural_callback_is_accepted_even_when_strict_machine_oracle_fails(self): + doc = self.document() + self.assertIs(self.check(doc), doc) + self.assertEqual(lifecycle.changed_target_results(self.definition, doc, doc)["drill"]["status"], "FAIL") + + def test_clock_advance_without_callback_proof_is_rejected(self): + doc = self.document() + del doc["roundTrip"] + with self.assertRaises(AssertionError): + self.check(doc) + + def test_each_prerequisite_is_required(self): + for name in native.ROUND_TRIP_FLAGS: + for value in (False, None, 1): + doc = self.document() + doc["roundTrip"]["prerequisites"]["drill"]["flags"][name] = value + with self.subTest(name=name, value=value), self.assertRaises(AssertionError): + self.check(doc) + + def test_wrong_phase_tick_world_operation_label_or_incomplete_dispatch_fails(self): + for key, value in (("endPhase", "START"), ("startPhase", "END"), ("endWorldTime", 108), + ("dimension", "minecraft:the_nether"), ("operation", "legacy-read"), + ("label", "stale"), ("mutationCount", 2), ("status", "FAIL")): + doc = self.document() + doc["roundTrip"][key] = value + with self.subTest(key=key), self.assertRaises(AssertionError): + self.check(doc) + for key, value in (("calls", 0), ("calls", 2), ("completed", 0), ("valid", False), + ("nativeStackValidated", False), ("worldTime", 108)): + doc = self.document() + doc["roundTrip"]["callbacks"]["drill"][key] = value + with self.subTest(key=key), self.assertRaises(AssertionError): + self.check(doc) + + def test_missing_duplicate_replaced_or_changed_first_callback_is_rejected(self): + for change in ("missing", "extra", "replaced", "later"): + doc = self.document() + if change == "missing": + doc["roundTrip"]["callbacks"].clear() + elif change == "extra": + doc["roundTrip"]["callbacks"]["other"] = doc["roundTrip"]["callbacks"]["drill"] + elif change == "replaced": + doc["roundTrip"]["prerequisites"]["drill"]["tileIdentity"] = 99 + else: + doc["cases"]["drill"][0] = {**doc["cases"]["drill"][0], "nbt": "{progress:0s}"} + with self.subTest(change=change), self.assertRaises(AssertionError): + self.check(doc) + + def test_probe_issues_one_operation_and_never_resamples_a_failed_first_machine_result(self): + with tempfile.TemporaryDirectory() as folder: + runner = self.runner(folder) + doc = self.document() + def probe(operation, label): + write_json(Path(folder) / "advantage-works-results" / ("lifecycle-" + label + ".json"), doc) + return doc + runner._raw_probe = Mock(side_effect=probe) + runner._wait_ready = Mock(side_effect=AssertionError("Clock-based resampling forbidden")) + self.assertIs(runner.probe("changed-target-read", "changed"), doc) + runner._round_trip_ready.assert_called_once_with("changed") + runner._raw_probe.assert_called_once_with("changed-target-read", "changed") + runner._wait_ready.assert_not_called() + + def test_failed_first_legacy_callback_blocks_changed_mutation_and_is_not_observer_repaired(self): + with tempfile.TemporaryDirectory() as folder: + runner = self.runner(folder) + doc = self.document('{progress:227s,targetX:2,targetY:80,targetZ:1}') + doc["roundTrip"].update(operation="legacy-read", label="save-read-legacy") + runner._raw_probe = Mock(side_effect=[doc, doc, self.document("{progress:0s}")]) + with self.assertRaisesRegex(AssertionError, "First eligible natural callback failed legacy restoration"): + runner.save_read_guards() + self.assertEqual([call.args[0] for call in runner._raw_probe.call_args_list], ["snapshot", "legacy-read"]) + self.assertEqual(runner.observations[-1]["classification"], "ELIGIBLE_FIRST_CALLBACK_PRODUCTION_GUARD_FAILURE") + + def test_zero_energy_cannot_delay_the_first_callback_or_make_a_bad_machine_result_pass(self): + doc = self.document() + for row in [doc["cases"]["drill"][0], doc["roundTrip"]["callbacks"]["drill"]["before"]]: + row["energy"] = {"steam": 0} + self.check(doc) + self.assertEqual(lifecycle.changed_target_results(self.definition, doc, doc)["drill"]["status"], "FAIL") + + def test_stack_flags_cannot_replace_the_actual_native_dispatch_frames(self): + for stack in ([], ["Harness.forceTick(Test.java:1)"], ["net.minecraft.world.level.Level.tickBlockEntities(Level.java:1)"], + ["net.minecraft.world.level.chunk.LevelChunk$BoundTickingBlockEntity.tick(LevelChunk.java:844)", + "net.minecraft.world.level.Level.(Level.java:1)"], + ["net.minecraft.world.level.chunk.LevelChunk$BoundTickingBlockEntity.tick(LevelChunk.java:844)", + "net.minecraft.world.level.Level.setBlock(Level.java:1)"]): + doc = self.document() + doc["roundTrip"]["callbacks"]["drill"]["nativeStack"] = stack + with self.assertRaises(AssertionError): + self.check(doc) + + def test_callback_prerequisites_cannot_be_substituted_by_earlier_start_flags(self): + for key in native.CALLBACK_FLAGS: + doc = self.document() + doc["roundTrip"]["callbacks"]["drill"]["prerequisiteFlags"][key] = False + with self.subTest(key=key), self.assertRaises(AssertionError): + self.check(doc) + + def test_wrong_position_or_unloaded_callback_row_is_rejected(self): + for key, value in (("pos", [2, 80, 1]), ("loaded", False)): + doc = self.document() + doc["roundTrip"]["callbacks"]["drill"]["before"][key] = value + with self.assertRaises(AssertionError): + self.check(doc) + + def test_legacy_first_callback_progress_or_target_loss_blocks_changed_read(self): + for nbt in ('{progress:0s,TargetState:{Name:"minecraft:stone"}}', + '{progress:12s,targetX:99,targetY:80,targetZ:1,TargetState:{Name:"minecraft:stone"}}'): + with tempfile.TemporaryDirectory() as folder: + runner = self.runner(folder) + doc = self.document() + doc["roundTrip"].update(operation="legacy-read", label="legacy") + doc["roundTrip"]["callbacks"]["drill"]["after"]["nbt"] = nbt + runner._raw_probe = Mock(return_value=doc) + with self.assertRaisesRegex(AssertionError, "First eligible natural callback failed legacy restoration"): + runner.probe("legacy-read", "legacy") + runner._raw_probe.assert_called_once() + self.assertEqual(runner.observations[-1]["classification"], "ELIGIBLE_FIRST_CALLBACK_PRODUCTION_GUARD_FAILURE") + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_machine_items.py b/test-rig/migration/test_native117_machine_items.py new file mode 100644 index 00000000..06151592 --- /dev/null +++ b/test-rig/migration/test_native117_machine_items.py @@ -0,0 +1,1043 @@ +"""Headless safety/oracle tests; synthetic NBT never substitutes for leased native proof.""" + +import base64 +import copy +import gzip +import io +import json +import os +from pathlib import Path +import shutil +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch +import zipfile +import zlib + +import nbtlib + +import matrix +import native117_code +import native117_machine_items as machine +import native117_observation_plan as observation + + +def encode(value): + stream = io.BytesIO() + nbtlib.File(nbtlib.parse_nbt(machine.snbt(value))).write(stream) + return base64.b64encode(gzip.compress(stream.getvalue())).decode() + + +def snapshot(case="wool14", source="original110", modern=False, ticks=0): + blocks = [dict(id="MI-" + str(index), station="machine-items", kind=name.split(":")[1], + name=name, x=index * 4, y=80, z=0) for index, name in enumerate(machine.MACHINES)] + definition = dict(format=1, sourceProfile=source, case=case, bounds=[0, 80, 0, 12, 80, 0], blocks=blocks) + document = dict(format=1, held=True, requestedTicks=ticks, advancedTicks=ticks, + buildCallsThisProcess=0, snapshot=copy.deepcopy(blocks)) + for row in document["snapshot"]: + stacks = machine.inventory(case, row["name"], "Chest" if source.endswith("110") else "minecraft:chest") + if modern: + stacks = [machine.modern_item(stack) for stack in stacks] + tile = dict(id=row["name"] if modern else row["name"].replace(":", "."), + x=row["x"], y=row["y"], z=row["z"], Items=stacks, + ForgeData=dict(AdvConnectedWitness=row["id"])) + row.update(actualName=row["name"], properties={"facing": "north"}, witness=row["id"], + objectIdentity=100 + row["x"], nbtBase64=encode(tile)) + return definition, document + + +def mutate_items(document, callback): + row = document["snapshot"][0] + tile = matrix.plain(matrix.parse_nbt(row)) + callback(tile["Items"]) + row["nbtBase64"] = encode(tile) + + +class ItemExpectationsTest(unittest.TestCase): + def test_known_metadata_is_identity_not_tool_damage(self): + for case, identity, count in (("wool14", "red_wool", 7), ("coal1", "charcoal", 5), + ("dye4", "lapis_lazuli", 9)): + with self.subTest(case=case): + self.assertEqual(machine.modern_item(machine.items(case)), dict(id="minecraft:" + identity, Count=count)) + + def test_tool_damage_and_both_enchantments_remain_exact(self): + actual = machine.modern_item(machine.items("enchanted-pick")) + self.assertNotIn("Damage", actual) + self.assertEqual(actual["tag"]["Damage"], 17) + self.assertEqual(actual["tag"]["Enchantments"], [dict(id="minecraft:efficiency", lvl=3), + dict(id="minecraft:unbreaking", lvl=2)]) + self.assertEqual(actual["tag"]["RepairCost"], 4) + self.assertEqual(actual["tag"]["AdvMachineItems"], dict(note="unchanged", number=19)) + + def test_stored_book_enchantments_do_not_become_regular_enchantments(self): + value = machine.modern_item(machine.items("enchanted-book")) + self.assertEqual(set(value["tag"]), {"StoredEnchantments"}) + self.assertEqual(value["tag"]["StoredEnchantments"][0], dict(id="minecraft:efficiency", lvl=3)) + + def test_nested_chest_has_all_five_actual_legacy_witnesses(self): + for identity in ("Chest", "minecraft:chest"): + old = machine.items("nested-chest", identity) + fixed = machine.modern_item(old)["tag"]["BlockEntityTag"] + self.assertEqual(fixed["id"], "minecraft:chest") + self.assertEqual(fixed["Items"], [dict(Slot=index, **machine.modern_item(machine.items(case))) + for index, case in enumerate(machine.CASES[:-1])]) + self.assertEqual(old["tag"]["BlockEntityTag"]["id"], identity) + + def test_unknown_metadata_and_enchantment_are_not_silently_normalized(self): + with self.assertRaises(ValueError): + machine.modern_item(dict(id="minecraft:wool", Count=7, Damage=13)) + old = machine.items("enchanted-pick") + old["tag"]["ench"][0]["id"] = 99 + with self.assertRaises(KeyError): + machine.modern_item(old) + + def test_exact_source_and_destination_all_cases_both_versions(self): + for case in machine.CASES: + for source in ("original110", "legacy112"): + for modern in (False, True): + with self.subTest(case=case, source=source, modern=modern): + definition, document = snapshot(case, source, modern) + self.assertEqual(len(machine.document_items(document, definition, modern=modern)), 4) + + def test_archived_source_uses_actual_array_without_fabricating_native_flags(self): + definition, document = snapshot("nested-chest") + rows = document["snapshot"] + for row in rows: + row.pop("objectIdentity") + row.pop("witness") + self.assertEqual(len(machine.source_items(rows, definition)), 4) + with self.assertRaisesRegex(AssertionError, "actual snapshot array"): + machine.source_items(document, definition) + tile = matrix.plain(matrix.parse_nbt(rows[0])) + tile.pop("ForgeData") + rows[0]["nbtBase64"] = encode(tile) + with self.assertRaisesRegex(AssertionError, "NBT source witness"): + machine.source_items(rows, definition) + + def test_failed_source_command_cannot_create_a_fresh_baseline(self): + definition, document = snapshot() + mutate_items(document, lambda stacks: stacks[0].update(id="minecraft:dirt", Count=13, Damage=0)) + with self.assertRaisesRegex(AssertionError, "Exact item mismatch"): + machine.document_items(document, definition, modern=False) + + def test_unfixed_legacy_metadata_fails_native_oracle(self): + for case in ("wool14", "coal1", "dye4"): + definition, document = snapshot(case) + with self.subTest(case=case), self.assertRaises(AssertionError): + machine.document_items(document, definition, modern=True) + + def test_count_slot_tags_and_damage_mutations_fail(self): + for mutate in (lambda stacks: stacks.clear(), lambda stacks: stacks.append(copy.deepcopy(stacks[0])), + lambda stacks: stacks[0].update(Count=2), lambda stacks: stacks[0].update(Slot=2), + lambda stacks: stacks[0]["tag"].update(Damage=0), + lambda stacks: stacks[0]["tag"]["Enchantments"][0].update(lvl=2), + lambda stacks: stacks[0]["tag"].pop("AdvMachineItems")): + definition, document = snapshot("enchanted-pick", modern=True) + mutate_items(document, mutate) + with self.subTest(mutate=mutate), self.assertRaises(AssertionError): + machine.document_items(document, definition, modern=True) + + def test_nested_item_loss_wrong_identity_and_zeroed_tool_fail(self): + for mutate in (lambda values: values.pop(), lambda values: values[0].update(id="minecraft:wool"), + lambda values: values[3]["tag"].update(Damage=0), + lambda values: values[4]["tag"]["StoredEnchantments"][0].update(id="minecraft:protection")): + definition, document = snapshot("nested-chest", modern=True) + mutate_items(document, lambda stacks: mutate(stacks[0]["tag"]["BlockEntityTag"]["Items"])) + with self.subTest(mutate=mutate), self.assertRaises(AssertionError): + machine.document_items(document, definition, modern=True) + + def test_filter_template_slot_is_preserved_not_ignored_as_ghost(self): + definition, document = snapshot("nested-chest", modern=True) + row = document["snapshot"][1] + tile = matrix.plain(matrix.parse_nbt(row)) + self.assertEqual([stack["Slot"] for stack in tile["Items"]], [0, 1]) + tile["Items"].pop() + row["nbtBase64"] = encode(tile) + with self.assertRaises(AssertionError): + machine.document_items(document, definition, modern=True) + + +class SafetyTest(unittest.TestCase): + def test_source_commands_use_only_blockdata_for_the_existing_four_tiles(self): + definition, _ = snapshot("nested-chest") + commands = machine.seed_commands(definition) + self.assertEqual(len(commands), 4) + for command, spec in zip(commands, definition["blocks"]): + prefix = "blockdata %d %d %d " % (spec["x"], spec["y"], spec["z"]) + self.assertTrue(command.startswith(prefix)) + self.assertEqual(set(nbtlib.parse_nbt(command[len(prefix):])), {"Items", "ForgeData"}) + machine.require_command(command, source_seeds=commands) + with self.assertRaises(AssertionError): + machine.require_command(command) + machine.require_command("advprobe snapshot source", source_seeds=commands) + with self.assertRaises(AssertionError): + machine.require_command("advconnected snapshot source", source_seeds=commands) + + def test_destination_mutators_multiline_and_build_are_rejected(self): + for command in ("advconnected build source", "advprobe build x", "advworks build all", + "setblock 0 80 0 minecraft:chest", "blockdata 0 80 0 {Items:[]}", + "data merge block 0 80 0 {Items:[]}", "replaceitem block 0 80 0 slot.container.0 stone", + "advconnected snapshot x\nstop", "advconnected step 0 x", "advconnected step 4001 x"): + with self.subTest(command=command), self.assertRaises(AssertionError): + machine.require_command(command, operated=True) + + def test_destination_operation_locked_until_preservation_passes(self): + with self.assertRaises(AssertionError): + machine.require_command("advconnected step 512 operated") + machine.require_command("advconnected step 512 operated", operated=True) + for command in ("stop", "save-all flush", "advconnected snapshot source"): + machine.require_command(command) + + def test_rebuild_unfrozen_wrong_tick_and_missing_witness_fail(self): + for field, value in (("held", False), ("buildCallsThisProcess", 1), ("advancedTicks", 1), + ("requestedTicks", 1)): + definition, document = snapshot() + document[field] = value + with self.subTest(field=field), self.assertRaises(AssertionError): + machine.document_items(document, definition, modern=False) + definition, document = snapshot() + document["snapshot"].pop() + with self.assertRaises(AssertionError): + machine.document_items(document, definition, modern=False) + + def test_moved_or_replaced_machine_and_missing_identity_fail(self): + for field, value in (("x", 999), ("actualName", "minecraft:chest"), ("witness", "wrong"), + ("objectIdentity", None)): + definition, document = snapshot() + document["snapshot"][0][field] = value + with self.subTest(field=field), self.assertRaises(AssertionError): + machine.document_items(document, definition, modern=False) + + def test_exact_512_ticks_and_same_object_required(self): + definition, before = snapshot(modern=True) + _, after = snapshot(modern=True, ticks=512) + machine.document_items(after, definition, modern=True, ticks=512, previous=before) + after["snapshot"][0]["objectIdentity"] += 1 + with self.assertRaisesRegex(AssertionError, "replaced"): + machine.document_items(after, definition, modern=True, ticks=512, previous=before) + + def test_missing_or_duplicate_existing_source_positions_fail(self): + _, old = snapshot() + with self.assertRaises(AssertionError): + machine.select_definition(old["snapshot"][:-1], "wool14", "original110") + old["snapshot"][1].update(x=0) + with self.assertRaises(AssertionError): + machine.select_definition(old["snapshot"], "wool14", "original110") + + def test_plan_selects_existing_machines_without_construction_seeds(self): + _, old = snapshot() + definition = machine.select_definition(old["snapshot"], "wool14", "original110") + self.assertEqual(len(definition["blocks"]), 4) + self.assertFalse(any("seed" in row for row in definition["blocks"])) + + def test_source_and_destination_cli_require_explicit_lease(self): + for mode in ("source", "destination"): + with self.subTest(mode=mode), patch("sys.stderr", new=io.StringIO()), self.assertRaises(SystemExit): + machine.parser().parse_args([mode, "--output", "new", "--java8", "java"]) + + def test_only_25574_can_be_used(self): + for port in (25575, 25576, 25595, 0): + with self.subTest(port=port), self.assertRaises(ValueError): + machine.require_lease(port) + + def test_new_output_cannot_overwrite_or_nest_in_protected_world(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + canonical = root / "canonical" + canonical.mkdir() + for output in (canonical, canonical / "child", root): + with self.subTest(output=output), self.assertRaises((ValueError, FileExistsError)): + machine.new_root(output, [canonical]) + self.assertEqual(machine.new_root(root / "new", [canonical]), root / "new") + + def test_wrong_manifest_hash_and_old_power_c_are_rejected(self): + with tempfile.TemporaryDirectory() as temp: + path = Path(temp) / "manifest.json" + matrix.write_json(path, {"target117": {"jars": [dict(id="poweradvantage", sourceRef="a" * 40, + sha256=machine.OLD_POWER_C)]}}) + with self.assertRaises(AssertionError): + machine.checked(path, "0" * 64) + with self.assertRaises(AssertionError): + machine.require_power(path, "a" * 40, machine.OLD_POWER_C) + value = json.loads(path.read_text()) + value["target117"]["jars"][0]["sha256"] = "B" * 64 + matrix.write_json(path, value) + machine.require_power(path, "a" * 40, "B" * 64) + with self.assertRaises(AssertionError): + machine.require_power(path, "c" * 40, "B" * 64) + + def test_failed_source_receipt_does_not_boot_destination(self): + with patch.object(machine, "source_receipt", side_effect=AssertionError("not closed")), \ + patch.object(machine.native117, "boot") as boot: + args = SimpleNamespace(source_receipt=Path("fake"), source_receipt_sha256="0" * 64) + with self.assertRaises(AssertionError): + machine.run_destination(args) + boot.assert_not_called() + + def test_boot_boundary_hold_never_launches_another_process(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + machine.hold_gate(root) + (root / "HOLD").touch() + with self.assertRaises(InterruptedError): + machine.hold_gate(root) + + def test_destination_mro_keeps_native_code_custody(self): + self.assertTrue(issubclass(machine.DestinationServer, machine.NativeServer)) + self.assertTrue(issubclass(machine.DestinationServer, machine.ObservedServer)) + with patch.object(native117_code.NativeLaunch, "__init__"), \ + patch("native117_code.verify_if_sealed") as verifier: + machine.DestinationServer(Path("new-unit-runtime"), Path("java"), "launcher", "test") + verifier.assert_called_once_with(Path("new-unit-runtime")) + + +class RawEvidenceTest(unittest.TestCase): + def chunk(self, tiles, version=2730): + root = dict(DataVersion=version, Level=dict(xPos=0, zPos=0, TileEntities=tiles)) + stream = io.BytesIO() + nbtlib.File(nbtlib.parse_nbt(machine.snbt(root))).write(stream) + payload = b"\x02" + zlib.compress(stream.getvalue()) + return len(payload).to_bytes(4, "big") + payload + + def test_malformed_saved_chunk_is_rejected(self): + for value in (b"", b"\0\0\0\x03\x02bad"): + with self.subTest(value=value), self.assertRaises((ValueError, zlib.error)): + machine.decode_chunk(value) + + def test_raw_stopped_chunks_independently_match_serializer_and_version(self): + definition, document = snapshot("nested-chest", modern=True) + tiles = [matrix.plain(matrix.parse_nbt(row)) for row in document["snapshot"]] + with tempfile.TemporaryDirectory() as temp, patch.object(machine, "read_region") as region: + region.return_value = (None, {0: self.chunk(tiles)}) + hashes = machine.stopped_evidence(Path(temp), "converted", definition, document, modern=True) + self.assertIn("0_0.chunk", hashes) + self.assertEqual(region.call_count, 1) + + def test_air_drop_nested_damage_reset_wrong_version_and_duplicate_tiles_fail(self): + definition, document = snapshot("nested-chest", modern=True) + original = [matrix.plain(matrix.parse_nbt(row)) for row in document["snapshot"]] + mutations = [lambda tiles: tiles[0]["Items"].clear(), + lambda tiles: tiles[0]["Items"][0]["tag"]["BlockEntityTag"]["Items"][3]["tag"].update(Damage=0), + lambda tiles: tiles.append(copy.deepcopy(tiles[0]))] + for mutation in mutations: + tiles = copy.deepcopy(original) + mutation(tiles) + with tempfile.TemporaryDirectory() as temp, patch.object(machine, "read_region", return_value=(None, {0: self.chunk(tiles)})): + with self.assertRaises(AssertionError): + machine.stopped_evidence(Path(temp), "converted", definition, document, modern=True) + with tempfile.TemporaryDirectory() as temp, patch.object(machine, "read_region", return_value=(None, {0: self.chunk(original, 512)})): + with self.assertRaisesRegex(AssertionError, "DataVersion"): + machine.stopped_evidence(Path(temp), "converted", definition, document, modern=True) + + +class PreparationTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.canonical = self.root / "canonical" + (self.canonical / matrix.WORLD).mkdir(parents=True) + level = nbtlib.parse_nbt('{Data:{DataVersion:512},FML:{Registries:{"minecraft:enchantments":{ids:[' + '{K:"minecraft:efficiency",V:32},{K:"minecraft:unbreaking",V:34},' + '{K:"steamadvantage:high_explosive",V:10},{K:"steamadvantage:powderless",V:11}]}}}}') + nbtlib.File(level, gzipped=True).save(self.canonical / matrix.WORLD / "level.dat") + definition, document = snapshot() + matrix.write_json(self.canonical / "migration-source.json", document["snapshot"]) + matrix.write_json(self.canonical / "migration-fixture.json", definition["blocks"]) + hashes = self.root / "hashes.json" + matrix.write_json(hashes, matrix.fingerprint(self.canonical / matrix.WORLD)) + self.seed = self.root / "seed" + (self.seed / "libraries").mkdir(parents=True) + (self.seed / "libraries/dep.jar").write_bytes(b"unit-test-only-not-java-code") + (self.seed / "forge-old.jar").write_bytes(b"unit-test-only-not-java-code") + jar = self.root / "AdvantageMigrationProbe-110-1.0.0.jar" + with zipfile.ZipFile(jar, "w") as archive: + archive.writestr("zone/moddev/mc/advantageworks/migration/LegacyProbe.class", + b"synthetic-unit-only migration-operate.allow field_175730_i nbtBase64") + archive.writestr("zone/moddev/mc/advantageworks/migration/LegacyActors.class", + b"synthetic-unit-only playerAdapter AdvWorksPlayer") + self.probe = patch.object(machine, "FROZEN_SOURCE_PROBE", matrix.digest(jar)) + self.probe.start() + self.addCleanup(self.probe.stop) + manifest = self.root / "profiles.json" + matrix.write_json(manifest, {"original110": dict(seed=str(self.seed), launcher="forge-old.jar", + jars=[dict(path=str(jar), sha256=matrix.digest(jar))])}) + self.route = dict(id="legacy/original110", kind="frozen", sourceKey="original110", source=str(self.canonical), + sourceSnapshot="migration-source.json", sourceSnapshotSha256=matrix.digest(self.canonical / "migration-source.json"), + sourceHashes=str(hashes), sourceHashesSha256=matrix.digest(hashes), + sourceManifest=str(manifest), sourceManifestSha256=matrix.digest(manifest)) + self.routes = self.root / "routes.json" + matrix.write_json(self.routes, dict(routes=[self.route])) + self.custody = dict(receiptSha256="0" * 64, files={"unit-only": "0" * 64}) + self.history = patch.object(machine, "history_custody", return_value=self.custody) + self.history.start() + self.addCleanup(self.history.stop) + console_root = self.root / "canonical-consoles" + console = console_root / "original110/console-source.log" + console.parent.mkdir(parents=True) + console.write_text("[01:00:00] [Server thread/INFO]: Done (unit-only)\n") + self.console_root = patch.object(machine, "SOURCE_CONSOLE_ROOT", console_root) + self.console_pins = patch.object(machine, "SOURCE_CONSOLE_PINS", {"original110": matrix.digest(console)}) + self.console_root.start() + self.console_pins.start() + self.addCleanup(self.console_root.stop) + self.addCleanup(self.console_pins.stop) + + def prepare_plan(self): + value = machine.plan(self.routes, matrix.digest(self.routes), ["original110"]) + path = self.root / "plan.json" + matrix.write_json(path, value) + return path, value + + def test_world_only_canonical_uses_exact_separate_profile_without_writes(self): + before = matrix.fingerprint(self.canonical) + _, value = self.prepare_plan() + self.assertEqual(value["sourceBoots"], 6) + self.assertEqual(value["destinationBoots"], 18) + self.assertEqual(value["sources"][0]["canonicalLayout"], "world-and-snapshots") + self.assertEqual(matrix.fingerprint(self.canonical), before) + self.assertFalse((self.canonical / "mods").exists()) + + def test_changed_canonical_fingerprint_fails_before_staging(self): + (self.canonical / matrix.WORLD / "level.dat").write_bytes(b"changed") + with self.assertRaisesRegex(AssertionError, "approved frozen receipt"): + machine.plan(self.routes, matrix.digest(self.routes), ["original110"]) + + def test_mismatched_existing_mods_directory_is_not_treated_as_world_only(self): + (self.canonical / "mods").mkdir() + with self.assertRaisesRegex(AssertionError, "old jar set"): + machine.plan(self.routes, matrix.digest(self.routes), ["original110"]) + + def test_duplicate_source_selection_and_route_ambiguity_fail(self): + with self.assertRaises(ValueError): + machine.plan(self.routes, matrix.digest(self.routes), ["original110", "original110"]) + matrix.write_json(self.routes, dict(routes=[self.route, self.route])) + with self.assertRaisesRegex(AssertionError, "unique approved"): + machine.plan(self.routes, matrix.digest(self.routes), ["original110"]) + + def test_plan_is_invalidated_by_runner_or_historical_custody_change(self): + path, value = self.prepare_plan() + value["runnerSha256"] = "0" * 64 + matrix.write_json(path, value) + with self.assertRaisesRegex(AssertionError, "runner changed"): + machine.load_plan(path, matrix.digest(path)) + value["runnerSha256"] = matrix.digest(Path(machine.__file__)) + value["historical"] = {} + matrix.write_json(path, value) + with self.assertRaisesRegex(AssertionError, "oracles changed"): + machine.load_plan(path, matrix.digest(path)) + + def source_args(self): + path, _ = self.prepare_plan() + return SimpleNamespace(plan=path, plan_sha256=matrix.digest(path), output=self.root / "sources", + lease_port=25574, java8=Path("never-launched")) + + def fake_server(self, *, failed_seed=False): + closed = [] + class FakeServer: + def __init__(inner, runtime, java, launcher, label, source_seeds=()): + inner.runtime = runtime + inner.commands = [] + inner.closed = False + inner.definition = machine.read(runtime / "connected-fixture.json") + (runtime / "console-source.log").write_text("Starting minecraft server version 1.10.2\nDone (unit-only)\n") + closed.append(inner) + + def send(inner, command): + inner.commands.append(command) + if command.startswith("advprobe snapshot "): + label = command.split()[-1] + _, document = snapshot(inner.definition["case"]) + for row in document["snapshot"]: + tile = matrix.plain(matrix.parse_nbt(row)) + tile["Items"] = machine.inventory(inner.definition["case"], row["name"], "Chest", + inner.definition.get("storedEnchantments")) + row["nbtBase64"] = encode(tile) + if failed_seed and label == "source": + mutate_items(document, lambda stacks: stacks[0].update(id="minecraft:air")) + matrix.write_json(inner.runtime / ("migration-" + label + ".json"), document["snapshot"]) + elif command.startswith("advprobe actors-read "): + label = command.split()[-1] + player = nbtlib.load(inner.runtime / matrix.WORLD / "playerdata" / (str(machine.PLAYER_UUID) + ".dat")) + stream = io.BytesIO() + player.write(stream) + matrix.write_json(inner.runtime / ("actors-" + label + ".json"), dict(playerAdapter=True, frozen=True, + player=dict(nbtBase64=base64.b64encode(gzip.compress(stream.getvalue())).decode()))) + + def wait(inner, *args): + pass + + def close(inner): + inner.commands.append("stop") + inner.closed = True + matrix.write_json(inner.runtime / "console-source.commands.json", inner.commands) + return FakeServer, closed + + def test_source_seeds_new_copies_closes_before_raw_capture_and_never_writes_canonical(self): + args = self.source_args() + before = matrix.fingerprint(self.canonical) + server, instances = self.fake_server() + def raw(runtime, *unused, **kwargs): + self.assertTrue(instances[-1].closed) + self.assertNotEqual(runtime, self.canonical) + return {"unit-only": "0" * 64} + with patch.object(machine, "ObservedServer", server), patch.object(machine, "require_lease"), \ + patch.object(machine, "stopped_evidence", side_effect=raw): + receipt = machine.run_source(args) + self.assertEqual(receipt["status"], machine.SOURCE_WITNESS) + self.assertEqual(len(receipt["rows"]), 6) + self.assertTrue(all(instance.closed for instance in instances)) + self.assertEqual(matrix.fingerprint(self.canonical), before) + self.assertTrue(all(command.startswith(("advprobe snapshot ", "advprobe actors-read ", "blockdata ")) + or command in ("save-all flush", "stop") for instance in instances for command in instance.commands)) + + def test_failed_live_source_stack_retains_red_receipt_and_stops_at_first_case(self): + args = self.source_args() + server, instances = self.fake_server(failed_seed=True) + with patch.object(machine, "ObservedServer", server), patch.object(machine, "require_lease"), \ + patch.object(machine, "stopped_evidence") as raw: + with self.assertRaisesRegex(AssertionError, "Exact item mismatch"): + machine.run_source(args) + raw.assert_not_called() + receipt = machine.read(args.output / "source-receipt.json") + self.assertEqual(receipt["status"], "FAIL") + self.assertEqual(len(instances), 1) + self.assertTrue(instances[0].closed) + self.assertTrue(list(args.output.rglob("migration-source.json"))) + + def test_source_preserves_freeze_marker_and_never_enables_connected_operation(self): + runtime = self.root / "marker-test" + runtime.mkdir() + (runtime / "server.properties").write_text("server-port=25584\n") + definition, _ = snapshot() + machine.configure(runtime, definition, 25574, destination=False) + self.assertFalse((runtime / "migration-operate.allow").exists()) + self.assertFalse((runtime / "advantage-connected.allow").exists()) + (runtime / "migration-operate.allow").touch() + with self.assertRaisesRegex(AssertionError, "frozen marker state"): + machine.configure(runtime, definition, 25574, destination=False) + + def test_source_admin_lists_prevent_missing_file_exceptions_without_overwriting_data(self): + runtime = self.root / "source-admin-config" + runtime.mkdir() + (runtime / "server.properties").write_text("server-port=25584\n") + definition, _ = snapshot() + machine.configure(runtime, definition, 25574, destination=False) + for filename in ("banned-players.json", "banned-ips.json", "ops.json", "whitelist.json"): + self.assertEqual(machine.read(runtime / filename), []) + path = runtime / "ops.json" + matrix.write_json(path, [dict(name="do-not-overwrite")]) + before = matrix.digest(path) + with self.assertRaisesRegex(AssertionError, "not empty"): + machine.configure(runtime, definition, 25574, destination=False) + self.assertEqual(matrix.digest(path), before) + + def test_changed_runtime_code_fails_before_any_source_boot(self): + args = self.source_args() + (self.seed / "libraries/dep.jar").write_bytes(b"changed") + with patch.object(machine, "require_lease"), patch.object(machine, "ObservedServer") as server: + with self.assertRaisesRegex(AssertionError, "source/code changed"): + machine.run_source(args) + server.assert_not_called() + + def test_installer_retained_in_seed_custody_but_not_staged_or_launched(self): + (self.seed / "forge-installer.jar").write_bytes(b"unit-only-installer") + args = self.source_args() + server, _ = self.fake_server() + with patch.object(machine, "ObservedServer", server), patch.object(machine, "require_lease"), \ + patch.object(machine, "stopped_evidence", return_value={}): + result = machine.run_source(args) + self.assertEqual(result["status"], machine.SOURCE_WITNESS) + self.assertFalse(list(args.output.rglob("forge-installer.jar"))) + self.assertIn("forge-installer.jar", machine.read(args.plan)["sources"][0]["seedCode"]) + + +class DestinationFlowTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.source = self.root / "fresh-stopped-source" + (self.source / matrix.WORLD).mkdir(parents=True) + (self.source / matrix.WORLD / "level.dat").write_bytes(b"synthetic-unit-only-not-a-world") + self.definition, _ = snapshot() + matrix.write_json(self.source / "migration-fixture.json", self.definition["blocks"]) + self.before = matrix.fingerprint(self.source) + self.fresh = dict(plan=str(self.root / "plan.json"), rows=[dict(runtime=str(self.source), + definition=self.definition, sourceKey="original110", case="wool14", runtimeFingerprint=self.before)]) + seed = self.root / "seed" + (seed / "libraries").mkdir(parents=True) + (seed / "forge-unit.jar").write_bytes(b"not-a-launcher") + jar = self.root / "candidate-unit.jar" + jar.write_bytes(b"not-a-mod") + self.profile = (seed, "forge-unit.jar", [jar]) + manifest = self.root / "manifest.json" + matrix.write_json(manifest, {"target117": {"jars": [dict(id="poweradvantage", sourceRef="a" * 40, sha256="B" * 64)]}}) + self.args = SimpleNamespace(source_receipt=self.root / "source-receipt.json", source_receipt_sha256="0" * 64, + target_manifest=manifest, target_manifest_sha256=matrix.digest(manifest), power_source="a" * 40, + power_sha256="B" * 64, output=self.root / "destinations", lease_port=25574, java8=Path("never-launched")) + self.instances = [] + + def run_fake(self, *, lose_item=False): + instances = self.instances + class FakeDestination: + def __init__(inner, runtime, java, launcher, label): + inner.runtime, inner.label = runtime, label + inner.commands, inner.operated, inner.closed = [], False, False + inner.latest = None + instances.append(inner) + (runtime / ("console-" + label + ".log")).write_text( + "Starting minecraft server version 1.17.1\n" + "Forge mod loading, version 37.1.1, for MC 1.17.1 with MCP 20210706.113038\nDone (unit-only)\n") + + def send(inner, command): + machine.require_command(command, operated=inner.operated) + inner.commands.append(command) + if command.startswith("advconnected "): + label = command.split()[-1] + ticks = 512 if " step " in command else 0 + _, document = snapshot(modern=True, ticks=ticks) + if lose_item: + mutate_items(document, lambda stacks: stacks.clear()) + inner.latest = document + matrix.write_json(inner.runtime / ("connected-" + label + ".json"), document) + matrix.write_json(inner.runtime / ("migration-" + label + ".json"), document["snapshot"]) + + def wait(inner, *unused): + pass + + def close(inner): + inner.commands.append("stop") + inner.closed = True + matrix.write_json(inner.runtime / ("console-" + inner.label + ".commands.json"), inner.commands) + + def stage(runtime, profile, java, port): + return matrix.stage(runtime, profile) + + def region(unused): + self.assertTrue(instances[-1].closed) + tiles = [matrix.plain(matrix.parse_nbt(row)) for row in instances[-1].latest["snapshot"]] + return None, {0: RawEvidenceTest().chunk(tiles)} + + with patch.object(machine, "source_receipt", return_value=self.fresh), \ + patch.object(machine.native117, "pinned_profile", return_value=self.profile), \ + patch.object(machine.native117, "boot", side_effect=stage), \ + patch.object(machine, "DestinationServer", FakeDestination), \ + patch.object(machine, "require_lease"), patch.object(machine, "read_region", side_effect=region): + result = machine.run_destination(self.args) + audited = machine.audit(self.args.output) + return result, audited + + def test_three_boots_use_only_snapshot_step_save_stop_and_replay_raw_evidence(self): + result, audited = self.run_fake() + self.assertEqual(result["status"], "PASS") + self.assertEqual(audited["boots"], 3) + self.assertEqual(matrix.fingerprint(self.source), self.before) + self.assertEqual(len(self.instances), 3) + self.assertEqual(self.instances[0].commands, ["advconnected snapshot converted", "save-all flush", "stop"]) + self.assertEqual(self.instances[1].commands, ["advconnected snapshot restarted", "advconnected step 512 operated", + "save-all flush", "stop"]) + self.assertTrue(all(server.closed for server in self.instances)) + + def test_first_conversion_mismatch_closes_and_preserves_fail_without_restart(self): + with self.assertRaisesRegex(AssertionError, "Exact item mismatch"): + self.run_fake(lose_item=True) + self.assertEqual(len(self.instances), 1) + self.assertTrue(self.instances[0].closed) + self.assertEqual(machine.read(self.args.output / "results.json")["status"], "FAIL") + self.assertTrue(list(self.args.output.rglob("connected-converted.json"))) + self.assertEqual(matrix.fingerprint(self.source), self.before) + + def test_audit_rejects_unrecorded_destination_mutation_command(self): + self.run_fake() + path = self.args.output / "original110/wool14/console-restarted.commands.json" + commands = machine.read(path) + commands.insert(0, "blockdata 0 80 0 {Items:[]}") + matrix.write_json(path, commands) + with patch.object(machine, "source_receipt", return_value=self.fresh), \ + patch.object(machine.native117, "pinned_profile", return_value=self.profile): + with self.assertRaisesRegex(AssertionError, "command custody"): + machine.audit(self.args.output) + + +class StoredBookPlayerTest(unittest.TestCase): + def definition(self): + definition, _ = snapshot("enchanted-book") + definition.update(storedEnchantments=[dict(id=32, lvl=3), dict(id=10, lvl=1), dict(id=11, lvl=1)], + enchantmentNames={"32": "minecraft:efficiency", "34": "minecraft:unbreaking", + "10": "steamadvantage:high_explosive", "11": "steamadvantage:powderless"}, playerBook=True) + return definition + + def test_new_player_file_exists_before_source_read_and_has_numeric_vanilla_and_steam_book(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + definition = self.definition() + checksum = machine.seed_player(root, definition) + path = root / matrix.WORLD / "playerdata" / (str(machine.PLAYER_UUID) + ".dat") + self.assertEqual(checksum, matrix.digest(path)) + data = matrix.plain(nbtlib.load(path).unpack()) + expected = machine.player_items(data, definition, modern=False) + self.assertEqual(expected["tag"]["StoredEnchantments"], definition["storedEnchantments"]) + self.assertEqual(len(data["Inventory"]), 1) + self.assertEqual(data["DataVersion"], 512) + + def test_existing_player_file_is_never_overwritten(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + machine.seed_player(root, self.definition()) + before = matrix.fingerprint(root) + with self.assertRaises(FileExistsError): + machine.seed_player(root, self.definition()) + self.assertEqual(matrix.fingerprint(root), before) + + def test_exact_named_steam_book_passes_and_protection_corruption_fails(self): + definition = self.definition() + book = machine.modern_item(dict(Slot=0, **machine.items("enchanted-book", + stored_enchantments=definition["storedEnchantments"])), machine.names(definition)) + words = [(machine.PLAYER_UUID.int >> shift) & 0xffffffff for shift in (96, 64, 32, 0)] + data = dict(Inventory=[book], EnderItems=[], UUID=words) + machine.player_items(data, definition, modern=True) + self.assertEqual(book["tag"]["StoredEnchantments"][1]["id"], "steamadvantage:high_explosive") + for index in range(3): + changed = copy.deepcopy(data) + changed["Inventory"][0]["tag"]["StoredEnchantments"][index]["id"] = "minecraft:protection" + with self.subTest(index=index), self.assertRaisesRegex(AssertionError, "StoredEnchantments"): + machine.player_items(changed, definition, modern=True) + + def test_numeric_ids_are_resolved_from_each_saved_registry_not_shared_assumptions(self): + with tempfile.TemporaryDirectory() as temp: + for number in (10, 14): + level = nbtlib.parse_nbt('{FML:{Registries:{"minecraft:enchantments":{ids:[' + '{K:"minecraft:efficiency",V:32},{K:"minecraft:unbreaking",V:34},' + '{K:"steamadvantage:recoil",V:%d}]}}}}' % number) + path = Path(temp) / (str(number) + ".dat") + nbtlib.File(level, gzipped=True).save(path) + registry = machine.enchantment_registry(path) + self.assertEqual(registry["stored"], [dict(id=32, lvl=3), dict(id=number, lvl=1)]) + self.assertEqual(registry["names"][str(number)], "steamadvantage:recoil") + + def test_wrong_player_uuid_count_slot_or_lost_steam_enchantment_fails(self): + definition = self.definition() + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + machine.seed_player(root, definition) + data = matrix.plain(nbtlib.load(root / matrix.WORLD / "playerdata" / (str(machine.PLAYER_UUID) + ".dat")).unpack()) + for mutate in (lambda value: value.update(UUIDMost=0), lambda value: value["Inventory"][0].update(Count=2), + lambda value: value["Inventory"][0].update(Slot=1), + lambda value: value["Inventory"][0]["tag"]["StoredEnchantments"].pop()): + changed = copy.deepcopy(data) + mutate(changed) + with self.subTest(mutate=mutate), self.assertRaises(AssertionError): + machine.player_items(changed, definition, modern=False) + + def test_only_existing_player_read_commands_are_exposed(self): + machine.require_command("advactors read converted-book") + machine.require_command("advprobe actors-read source-book", source_seeds=["blockdata unit"]) + for command in ("advactors build book", "advprobe actors-build book", "advactors operate book", + "advprobe actors-read book\nstop"): + with self.subTest(command=command), self.assertRaises(AssertionError): + machine.require_command(command) + + +class SourceDiagnosticTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.authority_root = self.root / "retained" + self.raw_historical = ["[01:02:03] " + line for line in machine.KNOWN_SOURCE_LINES] + self.pins = {} + for profile, lines in (("original110", self.raw_historical), ("legacy112", [])): + path = self.authority_root / profile / "console-source.log" + path.parent.mkdir(parents=True) + path.write_text("\n".join(lines + ["[01:02:05] [Server thread/INFO]: Done (retained-unit-only)"]) + "\n") + self.pins[profile] = matrix.digest(path) + self.root_patch = patch.object(machine, "SOURCE_CONSOLE_ROOT", self.authority_root) + self.pins_patch = patch.object(machine, "SOURCE_CONSOLE_PINS", self.pins) + self.root_patch.start() + self.pins_patch.start() + self.addCleanup(self.root_patch.stop) + self.addCleanup(self.pins_patch.stop) + self.actual = self.root / "new-runtime.log" + + def log(self, lines): + self.actual.write_text("\n".join(lines + ["[12:34:59] [Server thread/INFO]: Done (new-unit-only)"]) + "\n") + return self.actual + + def report(self, lines, profile="original110"): + return machine.source_log_report(self.log(lines), profile, machine.canonical_source_console(profile)) + + def test_exact_known_lines_record_raw_evidence_count_and_pinned_historical_sha(self): + actual = ["[12:34:56] " + line for line in machine.KNOWN_SOURCE_LINES] + report = self.report(actual) + self.assertEqual(report["status"], machine.SOURCE_WITH_DIAGNOSTICS) + self.assertNotEqual(report["status"], "PASS") + self.assertEqual(report["actualDiagnosticCount"], 2) + self.assertEqual(report["canonicalDiagnosticCount"], 2) + self.assertEqual(report["canonicalSourceLogSha256"], self.pins["original110"]) + self.assertEqual(report["actualLogSha256"], matrix.digest(self.actual)) + for entry in report["diagnostics"]: + self.assertEqual(entry["status"], "KNOWN_SOURCE_DIAGNOSTIC") + self.assertEqual(entry["actualCount"], 1) + self.assertEqual(entry["canonicalCount"], 1) + self.assertEqual(entry["actualRawLines"], [line for line in actual if line.endswith(entry["identity"])]) + self.assertEqual(entry["canonicalRawLines"], [line for line in self.raw_historical if line.endswith(entry["identity"])]) + + def test_clean_source_has_explicit_source_witness_status_not_blanket_pass(self): + report = self.report([]) + self.assertEqual(report["status"], machine.SOURCE_WITNESS) + self.assertEqual(report["diagnostics"], []) + + def test_profile_without_identical_historical_lines_cannot_use_another_profiles_allowance(self): + with self.assertRaisesRegex(AssertionError, "Unapproved source"): + self.report(self.raw_historical, "legacy112") + authority = machine.canonical_source_console("original110") + with self.assertRaisesRegex(AssertionError, "authority/profile"): + machine.source_log_report(self.log(self.raw_historical), "legacy112", authority) + + def test_near_misses_different_mod_threads_loggers_case_and_whitespace_fail(self): + signature, bucket = self.raw_historical + variants = [signature + " ", signature + ".", signature.replace("not a good thing", "not good"), + signature.replace("[main/ERROR]", "[Server thread/ERROR]"), signature.replace("[FML]", "[Other]"), + signature.replace("FML appears", "fml appears"), bucket.replace("Base Metals", "Steam Advantage"), + bucket.replace("Base Metals", "Base Metals Extra"), bucket.replace("too late.", "too late!"), + bucket.replace("[Server thread/ERROR]", "[main/ERROR]")] + for line in variants: + with self.subTest(line=line), self.assertRaisesRegex(AssertionError, "Unapproved source"): + self.report([line]) + + def test_additional_error_exception_nosuch_and_fatal_lines_always_fail(self): + for line in ("[12:34:56] [Server thread/ERROR] [FML]: another failure", + "java.lang.IllegalStateException: failed", "Exception in thread main", + "java.lang.NoSuchMethodError: missing", "NoSuchFieldError: missing", + "NoSuchThingException: missing", "java.lang.NoClassDefFoundError: absent", + "java.io.FileNotFoundException: ops.json (The system cannot find the file specified)", + "[12:34:56] [main/FATAL] [FML]: failed"): + with self.subTest(line=line), self.assertRaisesRegex(AssertionError, "Unapproved source"): + self.report(self.raw_historical + [line]) + + def test_repeated_known_line_beyond_historical_count_is_additional_error(self): + with self.assertRaisesRegex(AssertionError, "excessCounts"): + self.report(self.raw_historical + [self.raw_historical[0]]) + + def test_changed_or_unpinned_canonical_log_fails_closed(self): + authority = machine.canonical_source_console("original110") + Path(authority["path"]).write_text("changed") + with self.assertRaisesRegex(AssertionError, "Pinned file changed"): + machine.source_log_report(self.log(self.raw_historical), "original110", authority) + with self.assertRaises(ValueError): + machine.canonical_source_console("unapproved-profile") + + def test_all_target_errors_including_both_known_source_lines_still_fail(self): + boot = ("Starting minecraft server version 1.17.1\n" + "Forge mod loading, version 37.1.1, for MC 1.17.1 with MCP 20210706.113038\n") + errors = self.raw_historical + ["[12:34:56] " + line for line in machine.LEGACY112_INTEGRATION_LINES] + ["[12:34:56] [main/ERROR] [FML]: unexpected", + "[12:34:56] [main/ERROR] [FML]: java.lang.IllegalStateException: failed", + "java.lang.NoSuchFieldError: absent", "java.lang.NoSuchMethodError: absent"] + with patch.object(machine, "source_log_report") as source_check: + for line in errors: + self.actual.write_text(boot + line + "\n") + with self.subTest(line=line), self.assertRaises(AssertionError): + machine.clean_log(self.actual, modern=True) + source_check.assert_not_called() + + def legacy112_notices(self): + lines = ["[01:02:03] " + line for line in machine.LEGACY112_INTEGRATION_LINES] + path = self.authority_root / "legacy112/console-source.log" + path.write_text("\n".join(lines + ["[01:02:04] [Server thread/INFO]: Done (retained-unit-only)"]) + "\n") + self.pins["legacy112"] = matrix.digest(path) + return lines + + def test_only_exact_pinned_legacy112_integration_notices_are_witnessed(self): + lines = self.legacy112_notices() + report = self.report(lines, "legacy112") + self.assertEqual(report["status"], machine.SOURCE_WITH_DIAGNOSTICS) + self.assertEqual(report["actualDiagnosticCount"], 2) + self.assertEqual(report["canonicalSourceLogSha256"], self.pins["legacy112"]) + self.assertTrue(all(entry["actualCount"] == entry["canonicalCount"] == 1 for entry in report["diagnostics"])) + with self.assertRaises(AssertionError): + self.report(lines, "original110") + + def test_legacy112_near_miss_different_mod_logger_and_extra_count_fail(self): + lines = self.legacy112_notices() + for mutated in (lines[0] + " ", lines[0].replace("orespawn]:", "othermod]:"), + lines[0].replace("For orespawn", "For electricadvantage"), + lines[0].replace("Loading Integration", "Loading integration"), + lines[0].replace("/FATAL]", "/ERROR]")): + with self.subTest(mutated=mutated), self.assertRaises(AssertionError): + self.report([mutated, lines[1]], "legacy112") + with self.assertRaises(AssertionError): + self.report(lines + [lines[0]], "legacy112") + with self.assertRaises(AssertionError): + self.report(lines + self.raw_historical, "legacy112") + + def test_saved_registry_sha_is_exact_and_order_independent(self): + first = dict(names={"32": "minecraft:efficiency", "10": "steamadvantage:recoil"}, + stored=[dict(id=32, lvl=3), dict(id=10, lvl=1)]) + reordered = dict(stored=first["stored"], names={"10": "steamadvantage:recoil", "32": "minecraft:efficiency"}) + self.assertEqual(machine.registry_sha256(first), machine.registry_sha256(reordered)) + changed = copy.deepcopy(first) + changed["stored"][1]["id"] = 14 + self.assertNotEqual(machine.registry_sha256(first), machine.registry_sha256(changed)) + + +class ObservationInventoryTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.seed = self.root / "original-seed" + self.original = self.seed / observation.COREMODS + self.original.parent.mkdir(parents=True) + self.engine = "net/minecraftforge/coremod/CoreModEngine.class" + self.added = {"qualification/order/TransformerOrderBridge.class": b"synthetic bridge", + "qualification/order/TransformerOrderBridge$Observed.class": b"synthetic observer"} + self.jar(self.original, {self.engine: b"synthetic original", "META-INF/MANIFEST.MF": b"unchanged"}) + self.observer = self.root / "observer.jar" + self.jar(self.observer, {self.engine: b"synthetic instrumented", "META-INF/MANIFEST.MF": b"unchanged", **self.added}) + dependency = self.seed / "libraries/dependency.jar" + dependency.write_bytes(b"synthetic dependency") + (self.seed / "libraries/mappings.txt").write_text("synthetic pinned non-jar code input") + (self.seed / "forge-unit.jar").write_bytes(b"synthetic launcher; never executed") + matrix.write_json(self.seed / observation.INVENTORY, + dict(files=observation.code_files(self.seed), format=1, minecraft="1.17.1", forge="37.1.1")) + self.source = self.root / "UnitBridge.java" + self.source.write_text("synthetic unit source; never compiled") + self.proof = self.root / "probe-build.json" + proof = observation.validate_patch(self.original, self.observer) + proof.update(sourceSha256=matrix.digest(self.source), dependencies={str(dependency): matrix.digest(dependency)}) + matrix.write_json(self.proof, proof) + + def jar(self, path, entries): + with zipfile.ZipFile(path, "w") as archive: + for name, data in entries.items(): + archive.writestr(name, data) + + def derive(self): + return observation.make_plan(self.seed, matrix.digest(self.seed / observation.INVENTORY), self.observer, + matrix.digest(self.observer), self.proof, matrix.digest(self.proof), self.source) + + def plan(self): + path = self.root / "observation-plan.json" + matrix.write_json(path, self.derive()) + return path, matrix.digest(path) + + def staged_unsealed(self, runtime): + shutil.copytree(self.seed, runtime) + shutil.copy2(self.observer, runtime / observation.COREMODS) + + def test_derivative_changes_only_coremods_and_preserves_all_other_entries(self): + before = matrix.fingerprint(self.seed) + plan = self.derive() + self.assertEqual(plan["changedPaths"], [observation.COREMODS]) + self.assertEqual(plan["codeFileCount"], 4) + for name, metadata in plan["baselineInventory"]["files"].items(): + if name != observation.COREMODS: + self.assertEqual(plan["derivedInventory"]["files"][name], metadata) + self.assertEqual(matrix.fingerprint(self.seed), before) + + def test_worldless_stage_proves_original_gate_red_then_unchanged_native_gate_green(self): + before = matrix.fingerprint(self.seed) + path, checksum = self.plan() + runtime = self.root / "observation-code-only" + proof = observation.stage_probe(runtime, path, checksum) + self.assertEqual(proof["baselineGateBeforeReseal"], "REJECTED_AS_EXPECTED") + self.assertEqual(proof["status"], "PASS_CODE_CUSTODY_ONLY_NO_MC") + self.assertEqual(matrix.digest(runtime / observation.CUSTODY / "baseline-inventory.json"), matrix.digest(self.seed / observation.INVENTORY)) + observation.verify_code(runtime) + observation.audit_runtime(runtime, path, checksum) + self.assertEqual(matrix.fingerprint(self.seed), before) + self.assertFalse((runtime / matrix.WORLD).exists()) + + def test_seal_rejects_unknown_extra_or_changed_other_code_before_writing_any_receipt(self): + path, checksum = self.plan() + for index, mutate in enumerate((lambda root: (root / "unexpected.jar").write_bytes(b"unexpected"), + lambda root: (root / "libraries/dependency.jar").write_bytes(b"changed"), + lambda root: (root / "libraries/mappings.txt").write_text("changed"))): + runtime = self.root / ("invalid-" + str(index)) + self.staged_unsealed(runtime) + mutate(runtime) + before = matrix.digest(runtime / observation.INVENTORY) + with self.subTest(index=index), self.assertRaisesRegex(AssertionError, "not exactly baseline"): + observation.seal_runtime(runtime, path, checksum) + self.assertEqual(matrix.digest(runtime / observation.INVENTORY), before) + self.assertFalse((runtime / observation.CUSTODY).exists()) + + def test_shared_seed_and_aliased_baseline_receipt_cannot_be_written(self): + path, checksum = self.plan() + with self.assertRaises(ValueError): + observation.seal_runtime(self.seed, path, checksum) + runtime = self.root / "hardlink-negative" + self.staged_unsealed(runtime) + (runtime / observation.INVENTORY).unlink() + os.link(self.seed / observation.INVENTORY, runtime / observation.INVENTORY) + before = matrix.digest(self.seed / observation.INVENTORY) + with self.assertRaisesRegex(ValueError, "aliases"): + observation.seal_runtime(runtime, path, checksum) + self.assertEqual(matrix.digest(self.seed / observation.INVENTORY), before) + + def test_tampering_after_seal_is_rejected_not_resealed(self): + path, checksum = self.plan() + runtime = self.root / "later-tamper" + observation.stage_probe(runtime, path, checksum) + (runtime / "libraries/dependency.jar").write_bytes(b"tampered after seal") + with self.assertRaises(AssertionError): + observation.audit_runtime(runtime, path, checksum) + with self.assertRaises(AssertionError): + observation.seal_runtime(runtime, path, checksum) + + def test_replaced_observer_or_bridge_build_receipt_does_not_match_pins(self): + path, checksum = self.plan() + self.observer.write_bytes(b"untrusted replacement") + with self.assertRaisesRegex(AssertionError, "Changed pinned"): + observation.load_plan(path, checksum) + + def test_extra_zip_changes_and_duplicate_entries_fail_patch_validation(self): + self.jar(self.observer, {self.engine: b"synthetic instrumented", "META-INF/MANIFEST.MF": b"changed", **self.added}) + with self.assertRaises(AssertionError): + self.derive() + self.jar(self.observer, {self.engine: b"synthetic instrumented", "META-INF/MANIFEST.MF": b"unchanged", **self.added}) + with zipfile.ZipFile(self.observer, "a") as archive, patch("warnings.warn"): + archive.writestr(self.engine, b"duplicate engine") + with self.assertRaisesRegex(ValueError, "Duplicate ZIP"): + self.derive() + + def test_forged_inventory_metadata_or_non_coremods_changes_fail_plan_replay(self): + path, _ = self.plan() + plan = observation.read(path) + plan["derivedInventory"]["files"]["libraries/dependency.jar"]["sha256"] = "0" * 64 + matrix.write_json(path, plan) + with self.assertRaisesRegex(AssertionError, "pinned derivation"): + observation.load_plan(path, matrix.digest(path)) + + def test_changed_original_seed_or_dependency_provenance_fails_closed(self): + path, checksum = self.plan() + (self.seed / "libraries/dependency.jar").write_bytes(b"changed original") + with self.assertRaises(AssertionError): + observation.load_plan(path, checksum) + + def test_path_traversal_case_aliases_and_invalid_inventory_metadata_are_rejected(self): + baseline = self.derive()["baselineInventory"] + for name in ("libraries/../escape.jar", "C:/escape.jar", "./escape.jar", "metadata.txt"): + changed = copy.deepcopy(baseline) + changed["files"][name] = dict(sha256="A" * 64, size=1) + with self.subTest(name=name), self.assertRaises(ValueError): + observation.validate_inventory(changed) + changed = copy.deepcopy(baseline) + changed["files"][observation.COREMODS.upper()] = changed["files"][observation.COREMODS] + with self.assertRaises(ValueError): + observation.validate_inventory(changed) + + def test_worldless_output_cannot_overwrite_original_or_existing_evidence(self): + path, checksum = self.plan() + with self.assertRaises(ValueError): + observation.stage_probe(self.seed / "child", path, checksum) + runtime = self.root / "already-present" + runtime.mkdir() + with self.assertRaises(FileExistsError): + observation.stage_probe(runtime, path, checksum) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_machine_operations.py b/test-rig/migration/test_native117_machine_operations.py new file mode 100644 index 00000000..50051b45 --- /dev/null +++ b/test-rig/migration/test_native117_machine_operations.py @@ -0,0 +1,100 @@ +"""The new native suite must reject incomplete jobs and extra/lost restarted state.""" + +import copy +import unittest + +from native117_machine_operations import REQUIRED, validate_operation, validate_restart + + +def stored(nbt='{Items:[{Slot:0b,id:"minecraft:dirt",Count:16b}],Energy:[I;1065353216]}'): + return dict(id="power/test", positions=[dict(x=50000, y=80, z=50000, block="minecraft:chest", state="chest", nbt=nbt)]) + + +class FullProcessMachineOracleTests(unittest.TestCase): + def pair(self): + return {"cases": [stored()]}, dict(held=True, constructionCallsThisProcess=0, cases=[stored()]) + + def test_actual_same_loaded_state_passes(self): + before, after = self.pair() + self.assertEqual("PASS", validate_restart(before, after)["status"]) + + def test_target_only_extra_resource_is_not_ignored(self): + before, after = self.pair() + after["cases"][0]["positions"][0]["nbt"] = '{Items:[{Slot:0b,id:"minecraft:dirt",Count:16b}],Energy:[I;1065353216],HiddenTank:{Amount:1000}}' + self.assertEqual("FAIL", validate_restart(before, after)["status"]) + + def test_changed_quantity_or_nbt_type_fails(self): + for bad in ('{Items:[{Slot:0b,id:"minecraft:dirt",Count:15b}],Energy:[I;1065353216]}', + '{Items:[{Slot:0b,id:"minecraft:dirt",Count:16}],Energy:[I;1065353216]}'): + before, after = self.pair() + after["cases"][0]["positions"][0]["nbt"] = bad + self.assertEqual("FAIL", validate_restart(before, after)["status"]) + + def test_missing_or_rebuilt_or_unfrozen_cases_fail(self): + for bad in (dict(held=True, constructionCallsThisProcess=0, cases=[]), + dict(held=True, constructionCallsThisProcess=1, cases=[stored()]), + dict(held=False, constructionCallsThisProcess=0, cases=[stored()])): + with self.assertRaises(AssertionError): validate_restart({"cases": [stored()]}, bad) + + def test_extra_position_fails(self): + before, after = self.pair() + extra = copy.deepcopy(after["cases"][0]["positions"][0]); extra["x"] += 1 + after["cases"][0]["positions"].append(extra) + self.assertEqual("FAIL", validate_restart(before, after)["status"]) + + def test_operating_catalog_cannot_be_empty(self): + with self.assertRaises(AssertionError): validate_operation({"cases": []}, {"cases": []}, "all") + + def test_duplicate_entity_uuid_is_not_collapsed(self): + before, after = self.pair() + actor = dict(uuid="example", nbt='{Health:20.0f}') + before["cases"][0]["entities"] = [actor] + after["cases"][0]["entities"] = [actor, actor] + with self.assertRaises(AssertionError): validate_restart(before, after) + + def test_attribute_order_alone_is_not_entity_state(self): + before, after = self.pair() + first = '{Name:"minecraft:generic.max_health",Base:1000.0d}' + second = '{Name:"minecraft:generic.movement_speed",Base:0.2d}' + before["cases"][0]["entities"] = [dict(uuid="witness", nbt='{Health:1000.0f,Attributes:[' + first + ',' + second + ']}')] + after["cases"][0]["entities"] = [dict(uuid="witness", nbt='{Health:1000.0f,Attributes:[' + second + ',' + first + ']}')] + self.assertEqual("PASS", validate_restart(before, after)["status"]) + for changed in (second.replace('0.2d', '0.3d'), second.replace('0.2d', '0.2f')): + after["cases"][0]["entities"][0]["nbt"] = '{Health:1000.0f,Attributes:[' + changed + ',' + first + ']}' + self.assertEqual("FAIL", validate_restart(before, after)["status"]) + after["cases"][0]["entities"][0]["nbt"] = '{Health:1000.0f,Attributes:[' + first + ',' + first + ']}' + with self.assertRaisesRegex(AssertionError, "Duplicate native actor attribute"): + validate_restart(before, after) + + def test_actor_inventory_order_and_extra_nbt_remain_strict(self): + before, after = self.pair() + before["cases"][0]["entities"] = [dict(uuid="witness", nbt='{Items:[{Slot:0b,Count:1b},{Slot:1b,Count:2b}]}')] + for changed in ('{Items:[{Slot:1b,Count:2b},{Slot:0b,Count:1b}]}', + '{Items:[{Slot:0b,Count:1b},{Slot:1b,Count:2b}],Hidden:1b}'): + after["cases"][0]["entities"] = [dict(uuid="witness", nbt=changed)] + self.assertEqual("FAIL", validate_restart(before, after)["status"]) + + def test_only_exact_zero_volume_named_tank_representation_may_change(self): + for original, expected in (("{Amount:0,FluidName:\"poweradvantage:crude_oil\"}", "PASS"), + ("{Amount:1,FluidName:\"poweradvantage:crude_oil\"}", "FAIL"), + ("{Amount:0,FluidName:\"poweradvantage:crude_oil\",Hidden:1}", "FAIL"), + ("{Amount:0b,FluidName:\"poweradvantage:crude_oil\"}", "FAIL")): + before, after = self.pair() + before["cases"][0]["positions"][0]["nbt"] = "{Tank:" + original + "}" + after["cases"][0]["positions"][0]["nbt"] = "{Tank:{Empty:1b}}" + self.assertEqual(expected, validate_restart(before, after)["status"]) + + def test_pass_label_cannot_hide_short_clock_missing_output_or_failed_assertion(self): + ids = sorted("power/" + name for name in REQUIRED["power"]) + catalog = dict(cases=[dict(id=key) for key in ids]) + good = dict(status="PASS", held=True, cases=[dict(id=key, status="PASS", verifiedAssertions=1, + assertions=[dict(phase="verify", status="PASS")], before=[stored()], after=[stored()], + startTick=100, finishTick=180, requiredTicks=80) for key in ids]) + self.assertEqual("PASS", validate_operation(catalog, good, "power")["status"]) + for patch in (dict(finishTick=179), dict(verifiedAssertions=0), dict(after=[]), + dict(assertions=[dict(phase="verify", status="FAIL")])): + bad = copy.deepcopy(good); bad["cases"][0].update(patch) + self.assertEqual("FAIL", validate_operation(catalog, bad, "power")["status"]) + + +if __name__ == "__main__": unittest.main() diff --git a/test-rig/migration/test_native117_machine_saved.py b/test-rig/migration/test_native117_machine_saved.py new file mode 100644 index 00000000..ecf81e81 --- /dev/null +++ b/test-rig/migration/test_native117_machine_saved.py @@ -0,0 +1,67 @@ +import copy +import unittest + +import nbtlib + +from native117_machine_saved import compare_chunk, state + + +class SavedMachineTests(unittest.TestCase): + def fixture(self): + tile = nbtlib.parse_nbt('{id:"steamadvantage:steam_pump",x:1,y:80,z:2,NextPump:19b}') + section = nbtlib.Compound(Y=nbtlib.Byte(5), Palette=nbtlib.List[nbtlib.Compound]([ + nbtlib.Compound(Name=nbtlib.String('steamadvantage:steam_pump'), + Properties=nbtlib.Compound(facing=nbtlib.String('north')))])) + data = nbtlib.Compound(Sections=nbtlib.List[nbtlib.Compound]([section]), + TileEntities=nbtlib.List[nbtlib.Compound]([tile])) + row = dict(x=1, y=80, z=2, block='steamadvantage:steam_pump', + state='Block{steamadvantage:steam_pump}[facing=north]', nbt=tile.snbt()) + tile['keepPacked'] = nbtlib.Byte(0) + return [row], data + + def test_exact_native_saved_state_and_typed_nbt_pass(self): + rows, data = self.fixture() + self.assertEqual([], compare_chunk(rows, data)) + + def test_stale_timer_is_not_accepted_as_a_frozen_save(self): + rows, data = self.fixture() + data['TileEntities'][0]['NextPump'] = nbtlib.Byte(7) + self.assertEqual(1, len(compare_chunk(rows, data))) + + def test_same_value_with_wrong_nbt_type_fails(self): + rows, data = self.fixture() + data['TileEntities'][0]['NextPump'] = nbtlib.Int(19) + self.assertEqual(1, len(compare_chunk(rows, data))) + + def test_shortest_snbt_float_and_binary32_save_agree_without_rounding_other_types(self): + from native117_machine_operations import typed_nbt + import struct + actual = struct.unpack('>f', struct.pack('>f', 1346.7579))[0] + self.assertEqual(typed_nbt(nbtlib.Float(1346.7579)), typed_nbt(nbtlib.Float(actual))) + self.assertNotEqual(typed_nbt(nbtlib.Double(1346.7579)), typed_nbt(nbtlib.Double(actual))) + self.assertNotEqual(typed_nbt(nbtlib.Float(actual)), typed_nbt(nbtlib.Float(actual + 0.001))) + + def test_missing_tile_or_changed_facing_fails(self): + rows, data = self.fixture() + changed = copy.deepcopy(data) + changed['TileEntities'].clear() + self.assertTrue(compare_chunk(rows, changed)) + data['Sections'][0]['Palette'][0]['Properties']['facing'] = nbtlib.String('south') + self.assertTrue(compare_chunk(rows, data)) + + def test_inconsistent_or_duplicate_state_keys_rejected(self): + with self.assertRaises(ValueError): + state(dict(block='steamadvantage:steam_pump', state='Block{minecraft:stone}')) + with self.assertRaises(ValueError): + state(dict(block='minecraft:stone', state='Block{minecraft:stone}[facing=north,facing=south]')) + + def test_packed_or_wrongly_typed_storage_marker_is_not_ignored(self): + rows, data = self.fixture() + for value in (nbtlib.Byte(1), nbtlib.Int(0)): + data['TileEntities'][0]['keepPacked'] = value + with self.assertRaisesRegex(ValueError, 'unpacked-tile'): + compare_chunk(rows, data) + + +if __name__ == '__main__': + unittest.main() diff --git a/test-rig/migration/test_native117_mod_inventory.py b/test-rig/migration/test_native117_mod_inventory.py new file mode 100644 index 00000000..e3e58600 --- /dev/null +++ b/test-rig/migration/test_native117_mod_inventory.py @@ -0,0 +1,338 @@ +"""Worldless exact-stage inventory controls using isolated temporary files only.""" + +import copy +import hashlib +import json +from pathlib import Path +import stat +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +import native117_mod_inventory as inventory + + +class ModInventoryTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory(prefix="native117 inventory spaces ") + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.runtime = self.root / "Closed Runtime With Spaces" + self.mods = self.runtime / "mods" + self.mods.mkdir(parents=True) + self.sources = self.root / "Sealed Input Jars" + self.sources.mkdir() + self.manifest = self.root / "sealed-manifest.json" + self.ledger = self.runtime / "input-jars.json" + self.rows, self.jars = [], [] + for identity in sorted(inventory.EXPECTED_IDS): + name = identity + "-1.0.0.117011.jar" + source = self.sources / name + data = ("test jar payload: " + identity).encode("ascii") + source.write_bytes(data) + (self.mods / name).write_bytes(data) + checksum = hashlib.sha256(data).hexdigest().upper() + self.rows.append(dict(file=name, source=str(source), sha256=checksum)) + self.jars.append(dict(id=identity, version="1.0.0.117011", path=str(source), sha256=checksum)) + self.document = {"target117": {"jars": self.jars}} + self.write_inputs() + + def write_inputs(self): + self.manifest.write_text(json.dumps(self.document), encoding="utf-8") + self.ledger.write_text(json.dumps(self.rows), encoding="utf-8") + self.pin = hashlib.sha256(self.manifest.read_bytes()).hexdigest().upper() + + def validate(self): + return inventory.validate_mod_inventory(self.runtime, self.manifest, self.pin) + + def reject(self): + with self.assertRaises(inventory.InventoryError): + self.validate() + + def test_exact_actual_native_stage_schema_and_six_mod_set_pass(self): + result = self.validate() + self.assertEqual("PASS", result["status"]) + self.assertEqual(inventory.EXPECTED_IDS, {row["id"] for row in result["jars"]}) + self.assertEqual(6, len(result["jars"])) + self.assertFalse(result["atomicSnapshot"]) + self.assertEqual("EXACT_SIX_MOD_INVENTORY_ONLY", result["scope"]) + + def test_missing_manifest_sha_argument_is_not_inferred_from_receipts(self): + for pin in (None, "", "A" * 63, "G" * 64, "A" * 64 + " "): + with self.subTest(pin=pin), self.assertRaises(inventory.InventoryError): + inventory.validate_mod_inventory(self.runtime, self.manifest, pin) + + def test_manifest_byte_tampering_fails_even_when_semantics_unchanged(self): + self.manifest.write_bytes(self.manifest.read_bytes() + b"\n") + self.reject() + + def test_lowercase_hex_pins_are_accepted_but_values_remain_exact(self): + self.assertEqual("PASS", inventory.validate_mod_inventory(self.runtime, self.manifest, self.pin.lower())["status"]) + + def test_extra_jar_closes_original_receipt_only_false_positive(self): + (self.mods / "unapproved-mod.jar").write_bytes(b"unlisted") + self.assertTrue(all(hashlib.sha256((self.mods / row["file"]).read_bytes()).hexdigest().upper() == row["sha256"] + for row in self.rows)) + self.reject() + + def test_empty_ledger_does_not_vacuously_pass(self): + self.rows.clear() + self.write_inputs() + self.reject() + + def test_missing_physical_jar_fails(self): + (self.mods / self.rows[0]["file"]).unlink() + self.reject() + + def test_missing_manifest_ledger_or_mods_directory_fails(self): + for path in (self.manifest, self.ledger): + contents = path.read_bytes() + path.unlink() + self.reject() + path.write_bytes(contents) + self.mods.rename(self.runtime / "unavailable-mods") + self.reject() + + def test_altered_jar_fails_against_manifest_not_ledger(self): + target = self.mods / self.rows[0]["file"] + target.write_bytes(b"altered") + self.rows[0]["sha256"] = hashlib.sha256(b"altered").hexdigest().upper() + self.write_inputs() + self.reject() + + def test_altered_physical_jar_with_unchanged_ledger_fails(self): + (self.mods / self.rows[0]["file"]).write_bytes(b"altered") + self.reject() + + def test_changed_sealed_source_jar_is_rejected(self): + Path(self.jars[0]["path"]).write_bytes(b"altered sealed input") + self.reject() + + def test_missing_sealed_source_jar_is_rejected(self): + Path(self.jars[0]["path"]).unlink() + self.reject() + + def test_missing_ledger_entry_and_extra_entry_fail(self): + original = copy.deepcopy(self.rows) + for rows in (original[:-1], original + [dict(file="extra.jar", source=str(self.sources / "extra.jar"), sha256="A" * 64)]): + self.rows = rows + self.write_inputs() + self.reject() + + def test_duplicate_and_case_colliding_ledger_names_fail(self): + for case_change in (False, True): + self.rows[1] = copy.deepcopy(self.rows[0]) + if case_change: + self.rows[1]["file"] = self.rows[1]["file"].upper().replace(".JAR", ".jar") + self.write_inputs() + self.reject() + + def test_ledger_source_must_equal_manifest_source_not_only_jar_hash(self): + self.rows[0]["source"] = str(self.sources / "different.jar") + self.write_inputs() + self.reject() + + def test_path_traversal_absolute_ads_and_nonjar_ledger_names_fail(self): + for name in ("../outside.jar", "sub/inside.jar", "sub\\inside.jar", "C:\\escape.jar", "/escape.jar", + "a.jar:stream", "a.txt", "a.JAR", ".hidden.jar", "NUL.jar", "COM1.jar", "a.jar ", "a\0.jar"): + self.rows[0]["file"] = name + self.write_inputs() + with self.subTest(name=name): + self.reject() + + def test_ledger_relative_source_and_traversal_sources_fail(self): + for source in ("relative.jar", "D:relative.jar", str(self.sources / ".." / "Sealed Input Jars" / self.rows[0]["file"]), + str(self.sources / self.rows[0]["file"]) + ":stream"): + self.rows[0]["source"] = source + self.write_inputs() + self.reject() + + def test_extra_nonjar_file_directory_and_nested_jar_fail(self): + for name in ("README.txt", ".hidden", "disabled"): + path = self.mods / name + if name == "disabled": + path.mkdir() + (path / "nested.jar").write_bytes(b"nested") + else: + path.write_bytes(b"extra") + with self.subTest(name=name): + self.reject() + if path.is_dir(): + (path / "nested.jar").unlink() + path.rmdir() + else: + path.unlink() + + def test_directory_named_jar_is_not_a_regular_jar(self): + path = self.mods / self.rows[0]["file"] + path.unlink() + path.mkdir() + self.reject() + + def test_manifest_requires_exact_six_ids_no_optional_subset_or_extra_mod(self): + original = copy.deepcopy(self.jars) + for jars in (original[:-1], original + [copy.deepcopy(original[0])], + [dict(row, id="unknown") if index == 0 else row for index, row in enumerate(original)]): + self.document["target117"]["jars"] = jars + self.write_inputs() + self.reject() + + def test_duplicate_manifest_ids_are_rejected(self): + self.jars[1]["id"] = self.jars[0]["id"] + self.write_inputs() + self.reject() + + def test_duplicate_or_case_colliding_manifest_filenames_are_rejected(self): + for case_change in (False, True): + name = Path(self.jars[0]["path"]).name + if case_change: + name = name.upper().replace(".JAR", ".jar") + self.jars[1]["path"] = str(self.root / "other directory" / name) + self.write_inputs() + self.reject() + + def test_manifest_relative_source_is_resolved_like_native_pinned_profile(self): + for jar in self.jars: + jar["path"] = str(Path(jar["path"]).relative_to(self.root)) + self.write_inputs() + self.assertEqual("PASS", self.validate()["status"]) + + def test_malformed_manifest_and_ledger_rows_fail_closed(self): + good_document, good_rows = copy.deepcopy(self.document), copy.deepcopy(self.rows) + for document in ({}, [], {"target117": []}, {"target117": {"jars": {}}}): + self.document = document + self.write_inputs() + self.reject() + self.document = good_document + for row in (None, {}, dict(good_rows[0], extra=1), dict(good_rows[0], sha256=True), dict(good_rows[0], source=None)): + self.rows = [row] + good_rows[1:] + self.write_inputs() + self.reject() + + def test_malformed_manifest_jar_fields_fail_closed(self): + original = copy.deepcopy(self.jars[0]) + for field, value in (("id", []), ("version", None), ("version", "1.15.2"), ("path", None), + ("sha256", "bad"), ("extra", "unrecognized")): + self.jars[0] = dict(original, **{field: value}) + self.write_inputs() + with self.subTest(field=field): + self.reject() + + def test_duplicate_json_keys_are_rejected_in_both_inputs(self): + encoded = self.manifest.read_text() + self.manifest.write_text(encoded.replace('"id":', '"id":"duplicate","id":', 1)) + self.pin = hashlib.sha256(self.manifest.read_bytes()).hexdigest().upper() + self.reject() + self.write_inputs() + self.ledger.write_text(self.ledger.read_text().replace('"file":', '"file":"duplicate.jar","file":', 1)) + self.reject() + + def test_nonfinite_json_constants_invalid_encoding_and_truncated_json_fail(self): + original = self.ledger.read_bytes() + for raw in (b"[", b"\xff", original.replace(b'"sha256":', b'"sha256":NaN,"unused":', 1)): + self.ledger.write_bytes(raw) + self.reject() + + def fake_reparse(self, target, symbolic=False): + original = Path.lstat + + def metadata(path, *args, **kwargs): + value = original(path, *args, **kwargs) + if path == target: + fields = {name: getattr(value, name) for name in dir(value) if name.startswith("st_")} + fields["st_file_attributes"] = getattr(value, "st_file_attributes", 0) | inventory._REPARSE + if symbolic: + fields["st_mode"] = stat.S_IFLNK | 0o777 + fields["st_file_attributes"] &= ~inventory._REPARSE + return SimpleNamespace(**fields) + return value + + return patch.object(Path, "lstat", metadata) + + def test_reparse_file_mod_directory_ledger_manifest_source_and_ancestors_fail(self): + targets = (self.mods / self.rows[0]["file"], self.mods, self.ledger, + self.manifest, Path(self.jars[0]["path"]), self.runtime, self.root) + for target in targets: + with self.subTest(target=target), self.fake_reparse(target): + self.reject() + + def test_symbolic_link_mode_is_rejected_even_without_platform_reparse_flag(self): + target = self.mods / self.rows[0]["file"] + with self.fake_reparse(target, symbolic=True): + self.reject() + + def test_physical_case_collision_is_rejected_on_all_host_filesystems(self): + entries = list(self.mods.iterdir()) + collision = entries[0].with_name(entries[0].name.upper().replace(".JAR", ".jar")) + with patch.object(Path, "iterdir", return_value=iter(entries + [collision])), \ + patch.object(inventory, "_plain_path"): + with self.assertRaisesRegex(inventory.InventoryError, "case-colliding physical"): + inventory._physical(self.mods) + + def test_windows_path_handle_ctime_difference_is_not_a_content_change(self): + original = inventory.os.fstat + + def handle_metadata(descriptor): + metadata = original(descriptor) + fields = {name: getattr(metadata, name) for name in dir(metadata) if name.startswith("st_")} + fields["st_ctime_ns"] += 1 + return SimpleNamespace(**fields) + + with patch.object(inventory.os, "fstat", side_effect=handle_metadata): + self.assertEqual("PASS", self.validate()["status"]) + + def test_changed_handle_metadata_during_hash_read_is_rejected(self): + original = inventory.os.fstat + calls = 0 + + def handle_metadata(descriptor): + nonlocal calls + calls += 1 + metadata = original(descriptor) + fields = {name: getattr(metadata, name) for name in dir(metadata) if name.startswith("st_")} + fields["st_ctime_ns"] += calls + return SimpleNamespace(**fields) + + with patch.object(inventory.os, "fstat", side_effect=handle_metadata): + self.reject() + + def test_manifest_or_ledger_change_during_validation_is_rejected(self): + original = inventory._read + for target in (self.manifest, self.ledger): + calls = 0 + + def changed(path, retain=False): + nonlocal calls + result = original(path, retain) + if path == target: + calls += 1 + if calls > 1: + return "0" * 64, result[1], result[2] + return result + + with self.subTest(target=target), patch.object(inventory, "_read", side_effect=changed): + self.reject() + + def test_added_physical_entry_before_final_recheck_is_rejected(self): + original = inventory._physical + calls = 0 + + def changed(mods): + nonlocal calls + calls += 1 + return original(mods) | ({"late.jar"} if calls > 1 else set()) + + with patch.object(inventory, "_physical", side_effect=changed): + self.reject() + + def test_successful_validation_does_not_modify_any_file_or_return_mutable_inputs(self): + before = {str(path): path.read_bytes() for path in self.root.rglob("*") if path.is_file()} + first = self.validate() + first["jars"][0]["sha256"] = "changed result only" + self.assertEqual("PASS", self.validate()["status"]) + self.assertEqual(before, {str(path): path.read_bytes() for path in self.root.rglob("*") if path.is_file()}) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_oil.py b/test-rig/migration/test_native117_oil.py new file mode 100644 index 00000000..95de1367 --- /dev/null +++ b/test-rig/migration/test_native117_oil.py @@ -0,0 +1,468 @@ +"""Focused native oil safety, protocol and unchanged-oracle negative controls.""" + +from copy import deepcopy +import json +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch +import zipfile + +import lifecycle +from matrix import WORLD, digest, fingerprint, write_json +from native117 import LAUNCHER, RIG +from native117_code import INVENTORY, code_files +import native117_oil as native +import oil_transfer + + +BOOT_PROOF = ("Starting minecraft server version 1.17.1\n" + "Forge mod loading, version 37.1.1, for MC 1.17.1 with MCP 20210706.113038\n") + + +def snapshot(definition, amount, identity=0): + cases = {} + for index, lane in enumerate(definition): + cases[lane["id"]] = [ + dict(pos=lane["source"], block="minecraft:air"), + dict(pos=lane["drain"], block="basepower:fluid_drain", tileIdentity=identity + index * 2 + 1, + nbt="{Tank:{Empty:1b}}"), + *[dict(pos=pos, block="basepower:fluid_pipe") for pos in lane["pipes"]], + dict(pos=lane["output"], block=lane["tank"], tileIdentity=identity + index * 2 + 2, + nbt=('{Tank:{Amount:' + str(amount) + ',FluidName:"' + lane["fluid"] + '"}}') + if amount else "{Tank:{Empty:1b}}")] + return dict(cases=cases) + + +class NativeOilTests(unittest.TestCase): + def test_mineralogy_pin_matches_every_native117_profile(self): + seen = 0 + for path in (RIG / "profiles/native117").glob("*.json"): + for mod in json.loads(path.read_text(encoding="utf-8"))["mods"]: + if mod["id"] == "mineralogy": + self.assertEqual(native.MINERALOGY_VERSION, mod["version"], path.name) + self.assertEqual(native.MINERALOGY_SOURCE_COMMIT, mod["sourceRef"], path.name) + if mod["sha256"] is not None: + self.assertRegex(mod["sha256"], r"^[A-Fa-f0-9]{64}$") + seen += 1 + self.assertGreater(seen, 0) + + def setUp(self): + root = RIG / "build/port-evidence" + root.mkdir(parents=True, exist_ok=True) + self.temp = tempfile.TemporaryDirectory(dir=root) + self.root = Path(self.temp.name) + # The protocol double never starts Java; a real TCP port belongs to + # the independent live campaign and must not affect these checks. + listener = patch.object(native, "socket") + self.listener = listener.start().socket.return_value.__enter__.return_value + self.addCleanup(listener.stop) + + def tearDown(self): + self.temp.cleanup() + + def mineralogy_pin(self): + artifact = self.root / "Mineralogy-6.1.4.117011.jar" + with zipfile.ZipFile(artifact, "w") as archive: + archive.writestr("META-INF/mods.toml", '[[mods]]\nmodId="mineralogy"\nversion="6.1.4.117011"\n') + return dict(id="mineralogy", version=native.MINERALOGY_VERSION, sha256=digest(artifact), + path=str(artifact), sourceRef=native.MINERALOGY_SOURCE_COMMIT) + + def profile(self): + seed = self.root / "installed-seed" + (seed / "libraries/subdir").mkdir(parents=True) + (seed / "libraries/subdir/runtime.jar").write_bytes(b"unit-test-library-not-executed") + (seed / LAUNCHER).parent.mkdir(parents=True, exist_ok=True) + (seed / LAUNCHER).write_bytes(b"unit-test-launcher-not-executed") + (seed / "minecraft_server.1.17.1.jar").write_bytes(b"unit-test-vanilla-not-executed") + (seed / WORLD).mkdir() + (seed / WORLD / "level.dat").write_bytes(b"protected-unit-test-world-never-copied") + (seed / "mods").mkdir() + (seed / "mods/unpinned.jar").write_bytes(b"never-copy-seed-mods") + (seed / "server.properties").write_text("protected-seed-settings\n") + jars = [] + for mod in ("basepower", "advantageworkstest", "orespawn"): + jar = self.root / (mod + "-1.0.0.117011.jar") + with zipfile.ZipFile(jar, "w") as archive: + archive.writestr("META-INF/mods.toml", '[[mods]]\nmodId="' + mod + '"\nversion="1.0.0.117011"\n') + jars.append(dict(path=str(jar), sha256=digest(jar), id=mod, version="1.0.0.117011")) + server_code = {"files": code_files(seed)} + write_json(seed / INVENTORY, server_code) + pins = dict(seed=str(seed), launcher=LAUNCHER, launcher_sha256=digest(seed / LAUNCHER), jars=jars, + serverCode=server_code) + manifest = self.root / "target.json" + write_json(manifest, dict(target117=pins)) + java = self.root / "unit-java-not-executed.exe" + java.write_bytes(b"unit test executable placeholder, process factory is mocked") + args = SimpleNamespace(target_manifest=manifest, output=self.root / "native117-oil-unit", + java8=java, port=25576, seconds=2) + return args, (seed, LAUNCHER, [Path(jar["path"]) for jar in jars]), pins + + def test_occupied_port_rejects_before_staging(self): + args, _, _ = self.profile() + self.listener.bind.side_effect = OSError("port in use") + with self.assertRaisesRegex(OSError, "port in use"): + native.run(args) + self.listener.bind.assert_called_once_with(("127.0.0.1", args.port)) + self.assertFalse(args.output.exists()) + + def server_factory(self, mutate=None, transform=None, failure=None, shutdown=False): + owner = self + self.processes = [] + self.commands = [] + + class ProtocolServer: + """Unit protocol double; no Java or Minecraft process is started.""" + def __init__(self, runtime, java, launcher, label): + self.runtime, self.label = runtime, label + self.closed = False + self.identity = len(owner.processes) * 100 + owner.processes.append(self) + self.definition = native.read_json(runtime / "lifecycle-fixture.json") + (runtime / f"console-{label}.log").write_text(BOOT_PROOF) + + def send(self, command): + owner.commands.append((self.label, command)) + if mutate: + mutate(self, command) + if not command.startswith("advlifecycle "): + return + _, operation, label = command.split() + if label == failure: + raise AssertionError("unit-injected probe failure") + amount = {"oil-empty": 0, "oil-first-fill": 1000, "oil-restored": 1000, + "oil-refilled": 2000, "oil-retained": 2000}.get(label, 0) + value = snapshot(self.definition, amount, self.identity) + if transform: + transform(label, value) + write_json(self.runtime / "advantage-works-results" / f"lifecycle-{label}.json", value) + + def wait(self, token, timeout): + pass + + def close(self): + self.closed = True + if shutdown: + raise AssertionError("unit-injected shutdown failure") + + return ProtocolServer + + def report(self, args): + return native.read_json(args.output / "native117-oil-results.json") + + def test_power_definitions_change_only_the_registry_namespace(self): + original = oil_transfer.fixtures() + lanes = native.fixtures(dict(jars=[])) + self.assertEqual(lanes, oil_transfer.fixtures("basepower")) + lanes[0]["source"][0] += 1 + self.assertEqual(oil_transfer.fixtures(), original) + + def test_optional_mineralogy_has_two_distinct_native_lanes_without_mutating_power(self): + pins = dict(jars=[self.mineralogy_pin()]) + lanes = native.fixtures(pins) + self.assertEqual(lanes[:4], oil_transfer.fixtures("basepower")) + self.assertEqual(len(lanes), 6) + for lane, template in zip(lanes[4:], lanes[:2]): + self.assertEqual(lane["fluid"], "mineralogy:crude_oil") + self.assertEqual(lane["bucket"], "mineralogy:crude_oil_bucket") + self.assertEqual(lane["tank"], template["tank"]) + for point, old in zip(lane["witnesses"], template["witnesses"]): + self.assertEqual(point, [old[0] + 256, *old[1:]]) + native.require_document(snapshot(lanes, 1000), lanes) + self.assertEqual(len(oil_transfer.inspect(snapshot(lanes, 1000), lanes, 1000)), 6) + + def test_mineralogy_wrong_version_hash_or_duplicate_is_not_silently_skipped(self): + good = self.mineralogy_pin() + for jars in ([dict(good, version="6.0.0.113021")], [dict(good, sha256="0" * 64)], [good, good]): + with self.subTest(jars=jars), self.assertRaises(ValueError): + native.fixtures(dict(jars=jars)) + + def test_bounds_headroom_connectivity_overlap_and_duplicate_ids_are_strict(self): + def moved(lanes): + lanes[0]["source"][0] += 1 + mutations = [lambda lanes: lanes[0]["min"].__setitem__(0, 0), + lambda lanes: lanes[0]["max"].__setitem__(0, lanes[0]["min"][0] + 32), + lambda lanes: lanes[0]["source"].__setitem__(1, lanes[0]["max"][1]), + moved, lambda lanes: lanes.append(deepcopy(lanes[0]))] + for mutate in mutations: + lanes = oil_transfer.fixtures("basepower") + mutate(lanes) + with self.assertRaises(AssertionError): + native.require_bounds(lanes) + + def test_missing_extra_duplicate_and_moved_snapshot_witnesses_fail(self): + lanes = oil_transfer.fixtures("basepower") + for mutate in (lambda s: s["cases"].pop(lanes[0]["id"]), + lambda s: s["cases"].update(extra=[]), + lambda s: s["cases"][lanes[0]["id"]].pop(), + lambda s: s["cases"][lanes[0]["id"]].append(s["cases"][lanes[0]["id"]][0]), + lambda s: s["cases"][lanes[0]["id"]][0].update(pos=[0, 80, 0])): + document = snapshot(lanes, 1000) + mutate(document) + with self.assertRaises(AssertionError): + native.require_document(document, lanes) + + def test_existing_quantity_fluid_and_tile_identity_oracles_remain_strict(self): + lanes = native.fixtures(dict(jars=[])) + for amount in (0, 999, 1001, 2000): + with self.assertRaises(AssertionError): + oil_transfer.inspect(snapshot(lanes, amount), lanes, 1000) + wrong = snapshot(lanes, 1000) + wrong["cases"][lanes[0]["id"]][-1]["nbt"] = '{Tank:{Amount:1000,FluidName:"minecraft:water"}}' + with self.assertRaises(AssertionError): + oil_transfer.inspect(wrong, lanes, 1000) + with self.assertRaises(AssertionError): + oil_transfer.require_same_machines(snapshot(lanes, 1000), snapshot(lanes, 2000, 100), lanes) + + def test_native_log_requires_both_versions_and_rejects_runtime_command_and_probe_errors(self): + log = self.root / "console.log" + log.write_text(BOOT_PROOF + "[watcher/DEBUG] " + native.CONFIG_NOTICE + "\n") + native.require_native_boot(log) + for text in (BOOT_PROOF.replace("1.17.1", "1.13.2"), BOOT_PROOF.replace("37.1.1", "25.0.223"), + *[BOOT_PROOF + failure + "\n" for failure in ( + "[main/ERROR] bad", "[main/FATAL] actual fatal", "[watcher/FATAL] " + native.CONFIG_NOTICE, + "NoSuchFieldError: bad", + "NoSuchMethodError: bad", "Failed to reload data packs", "Lifecycle probe failed", + "ADVLIFECYCLE FAIL", "Unknown or incomplete command", + "Incorrect argument for command", "An unexpected error occurred trying to execute that command")]): + log.write_text(text) + with self.subTest(text=text), self.assertRaises(AssertionError): + native.require_native_boot(log) + + def test_prepare_copies_pinned_libraries_only_never_seed_world_or_settings(self): + args, profile, pins = self.profile() + before = fingerprint(profile[0]) + runtime = self.root / "target" + definition = native.fixtures(pins) + self.assertEqual(native.prepare(runtime, profile, definition, args.port, pins, before), LAUNCHER) + self.assertFalse((runtime / WORLD).exists()) + self.assertFalse((runtime / "mods/unpinned.jar").exists()) + self.assertIn("server-port=25576", (runtime / "server.properties").read_text()) + self.assertNotIn("3;minecraft:bedrock", (runtime / "server.properties").read_text()) + self.assertEqual(fingerprint(profile[0]), before) + self.assertEqual(native.read_json(runtime / "lifecycle-fixture.json"), definition) + + def test_staged_jars_cannot_follow_a_changed_original_instead_of_captured_pins(self): + args, profile, pins = self.profile() + before = fingerprint(profile[0]) + runtime = self.root / "target" + native.prepare(runtime, profile, native.fixtures(pins), args.port, pins, before) + jar = profile[2][0] + jar.write_bytes(b"changed after pin") + (runtime / "mods" / jar.name).write_bytes(jar.read_bytes()) + write_json(runtime / "input-jars.json", [dict(file=p.name, sha256=digest(p)) for p in profile[2]]) + with self.assertRaises(AssertionError): + native.verify_staged(runtime, pins, before) + + def test_staged_launcher_and_library_tampering_fail(self): + args, profile, pins = self.profile() + before = fingerprint(profile[0]) + runtime = self.root / "target" + native.prepare(runtime, profile, native.fixtures(pins), args.port, pins, before) + for path in (runtime / LAUNCHER, runtime / "libraries/subdir/runtime.jar"): + old = path.read_bytes() + path.write_bytes(b"changed") + with self.assertRaises(AssertionError): + native.verify_staged(runtime, pins, before) + path.write_bytes(old) + + def test_only_ordered_initial_construction_is_allowed_and_never_after_baseline(self): + args, profile, pins = self.profile() + lanes = native.fixtures(pins) + runner = native.NativeOilRun(self.root, args.java8, LAUNCHER, lanes, pins, {}) + runner.boots = [native.BOOT_LABELS[0]] + runner.server = SimpleNamespace(send=lambda command: None) + for command in ("advlifecycle offer illicit", "data merge block 11272 80 2056 {}", + "setblock 11272 81 2056 basepower:crude_oil", + "advworks reset all", "advlifecycle snapshot ok\nstop"): + with self.subTest(command=command), self.assertRaises(AssertionError): + runner.command(command) + with self.assertRaises(AssertionError): + runner.seal_construction() + initial = native.construction_commands(lanes) + for command in initial: + runner.command(command) + runner.seal_construction() + for command in initial: + with self.assertRaises(AssertionError): + runner.command(command) + + def test_buckets_require_empty_or_restored_gate_and_at_most_two_placements(self): + args, profile, pins = self.profile() + runner = native.NativeOilRun(self.root, args.java8, LAUNCHER, native.fixtures(pins), pins, {}) + runner.server = SimpleNamespace(send=lambda command: None) + runner.boots = [native.BOOT_LABELS[0]] + with self.assertRaises(AssertionError): + runner.command("advlifecycle place-buckets too-soon") + with self.assertRaises(AssertionError): + runner.permit_buckets() + runner.pending_construction = [] + runner.seal_construction() + for index in range(2): + runner.boots = list(native.BOOT_LABELS[:index + 1]) + runner.permit_buckets() + runner.command("advlifecycle place-buckets permitted") + with self.assertRaises(AssertionError): + runner.command("advlifecycle place-buckets duplicate") + runner.boots = list(native.BOOT_LABELS) + with self.assertRaises(AssertionError): + runner.permit_buckets() + + def test_complete_protocol_three_boots_two_source_only_placements_and_all_evidence(self): + args, profile, pins = self.profile() + before = fingerprint(profile[0]) + with patch.object(native, "Server", self.server_factory()), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 0) + report = self.report(args) + self.assertEqual(report["status"], "PASS") + self.assertEqual(report["full_process_restarts"], 2) + self.assertEqual(report["bucket_placements"], 2) + self.assertEqual([phase["phase"] for phase in report["phases"]], ["first-fill", "restored", "refilled", "retained"]) + self.assertEqual([p["cases"][0]["total_mB"] for p in report["phases"]], [1000, 1000, 2000, 2000]) + self.assertEqual(report["laneCount"], 4) + self.assertEqual(report["seedFingerprintBefore"], before) + self.assertEqual(report["seedFingerprintAfter"], before) + self.assertTrue(report["helpers_unchanged"] and report["seed_unchanged"] and report["manifest_unchanged"]) + self.assertEqual([p.label for p in self.processes], list(native.BOOT_LABELS)) + self.assertTrue(all(p.closed for p in self.processes)) + later = [command for label, command in self.commands if label != native.BOOT_LABELS[0]] + self.assertFalse(any(command.startswith(("fill ", "setblock ", "data ")) for command in later)) + self.assertEqual(sum("place-buckets" in command for command in later), 1) + self.assertEqual((args.output / "target-manifest.json").read_bytes(), args.target_manifest.read_bytes()) + + def test_probe_failure_is_retained_with_seed_fingerprints_and_process_closed(self): + args, profile, _ = self.profile() + before = fingerprint(profile[0]) + with patch.object(native, "Server", self.server_factory(failure="oil-refilled")), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + report = self.report(args) + self.assertEqual(report["status"], "FAIL") + self.assertIn("probe failure", report["failure"]) + self.assertEqual(report["seedFingerprintAfter"], before) + self.assertTrue(all(p.closed for p in self.processes)) + + def test_shutdown_failure_cannot_be_success(self): + args, _, _ = self.profile() + with patch.object(native, "Server", self.server_factory(shutdown=True)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + self.assertIn("shutdown failure", self.report(args)["failure"]) + + def test_restart_quantity_loss_blocks_refill_not_repaired_by_new_sources(self): + args, _, _ = self.profile() + def transform(label, value): + if label == "oil-restored": + lane = next(iter(value["cases"].values())) + lane[-1]["nbt"] = lane[-1]["nbt"].replace("Amount:1000", "Amount:999") + with patch.object(native, "Server", self.server_factory(transform=transform)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + self.assertIn("Lost/duplicated oil", self.report(args)["failure"]) + self.assertEqual(sum("place-buckets" in command for _, command in self.commands), 1) + self.assertEqual(len(self.processes), 2) + + def test_nonempty_baseline_cannot_place_any_bucket(self): + args, _, _ = self.profile() + def transform(label, value): + if label == "oil-empty": + next(iter(value["cases"].values()))[-1]["nbt"] = '{Tank:{Amount:1,FluidName:"basepower:crude_oil"}}' + with patch.object(native, "Server", self.server_factory(transform=transform)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + self.assertIn("Fixture was not empty", self.report(args)["failure"]) + self.assertFalse(any("place-buckets" in command for _, command in self.commands)) + + def test_refill_cannot_replace_a_machine_even_with_exact_amount(self): + args, _, _ = self.profile() + def transform(label, value): + if label == "oil-refilled": + next(iter(value["cases"].values()))[-1]["tileIdentity"] += 1 + with patch.object(native, "Server", self.server_factory(transform=transform)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + self.assertIn("Refill replaced a machine", self.report(args)["failure"]) + self.assertEqual(len(self.processes), 2) + + def test_missing_initial_identity_cannot_qualify(self): + args, _, _ = self.profile() + def transform(label, value): + if label == "oil-empty": + next(iter(value["cases"].values()))[1].pop("tileIdentity") + with patch.object(native, "Server", self.server_factory(transform=transform)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + self.assertFalse(any("place-buckets" in command for _, command in self.commands)) + + def test_staging_failure_still_retains_full_seed_fingerprints(self): + args, profile, _ = self.profile() + before = fingerprint(profile[0]) + with patch.object(native, "prepare", side_effect=AssertionError("unit-injected staging failure")): + self.assertEqual(native.run(args), 1) + report = self.report(args) + self.assertIn("staging failure", report["failure"]) + self.assertEqual(report["seedFingerprintBefore"], before) + self.assertEqual(report["seedFingerprintAfter"], before) + + def test_wrong_launcher_and_out_of_order_boots_are_rejected_before_process(self): + args, _, pins = self.profile() + runner = native.NativeOilRun(self.root, args.java8, "forge-1.13.2-25.0.223.jar", native.fixtures(pins), pins, {}) + with self.assertRaises(AssertionError): + runner.boot(native.BOOT_LABELS[0]) + runner.launcher = LAUNCHER + for label in (native.BOOT_LABELS[1], "invented-boot"): + with self.assertRaises(AssertionError): + runner.boot(label) + + def test_late_log_errors_are_audited_after_stop(self): + args, _, _ = self.profile() + def mutate(server, command): + if command == "save-all flush": + with (server.runtime / f"console-{server.label}.log").open("a") as stream: + stream.write("[main/ERROR] unit-injected late failure\n") + with patch.object(native, "Server", self.server_factory(mutate=mutate)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + self.assertIn("runtime_audit_failure", self.report(args)) + + def test_full_seed_fingerprint_catches_changes_outside_world(self): + args, profile, _ = self.profile() + def mutate(server, command): + (profile[0] / "server.properties").write_text("unit-injected concurrent seed change") + with patch.object(native, "Server", self.server_factory(mutate=mutate)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + report = self.report(args) + self.assertFalse(report["seed_unchanged"]) + self.assertIn("server.properties", report["seedFingerprintAfter"]) + + def test_mutable_manifest_is_detected_and_captured_manifest_retained(self): + args, _, _ = self.profile() + original = args.target_manifest.read_bytes() + def mutate(server, command): + args.target_manifest.write_bytes(original + b"\n") + with patch.object(native, "Server", self.server_factory(mutate=mutate)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + self.assertFalse(self.report(args)["manifest_unchanged"]) + self.assertEqual((args.output / "target-manifest.json").read_bytes(), original) + + def test_stale_input_manifest_fails_before_staging_or_process(self): + args, profile, _ = self.profile() + profile[2][0].write_bytes(b"not the pinned jar") + with patch.object(native, "prepare") as prepare, self.assertRaises(AssertionError): + native.run(args) + prepare.assert_not_called() + self.assertFalse(args.output.exists()) + + def test_output_cannot_overlap_seed_manifest_or_leave_scoped_build(self): + args, profile, _ = self.profile() + for output in (profile[0], profile[0] / "native117-oil-unsafe", args.target_manifest, + RIG / "migration/native117-oil-unsafe", self.root / "wrong-prefix"): + args.output = output + with self.subTest(output=output), self.assertRaises((ValueError, FileExistsError)): + native.run(args) + + def test_invalid_port_or_interval_fails_before_staging(self): + args, _, _ = self.profile() + for port, seconds in ((0, 8), (65536, 8), (25576, 1), (25576, 121)): + args.port, args.seconds = port, seconds + with self.assertRaises(ValueError): + native.run(args) + self.assertFalse(args.output.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_operating_import.py b/test-rig/migration/test_native117_operating_import.py new file mode 100644 index 00000000..8b5f523c --- /dev/null +++ b/test-rig/migration/test_native117_operating_import.py @@ -0,0 +1,45 @@ +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from matrix import digest +from native117_operating_import import approved_source + + +class OperatingSourceApprovalTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.report = self.root / 'native116-machine-operation-results.json' + + def write(self, value): + self.report.write_text(json.dumps(value)) + return digest(self.report) + + def test_changed_source_report_is_rejected_before_any_profile_is_opened(self): + checksum = self.write(dict(status='PASS')) + self.write(dict(status='FAIL')) + with patch('native117_operating_import.verify_source_profile') as profile: + with self.assertRaisesRegex(AssertionError, 'report changed'): + approved_source(self.root, checksum) + profile.assert_not_called() + + def test_failed_partial_or_rewritten_protocol_is_not_source_approval(self): + baseline = dict(status='PASS', group='all', protocolUnchanged=True, + phases={phase: dict(status='PASS', cases=132) for phase in ('operation', 'restart')}) + changes = [dict(status='FAIL'), dict(group='steam'), dict(protocolUnchanged=False), + dict(phases={'operation': dict(status='PASS', cases=132)}), + dict(phases={phase: dict(status='PASS', cases=131) for phase in ('operation', 'restart')})] + for change in changes: + with self.subTest(change=change), patch('native117_operating_import.verify_source_profile') as profile: + checksum = self.write(dict(baseline, **change)) + with self.assertRaisesRegex(AssertionError, 'complete accepted 132-case source'): + approved_source(self.root, checksum) + profile.assert_not_called() + + +if __name__ == '__main__': + unittest.main() diff --git a/test-rig/migration/test_native117_partial.py b/test-rig/migration/test_native117_partial.py new file mode 100644 index 00000000..a96fd035 --- /dev/null +++ b/test-rig/migration/test_native117_partial.py @@ -0,0 +1,115 @@ +"""Native transport controls do not change historical fixture/oracle semantics.""" + +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import extended_lifecycle +from native117_partial import (NativeExtendedRun, NativePartialRun, common_parser, selected_chunks, + subset_load_issues, subset_unload_pending) +from native117_lifecycle import NativeLifecycleRun + + +def snapshot(tick, loaded=True, loads=1, unloads=0, events=None): + return dict(worldTime=tick, chunks=[dict(pos=[1, 2], loaded=loaded, loads=loads, unloads=unloads)], + chunkEvents=events if events is not None else [dict(pos=[1, 2], operation="load")], + cases={"case": [dict(pos=[16, 80, 32], loaded=loaded)]}) + + +class NativePartialTest(unittest.TestCase): + def test_assigned_native117_core_port_is_accepted(self): + arguments = ["--source", "source", "--source-manifest", "source.json", + "--source-hashes", "hashes.json", "--target-manifest", "target.json", + "--java8", "java.exe", "--output", "output", "--port", "25580"] + self.assertEqual(common_parser("core").parse_args(arguments).port, 25580) + self.assertEqual(common_parser("core").parse_args(arguments[:-1] + ["25576"]).port, 25576) + with patch("sys.stderr"), self.assertRaises(SystemExit): + common_parser("core").parse_args(arguments[:-1] + ["25584"]) + + def test_extended_keeps_historical_operations(self): + self.assertIs(NativeExtendedRun.build, extended_lifecycle.ExtendedRun.build) + self.assertIs(NativeExtendedRun.partial, extended_lifecycle.ExtendedRun.partial) + self.assertIs(NativeExtendedRun.wait_step, extended_lifecycle.ExtendedRun.wait_step) + self.assertIs(NativeExtendedRun.save_read_guards, NativeLifecycleRun.save_read_guards) + + def test_group_selection_does_not_acquire_unrequested_group(self): + definition = [dict(groups=dict(machine=[[1, 2]], supply=[[2, 2]]))] + self.assertEqual(selected_chunks(definition, "machine"), {(1, 2)}) + + def test_load_requires_actual_event_not_loaded_flag_alone(self): + self.assertTrue(subset_load_issues(snapshot(1, False), snapshot(2, events=[]), {(1, 2)})) + + def test_load_requires_increment_after_absence(self): + self.assertTrue(subset_load_issues(snapshot(1, False), snapshot(2), {(1, 2)})) + self.assertFalse(subset_load_issues(snapshot(1, False), snapshot(2, loads=2), {(1, 2)})) + + def test_load_rejects_unloaded_witness(self): + current = snapshot(2, loads=2) + current["cases"]["case"][0]["loaded"] = False + self.assertTrue(subset_load_issues(snapshot(1, False), current, {(1, 2)})) + + def test_unload_requires_counter_and_callback(self): + before = snapshot(1) + current = snapshot(2, loaded=False, unloads=1) + self.assertEqual(subset_unload_pending(before, current, {(1, 2)}), (True, ["(1, 2)"])) + current["chunkEvents"].append(dict(pos=[1, 2], operation="unload")) + self.assertEqual(subset_unload_pending(before, current, {(1, 2)}), (True, [])) + + def test_unload_does_not_accept_callback_without_absence(self): + current = snapshot(2, unloads=1, events=[dict(pos=[1, 2], operation="unload")]) + self.assertEqual(subset_unload_pending(snapshot(1), current, {(1, 2)}), (False, [])) + + def test_unknown_load_and_release_groups_fail_before_commands(self): + runner = NativePartialRun(Path("unused"), None, None, []) + with patch.object(runner, "_raw_probe") as raw: + for operation in ("load-foreign", "release-foreign"): + with self.assertRaises(ValueError): + runner.probe(operation, "unknown") + raw.assert_not_called() + + def test_unload_callback_wait_preserves_actual_raw_event_and_tick(self): + with tempfile.TemporaryDirectory() as temp: + runner = NativePartialRun(Path(temp), None, None, []) + runner._released_groups["machine"] = dict(before=snapshot(1), selected={(1, 2)}, deadline=1000) + pending = snapshot(2, False, unloads=0) + actual = snapshot(3, False, unloads=1, events=[dict(pos=[1, 2], operation="unload")]) + with patch.object(runner, "_raw_probe", side_effect=[pending, actual]) as raw, \ + patch.object(runner, "_canonical") as canonical, patch.object(runner, "_record"), \ + patch("native117_partial.time.monotonic", return_value=100), patch("native117_partial.time.sleep"): + selected = runner._snapshot_ready("snapshot", "absent") + self.assertIs(selected, actual) + self.assertEqual(raw.call_count, 2) + self.assertEqual(selected["worldTime"], 3) + canonical.assert_called_once_with("absent-native-pending-01", "absent") + + def test_load_selects_first_real_tick_even_if_gameplay_wrong(self): + definition = [dict(min=[16, 0, 32], max=[31, 100, 47])] + with tempfile.TemporaryDirectory() as temp: + runner = NativePartialRun(Path(temp), None, None, definition) + rows = [snapshot(1, False, loads=0), snapshot(2, loads=1), snapshot(2, loads=1), snapshot(3, loads=1)] + rows[-1]["cases"]["case"][0]["nbt"] = '{progress:0,WrongGameplay:1}' + with patch.object(runner, "_raw_probe", side_effect=rows) as raw, \ + patch.object(runner, "_canonical") as canonical, patch.object(runner, "_record"), \ + patch("native117_partial.time.sleep"): + selected = runner._load_ready("load", "reload") + self.assertIs(selected, rows[-1]) + self.assertEqual(raw.call_count, 4) + canonical.assert_called_once_with("reload-native-pending-002", "reload") + + def test_canonical_is_exact_raw_bytes_and_never_overwritten(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) / "advantage-works-results" + root.mkdir() + raw = root / "lifecycle-selected.json" + raw.write_bytes(b'{"worldTime":5,"raw":true}\n') + runner = NativePartialRun(Path(temp), None, None, []) + runner._canonical("selected", "canonical") + self.assertEqual(raw.read_bytes(), (root / "lifecycle-canonical.json").read_bytes()) + with self.assertRaises(FileExistsError): + runner._canonical("selected", "canonical") + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_player_layout.py b/test-rig/migration/test_native117_player_layout.py new file mode 100644 index 00000000..2d4ebe0e --- /dev/null +++ b/test-rig/migration/test_native117_player_layout.py @@ -0,0 +1,21 @@ +import unittest + +from native117_player_layout import verdict + + +class NativeLayoutOracleTest(unittest.TestCase): + def test_scope_is_exact_player_data_not_unparented_fixture_turbine_operation(self): + player = {"Inventory": [{"id": "steamadvantage:musket", "Count": 1, "Slot": 35, + "tag": {"Nested": {"Items": [{"id": "electricadvantage:lithium_battery", "Count": 3, "tag": {"energy": 123}}]}}}], + "EnderItems": [{"id": "poweradvantage:crude_oil_bucket", "Count": 1, "Slot": 26}]} + expected = {"file": player, "adapter": player, "entities": {"unparented-turbine": {"id": "electricadvantage:hydroturbine"}}} + self.assertEqual("PASS", verdict(expected, {"file": player, "adapter": player})["status"]) + + def test_extra_nested_item_tag_is_rejected(self): + expected = {"file": {"Inventory": [{"id": "x:y", "Count": 3, "tag": {"energy": 123}}]}, "adapter": {}} + actual = {"file": {"Inventory": [{"id": "x:y", "Count": 3, "tag": {"energy": 123, "extra": "changed"}}]}, "adapter": {}} + self.assertEqual("FAIL", verdict(expected, actual)["status"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_player_spawn_default.py b/test-rig/migration/test_native117_player_spawn_default.py new file mode 100644 index 00000000..44963748 --- /dev/null +++ b/test-rig/migration/test_native117_player_spawn_default.py @@ -0,0 +1,274 @@ +import copy +import hashlib +import json +from pathlib import Path +import unittest +from unittest.mock import patch + +import dimensions +import native117_dimension_assessment as assessment +from test_native117_dimension_assessment import PROOF, states + + +FIXTURE = Path(__file__).parent / "fixtures/native117_player_spawn_default.json" +LEGACY_FIXTURE = FIXTURE.with_name("native116_player_spawn_default.json") +COORDINATES = ("SpawnX", "SpawnY", "SpawnZ") + + +def player_states(): + before, after = states() + for state in (before, after): + state["file"].update(SpawnX=160, SpawnY=100, SpawnZ=24, SpawnForced=1, Dimension=-1) + state["adapter"] = copy.deepcopy(state["file"]) + state["playerWorldDimension"] = -1 + for root in ("file", "adapter"): + after[root]["SpawnDimension"] = 0 + return before, after + + +def assess(before, after): + raw = dimensions.verdict(before, after) + with patch.object(assessment, "load_native_proof", return_value=PROOF): + return assessment.assess(before, after, raw, Path("reviewed-control.log"), "survival") + + +class NativePlayerSpawnDefaultTest(unittest.TestCase): + def test_inherited_payloads_and_historical_provenance_are_preserved_exactly(self): + legacy_bytes = LEGACY_FIXTURE.read_bytes() + legacy = json.loads(legacy_bytes) + fixture = json.loads(FIXTURE.read_text()) + self.assertEqual("BE92D8F3544634BF2E8BAF12BDD4EA4DCDED8467D5BE748893640963E71EC4E3", + hashlib.sha256(legacy_bytes).hexdigest().upper()) + self.assertEqual(LEGACY_FIXTURE.name, fixture["inheritedFixture"]["fixture"]) + self.assertEqual(hashlib.sha256(legacy_bytes).hexdigest().upper(), + fixture["inheritedFixture"]["sha256"]) + # Canonical JSON also distinguishes bool/int and int/float payload types. + self.assertEqual(json.dumps(legacy["cases"], sort_keys=True), + json.dumps(fixture["cases"], sort_keys=True)) + self.assertEqual(legacy["primarySourceProof"][:2], fixture["primarySourceProof"][:2]) + self.assertEqual(legacy["scope"], fixture["scope"]) + self.assertEqual("minecraft:overworld", fixture["nativeTargetSemantics"]["nativeEncodedOverworld"]) + self.assertEqual(0, fixture["nativeTargetSemantics"]["historicalSemanticOverworld"]) + self.assertEqual("PATCHED_117_PRIMARY_SOURCE_PROOF_NOT_NEW_PLAYER_RUNTIME_QUALIFICATION", + fixture["nativeTargetSemantics"]["qualificationScope"]) + + def test_authentic_110_and_112_player_roots_differ_only_by_explicit_default(self): + fixture = json.loads(FIXTURE.read_text()) + self.assertEqual(["original110", "legacy112"], [case["name"] for case in fixture["cases"]]) + for case in fixture["cases"]: + with self.subTest(case=case["name"]): + original = copy.deepcopy(case) + self.assertEqual(case["after"], assessment.semantic_player_spawn_defaults(case["before"])) + self.assertEqual(case["after"], assessment.semantic_player_spawn_defaults(case["after"])) + self.assertEqual(original, case) + + def test_authentic_player_payload_preserves_raw_health_fail_and_exact_inventory(self): + for case in json.loads(FIXTURE.read_text())["cases"]: + with self.subTest(case=case["name"]): + before, after = states() + before.update(copy.deepcopy(case["before"])) + after.update(copy.deepcopy(case["after"])) + raw = dimensions.verdict(before, after) + result = assess(before, after) + self.assertEqual("PASS_WITH_NATIVE_DIFFERENCE", result["status"]) + self.assertEqual("FAIL", result["rawStrictStatus"]) + self.assertEqual(raw, result["rawStrictResult"]) + self.assertEqual([assessment.HEALTH_DIFFERENCE], raw["differences"]) + + def test_primary_patched_source_hashes_and_exact_entry_lines_are_captured(self): + proof = json.loads(FIXTURE.read_text())["primarySourceProof"] + self.assertEqual([ + "7D0148D301495D7A6E24D1030B7FD98163E748A8E0F37FCF74D1B7C4A05BB94B", + "C2C7CAF7A08BD441992F6A38B2EC93195465D41D4127630A9C0F2102C921CF58", + "531053A81F6591CA8CFD252154920B62D2CC509CB3DDC3FAC2E37F368003C5B6", + ], [record["sha256"] for record in proof]) + old110 = proof[0]["entries"] + self.assertEqual("net/minecraft/world/WorldProvider.java", old110[1]["entry"]) + self.assertEqual(404, old110[1]["excerpts"][0]["firstLine"]) + self.assertIn("return 0;", old110[1]["excerpts"][0]["text"]) + old112 = proof[1]["entries"][0]["excerpts"] + self.assertEqual(1005, old112[0]["firstLine"]) + self.assertIn('getBoolean("HasSpawnDimensionSet")', old112[0]["text"]) + self.assertIn("spawnDimension != null ? spawnDimension : 0", old112[2]["text"]) + target = proof[2] + self.assertEqual(("1.17.1", "37.1.1", "official", "1.17.1", "20210706.113038"), + tuple(target[key] for key in ("minecraftVersion", "forgeVersion", "mappingChannel", + "mappingVersion", "mcpVersion"))) + self.assertTrue(target["archive"].replace("\\", "/").endswith( + "/1.17.1-37.1.1/official/1.17.1/remapped-javadoc.jar")) + native, level, location = target["entries"] + self.assertEqual("net/minecraft/server/level/ServerPlayer.java", native["entry"]) + self.assertEqual("FAA1A410A3D7CAC6DFDE7F15BAF5AFFC179E0573DC56E67FA21C9020BB682561", + native["entrySha256"]) + self.assertEqual([(171, 171), (294, 301), (329, 339)], + [(excerpt["firstLine"], excerpt["lastLine"]) for excerpt in native["excerpts"]]) + self.assertEqual(" private ResourceKey respawnDimension = Level.OVERWORLD;", + native["excerpts"][0]["text"]) + self.assertIn('contains("SpawnX", 99)', native["excerpts"][1]["text"]) + self.assertIn('contains("SpawnY", 99)', native["excerpts"][1]["text"]) + self.assertIn('contains("SpawnZ", 99)', native["excerpts"][1]["text"]) + self.assertIn('if (p_9131_.contains("SpawnDimension"))', native["excerpts"][1]["text"]) + self.assertIn("orElse(Level.OVERWORLD)", native["excerpts"][1]["text"]) + self.assertIn("if (this.respawnPosition != null)", native["excerpts"][2]["text"]) + self.assertIn("ResourceLocation.CODEC.encodeStart(NbtOps.INSTANCE, this.respawnDimension.location())", + native["excerpts"][2]["text"]) + self.assertIn('put("SpawnDimension"', native["excerpts"][2]["text"]) + self.assertEqual("net/minecraft/world/level/Level.java", level["entry"]) + self.assertEqual("927DD75F1CFEEA6CD63FC679992258327DCF7A1BDD2543FEFEB5E3166547A6D7", + level["entrySha256"]) + self.assertIn('OVERWORLD = ResourceKey.create(Registry.DIMENSION_REGISTRY, new ResourceLocation("overworld"))', + level["excerpts"][0]["text"]) + self.assertEqual("net/minecraft/resources/ResourceLocation.java", location["entry"]) + self.assertEqual("F392A113C24DF2B087AF8289D987C9F1F20C2ECD2966D522199CF6C6E486CD98", + location["entrySha256"]) + self.assertIn("CODEC = Codec.STRING.comapFlatMap(ResourceLocation::read, ResourceLocation::toString)", + location["excerpts"][0]["text"]) + self.assertIn('new String[]{"minecraft", p_135833_}', location["excerpts"][3]["text"]) + self.assertIn('return this.namespace + ":" + this.path;', location["excerpts"][4]["text"]) + + def test_native_excerpts_record_valid_exact_line_spans(self): + for entry in json.loads(FIXTURE.read_text())["primarySourceProof"][2]["entries"]: + for excerpt in entry["excerpts"]: + with self.subTest(entry=entry["entry"], firstLine=excerpt["firstLine"]): + self.assertGreaterEqual(excerpt["firstLine"], 1) + self.assertGreaterEqual(excerpt["lastLine"], excerpt["firstLine"]) + self.assertEqual(excerpt["lastLine"] - excerpt["firstLine"] + 1, + len(excerpt["text"].split("\n"))) + + def test_equivalence_is_symmetric_idempotent_and_never_mutates_callers(self): + before, after = player_states() + original = copy.deepcopy((before, after)) + old = assessment.semantic_player_spawn_defaults(before) + new = assessment.semantic_player_spawn_defaults(after) + self.assertEqual(old["file"], new["file"]) + self.assertEqual(old["adapter"], new["adapter"]) + self.assertEqual(old, assessment.semantic_player_spawn_defaults(old)) + self.assertEqual(original, (before, after)) + + def test_all_signed32_integral_coordinates_are_retained_exactly(self): + for value in (-(1 << 31), -1, 0, (1 << 31) - 1): + before, after = player_states() + for state in (before, after): + for root in ("file", "adapter"): + for key in COORDINATES: + state[root][key] = value + self.assertEqual("PASS_WITH_NATIVE_DIFFERENCE", assess(before, after)["status"]) + + def test_missing_any_coordinate_does_not_default(self): + for root in ("file", "adapter"): + for key in COORDINATES: + before, after = player_states() + del before[root][key]; del after[root][key] + self.assertNotIn("SpawnDimension", assessment.semantic_player_spawn_defaults(before)[root]) + self.assertEqual("FAIL", assess(before, after)["status"]) + + def test_no_coordinates_do_not_default(self): + before, after = player_states() + for state in (before, after): + for root in ("file", "adapter"): + for key in COORDINATES: + del state[root][key] + self.assertEqual("FAIL", assess(before, after)["status"]) + + def test_bool_float_string_malformed_or_overflow_coordinates_do_not_default(self): + for value in (True, False, 0.0, 160.0, "160", None, [], {}, 1 << 31, -(1 << 31) - 1): + for root in ("file", "adapter"): + for key in COORDINATES: + before, after = player_states() + before[root][key] = value; after[root][key] = value + self.assertNotIn("SpawnDimension", assessment.semantic_player_spawn_defaults(before)[root]) + self.assertEqual("FAIL", assess(before, after)["status"]) + + def test_explicit_nonzero_bool_float_string_or_malformed_dimensions_are_not_equated(self): + for value in (-1, 1, True, False, 0.0, "0", "minecraft:overworld", None, [], {}): + for root in ("file", "adapter"): + before, after = player_states(); after[root]["SpawnDimension"] = value + self.assertEqual(after[root], assessment.semantic_player_spawn_defaults(after)[root]) + self.assertEqual("FAIL", assess(before, after)["status"]) + + def test_matching_nonzero_dimension_is_preserved_without_rewriting(self): + before, after = player_states() + for state in (before, after): + for root in ("file", "adapter"): + state[root]["SpawnDimension"] = -1 + self.assertEqual(-1, assessment.semantic_player_spawn_defaults(before)["file"]["SpawnDimension"]) + self.assertEqual("PASS_WITH_NATIVE_DIFFERENCE", assess(before, after)["status"]) + + def test_changed_coordinate_or_forced_flag_cannot_be_classified(self): + for root in ("file", "adapter"): + for key in (*COORDINATES, "SpawnForced"): + before, after = player_states(); after[root][key] += 1 + self.assertEqual("FAIL", assess(before, after)["status"]) + + def test_removed_or_changed_type_of_forced_flag_remains_exact(self): + for root in ("file", "adapter"): + for value in (None, True, 1.0, "1"): + before, after = player_states() + if value is None: + del after[root]["SpawnForced"] + else: + after[root]["SpawnForced"] = value + self.assertEqual("FAIL", assess(before, after)["status"]) + + def test_unknown_extra_root_or_player_keys_cannot_be_dropped(self): + for root in (None, "file", "adapter"): + before, after = player_states() + target = after if root is None else after[root] + target["UnknownPrivateKey"] = {"keep": [1, 2]} + self.assertEqual("FAIL", assess(before, after)["status"]) + + def test_entity_item_nested_and_private_player_like_compounds_are_untouched(self): + before, _ = player_states() + payload = {"SpawnX": 160, "SpawnY": 100, "SpawnZ": 24, "SpawnForced": 1} + before["entities"]["passenger-wolf"]["Private"] = copy.deepcopy(payload) + before["entities"]["other"] = copy.deepcopy(payload) + before["file"]["Private"] = copy.deepcopy(payload) + before["file"]["Inventory"][0]["tag"]["PrivateSpawn"] = copy.deepcopy(payload) + before["file"]["RootVehicle"] = {"Entity": copy.deepcopy(payload)} + before["other"] = copy.deepcopy(payload) + result = assessment.semantic_player_spawn_defaults(before) + for root in ("entities", "other"): + self.assertEqual(before[root], result[root]) + for key in ("Private", "Inventory", "RootVehicle"): + self.assertEqual(before["file"][key], result["file"][key]) + + def test_new_nested_spawn_dimension_is_not_classified_as_a_player_default(self): + for location in ("entity", "item", "private"): + before, after = player_states() + if location == "entity": + after["entities"]["passenger-wolf"]["SpawnDimension"] = 0 + elif location == "item": + after["file"]["Inventory"][0]["tag"]["SpawnDimension"] = 0 + else: + after["adapter"]["Private"] = {"SpawnDimension": 0} + self.assertEqual("FAIL", assess(before, after)["status"]) + + def test_attribute_count_and_item_payload_changes_still_fail(self): + for change in ("attribute", "count", "itemPayload"): + before, after = player_states() + if change == "attribute": + after["entities"]["passenger-wolf"]["Attributes"]["generic.maxHealth"]["Base"] = 21.0 + elif change == "count": + after["file"]["Inventory"][0]["Count"] += 1 + else: + after["adapter"]["Inventory"][0]["tag"]["extra"] = 1 + self.assertEqual("FAIL", assess(before, after)["status"]) + + def test_world_entity_identity_and_relationship_context_remains_exact(self): + for change in ("world", "dimension", "uuid", "owner", "relationship"): + before, after = player_states() + if change == "world": after["playerWorldDimension"] = 0 + elif change == "dimension": after["file"]["Dimension"] = 0 + elif change == "uuid": after["entities"]["passenger-wolf"]["UUIDMost"] = 29 + elif change == "owner": after["entities"]["passenger-wolf"]["OwnerUUID"] = "00000000-0000-0000-0000-000000000001" + else: after["relationships"]["extra"] = {"passengerUuids": ["unapproved"]} + self.assertEqual("FAIL", assess(before, after)["status"]) + + def test_default_equivalence_does_not_tolerate_other_health_values(self): + for health in (19.0, 20.00001, 21.0): + before, after = player_states(); after["entities"]["passenger-wolf"]["Health"] = health + self.assertEqual("FAIL", assess(before, after)["status"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_readiness.py b/test-rig/migration/test_native117_readiness.py new file mode 100644 index 00000000..1ba53ec4 --- /dev/null +++ b/test-rig/migration/test_native117_readiness.py @@ -0,0 +1,103 @@ +"""Adversarial receipt checks; these do not substitute for a native server run.""" +from copy import deepcopy +import json +from pathlib import Path +import tempfile +import unittest + +import native117_readiness as native + +BOUNDS = (74, 63, 75, 64) + + +def sample(): + initial = [dict(x=x, z=z, blockAndEntitiesLoaded=True, entityTicking=False) + for x, z in sorted(native.chunk_set(BOUNDS))] + ready = [dict(row, entityTicking=True) for row in initial] + return dict(protocol=native.PROTOCOL, status="PASS", label="harvest-exercise", minecraft="1.17.1", + dimension="minecraft:overworld", bounds=dict(zip(("minX", "minZ", "maxX", "maxZ"), BOUNDS)), + checks=3, elapsedMillis=100, requestedWorldTime=1, completedWorldTime=4, + maximumChecks=1200, maximumWaitMillis=120000, chunks=ready, + transitions=[dict(check=1, worldTime=2, changes=initial), dict(check=3, worldTime=4, changes=ready)]) + + +class NativeReadinessTests(unittest.TestCase): + def test_actual_transition_replay_is_required_for_all_boundary_chunks(self): + value = sample() + self.assertIs(value, native.require_ready(value, BOUNDS, "harvest-exercise")) + + def test_missing_chunks_duplicates_wrong_positions_and_boolean_coordinates_fail(self): + for mutate in (lambda rows: rows.pop(), lambda rows: rows.append(rows[0]), + lambda rows: rows[0].update(x=999), lambda rows: rows[0].update(x=True)): + value = sample() + mutate(value["chunks"]) + with self.subTest(mutate=mutate), self.assertRaises(AssertionError): + native.require_ready(value, BOUNDS, "harvest-exercise") + + def test_either_native_readiness_fact_false_or_truthy_nonboolean_is_rejected(self): + for field in ("blockAndEntitiesLoaded", "entityTicking"): + for state in (False, 1, "true", None): + value = sample() + value["chunks"][0][field] = state + with self.subTest(field=field, state=state), self.assertRaises(AssertionError): + native.require_ready(value, BOUNDS, "harvest-exercise") + + def test_timeouts_failure_protocol_label_and_dimension_cannot_be_waived(self): + for field, bad in (("status", "FAIL"), ("protocol", "legacy"), ("label", "another"), + ("dimension", ""), ("dimension", "minecraft:the_nether"), + ("checks", 0), ("checks", True), ("checks", 1201), + ("elapsedMillis", 120001), ("maximumChecks", 1300), + ("maximumWaitMillis", 150000), ("completedWorldTime", 0)): + value = sample() + value[field] = bad + with self.subTest(field=field, bad=bad), self.assertRaises(AssertionError): + native.require_ready(value, BOUNDS, "harvest-exercise") + + def test_forged_truncated_or_redundant_transitions_cannot_prove_completion(self): + for mutate in (lambda value: value.update(transitions=[]), + lambda value: value["transitions"][0]["changes"].pop(), + lambda value: value["transitions"][1].update(check=1), + lambda value: value["transitions"][1].update(worldTime=0), + lambda value: value["transitions"].pop(), + lambda value: value["transitions"][1]["changes"][0].update(entityTicking=False), + lambda value: value.update(checks=4)): + value = sample() + mutate(value) + with self.subTest(mutate=mutate), self.assertRaises(AssertionError): + native.require_ready(value, BOUNDS, "harvest-exercise") + + def test_bounds_and_labels_are_bounded_before_any_console_command(self): + for bounds in ((0, 0, 16, 15), (0, 0, 64, 0), (1, 0, 0, 0), + (-1875001, 0, -1875001, 0), (False, 0, 0, 0)): + with self.subTest(bounds=bounds), self.assertRaises(AssertionError): native.chunk_set(bounds) + self.assertEqual(256, len(native.chunk_set((0, 0, 15, 15)))) + + 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) + for forbidden in ("setChunkForced(", "setChanged(", "setBlock(", "getChunk(", + "addFreshEntity(", "getPrivateValue(", "Thread.sleep("): + self.assertNotIn(forbidden, source) + + def test_python_waits_for_result_validates_evidence_then_requires_complete(self): + with tempfile.TemporaryDirectory() as directory: + runtime = Path(directory) + calls = [] + class Server: + def send(self, command): calls.append(command) + def wait(self, marker, timeout): + calls.append((marker, timeout)) + if marker.startswith("ADVREADINESS RESULT"): + target = runtime / "advantage-works-results/native-readiness-harvest-exercise.json" + target.parent.mkdir() + target.write_text(json.dumps(sample())) + native.await_ready(Server(), runtime, BOUNDS, "harvest-exercise") + self.assertEqual("advreadiness chunks 74 63 75 64 harvest-exercise", calls[0]) + self.assertEqual(("ADVREADINESS RESULT harvest-exercise status=", 150), calls[1]) + self.assertEqual(("ADVREADINESS COMPLETE harvest-exercise", 10), calls[2]) + + +if __name__ == "__main__": unittest.main() diff --git a/test-rig/migration/test_native117_recipes.py b/test-rig/migration/test_native117_recipes.py new file mode 100644 index 00000000..bbfceda9 --- /dev/null +++ b/test-rig/migration/test_native117_recipes.py @@ -0,0 +1,249 @@ +"""Native recipe runner safety/oracle tests. No Minecraft or protected input writes.""" + +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import Mock, call, patch +import zipfile + +import native117_recipes as recipes + + +class NativeRecipeTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + + def test_progression_waits_for_reload_and_native_entity_readiness(self): + server = Mock() + bounds = {"min": [16384, 78, 16384], "max": [16415, 88, 16415]} + with patch.object(recipes, "await_ready", side_effect=AssertionError("entities not ready")) as ready: + with self.assertRaisesRegex(AssertionError, "entities not ready"): + recipes.prepare_progression(server, self.root, bounds, "normal-initial-reload", True) + self.assertEqual([ + call.send("reload"), call.wait("Reloading!", 60), call.wait(" advancements", 120), + 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") + + def packaged(self, counts=None, bad_predicate=False, legacy_predicate=False): + jars = [] + for mod, count in (counts or recipes.MOD_COUNTS).items(): + path = self.root / (mod + ".jar") + with zipfile.ZipFile(path, "w") as jar: + for index in range(count): + stem = f"case_{index}" + jar.writestr(f"data/{mod}/recipes/{stem}.json", json.dumps({"type": "minecraft:crafting_shapeless"})) + predicate = {"items": ["minecraft:iron_ingot"]} + if legacy_predicate: predicate = {"item": "minecraft:iron_ingot"} + if bad_predicate: + predicate["count"] = {"min": 1} + jar.writestr(f"data/{mod}/advancements/recipes/{stem}.json", json.dumps({"criteria": { + "has_ingredient": {"trigger": "minecraft:inventory_changed", "conditions": {"items": [predicate]}} + }})) + jar.writestr(f"data/{mod}/recipes/crushing_test.json", json.dumps({"type": "basemetals:crushing"})) + jars.append(path) + return jars + + def test_fixture_uses_packaged_advancements_and_preserves_109_inventory(self): + definition, sources = recipes.recipe_fixture(self.packaged()) + self.assertEqual(109, len(definition["recipes"])) + self.assertEqual(109, len(sources)) + self.assertEqual(27, len(definition["materials"])) + self.assertTrue(all(row["ingredients"] == {"has_ingredient": {"item": "minecraft:iron_ingot"}} + for row in definition["recipes"])) + self.assertTrue(all("crushing" not in identity for identity in sources)) + + def test_fixture_inventory_cannot_silently_shrink(self): + counts = dict(recipes.MOD_COUNTS, basepower=34) + with self.assertRaises(AssertionError): + recipes.recipe_fixture(self.packaged(counts)) + + def test_fixture_rejects_unsupported_predicates(self): + with self.assertRaises(AssertionError): + recipes.recipe_fixture(self.packaged(bad_predicate=True)) + + def test_fixture_rejects_ignored_singular_item_predicates(self): + with self.assertRaisesRegex(AssertionError, "Unsupported inventory predicate"): + recipes.recipe_fixture(self.packaged(legacy_predicate=True)) + + def test_fixture_rejects_duplicate_packaged_recipes(self): + jars = self.packaged() + with self.assertRaises(AssertionError): + recipes.recipe_fixture(jars + jars[:1]) + + def test_unlock_schedule_preserves_modes_and_full_process_persistence(self): + self.assertEqual(5, len(recipes.UNLOCK_BOOTS)) + self.assertEqual(["NORMAL", "NORMAL", "TECH_PROGRESSION", "APOCALYPTIC", "NORMAL"], + [row[0] for row in recipes.UNLOCK_BOOTS]) + self.assertEqual(3, sum("sweep" in row[2] for row in recipes.UNLOCK_BOOTS)) + self.assertEqual(3, sum("persist" in row[2] for row in recipes.UNLOCK_BOOTS)) + self.assertEqual(4, sum("restore" in row[2] for row in recipes.UNLOCK_BOOTS)) + + def unlock_report(self): + definition = {"recipes": [{"id": "basepower:case"}]} + report = {"status": "PASS", "potatoCraftingCases": 59, "bookRecipes": [{"id": "basepower:case"}], + "cases": [{"id": "basepower:case", "recipeActive": True, "status": "PASS"}], + "additionalMaterials": []} + return definition, report + + def test_unlock_requires_every_case_and_all_59_native_potato_checks(self): + definition, report = self.unlock_report() + self.assertEqual(1, recipes.validate_unlock(report, definition, "sweep")["passed"]) + for key, value in (("cases", []), ("potatoCraftingCases", 58), ("bookRecipes", []), ("status", "FAIL")): + changed = dict(report, **{key: value}) + with self.subTest(key=key), self.assertRaises(AssertionError): + recipes.validate_unlock(changed, definition, "sweep") + + def test_unlock_cannot_replace_missing_case_with_duplicate(self): + definition, report = self.unlock_report() + definition["recipes"].append({"id": "basepower:other"}) + report["cases"] *= 2 + with self.assertRaises(AssertionError): + recipes.validate_unlock(report, definition, "sweep") + + def test_human_materials_gaps_remain_failures(self): + definition, report = self.unlock_report() + report["additionalMaterials"] = ["baseelectric:control_circuit"] + with self.assertRaises(AssertionError): + recipes.validate_unlock(report, definition, "persist") + + def test_unavailable_ingredient_is_reported_as_skip_not_pass(self): + definition, report = self.unlock_report() + report["cases"][0]["status"] = "SKIP_NO_INGREDIENT_PROVIDER" + counts = recipes.validate_unlock(report, definition, "sweep") + self.assertEqual((0, 1), (counts["passed"], counts["skipped"])) + + def test_restore_requires_native_forge_login_event(self): + definition, report = self.unlock_report() + with self.assertRaisesRegex(AssertionError, "real native Forge login event"): + recipes.validate_unlock(report, definition, "restore") + report["nativeForgeLoginEvent"] = True + self.assertEqual(1, recipes.validate_unlock(report, definition, "restore")["passed"]) + + def test_progression_rejects_empty_evidence_and_ignored_configuration(self): + report = {"status": "PASS", "mode": "NORMAL", "plasticEqualsRubber": True, "blocked": 0, + "blockedRecipes": [], "targets": [{"id": "basepower:case", "status": "PASS"}], + "reachable": {"minecraft:iron_ingot": {}}, "executedRecipes": ["basepower:case"], + "processWitnesses": [{"route": "live_furnace"}], "lootDataPackOverrideVerified": True} + with patch.object(recipes, "validate_tags"): + recipes.validate_progression(report, "NORMAL", True) + for key, value in (("mode", "APOCALYPTIC"), ("plasticEqualsRubber", False), ("blocked", 1), + ("blockedRecipes", ["missing"]), ("targets", []), ("processWitnesses", []), + ("lootDataPackOverrideVerified", False)): + with self.subTest(key=key), self.assertRaises(AssertionError): + recipes.validate_progression(dict(report, **{key: value}), "NORMAL", True) + + def test_original_natural_basis_and_exclusions_are_not_expanded(self): + from progression import fixture + from native117_identity import fixture_ids + original = fixture() + self.assertEqual(original, recipes.progression_fixture()) + mapped = fixture_ids(original) + self.assertNotIn("baseelectric:psu", mapped["exclusions"]) + self.assertEqual(["baseelectric:li_ore", "baseelectric:sulfur_ore"], + [item for item in mapped["naturalResources"] if item.startswith("baseelectric:")]) + + def test_target_requires_all_five_mods_and_native_harness(self): + manifest = self.root / "manifest.json" + manifest.write_text(json.dumps({"target117": {"jars": [{"id": "basepower"}]}})) + with self.assertRaises(ValueError), patch.object(recipes, "pinned_profile") as resolve: + recipes.native_target(manifest) + resolve.assert_not_called() + + def test_missing_and_historical_packet_exports_cannot_claim_native_level(self): + runtime = self.root / "runtime" + with self.assertRaises(FileNotFoundError): + recipes.verify_level(runtime, {"jars": []}) + old_level = {"Data": {"Version": {"Name": "1.13.2"}}} + with patch.object(recipes.nbtlib, "load", return_value=old_level), self.assertRaises(AssertionError): + recipes.verify_level(runtime, {"jars": []}) + + def test_native_level_requires_nonempty_singular_item_and_block_registries(self): + level = {"Data": {"Version": {"Name": "1.17.1"}}, "fml": { + "LoadingModList": [{"ModId": "forge", "ModVersion": "37.1.1"}], + "Registries": {"minecraft:item": {"ids": [1]}, "minecraft:block": {"ids": [1]}}}} + with patch.object(recipes.nbtlib, "load", return_value=level), patch.object(recipes, "digest", return_value="test-hash"): + self.assertEqual("1.17.1", recipes.verify_level(self.root, {"jars": []})["minecraft"]) + for registry in ({"minecraft:items": {"ids": [1]}, "minecraft:blocks": {"ids": [1]}}, + {"minecraft:item": {"ids": []}, "minecraft:block": {"ids": [1]}}): + level["fml"]["Registries"] = registry + with self.subTest(registry=registry), patch.object(recipes.nbtlib, "load", return_value=level), self.assertRaises(AssertionError): + recipes.verify_level(self.root, {"jars": []}) + + def test_native_level_rejects_wrong_forge_or_mod_version(self): + level = {"Data": {"Version": {"Name": "1.17.1"}}, "fml": { + "LoadingModList": [{"ModId": "forge", "ModVersion": "25.0.223"}], "Registries": {}}} + with patch.object(recipes.nbtlib, "load", return_value=level), self.assertRaises(AssertionError): + recipes.verify_level(self.root, {"jars": []}) + level["fml"]["LoadingModList"][0]["ModVersion"] = "37.1.1" + with patch.object(recipes.nbtlib, "load", return_value=level), self.assertRaises(AssertionError): + recipes.verify_level(self.root, {"jars": [{"id": "basepower", "version": "2.4.6.117011"}]}) + + def test_staging_only_copies_binaries_and_replaces_legacy_generator(self): + seed = self.root / "seed" + seed.mkdir() + (seed / "libraries").mkdir() + (seed / "forge-1.17.1-37.1.1.jar").write_bytes(b"test-launcher") + (seed / WORLD_NAME).mkdir() + protected = seed / WORLD_NAME / "untouched.dat" + protected.write_bytes(b"protected-existing-world") + runtime = self.root / "new-runtime" + target = seed, "forge-1.17.1-37.1.1.jar", [] + before = recipes.fingerprint(seed) + with patch.object(recipes, "require_free_port"): + recipes.stage_native(runtime, target, 25577, "unlock") + self.assertEqual(before, recipes.fingerprint(seed)) + self.assertFalse((runtime / WORLD_NAME).exists()) + self.assertTrue((runtime / "advantage-recipes.allow").exists()) + props = (runtime / "server.properties").read_text() + self.assertIn("server-port=25577\n", props) + self.assertIn("server-ip=127.0.0.1\n", props) + self.assertIn("generator-settings=\n", props) + self.assertNotIn("3;minecraft:bedrock", props) + with patch.object(recipes, "require_free_port"), self.assertRaises(FileExistsError): + recipes.stage_native(runtime, target, 25577, "unlock") + + def test_mode_pruning_requires_exact_native_packet_difference(self): + log = self.root / "console-tech.log" + event = "[Server thread/ERROR] [minecraft/ServerRecipeBook]: Tried to load unrecognized recipe: basepower:normal_only removed now.\n" + log.write_text(event * 2) + result = recipes.audit_unlock_log(log, {"basepower:normal_only", "basepower:common"}, {"basepower:common"}) + self.assertEqual(["basepower:normal_only"], result["observed_removed_ids"]) + self.assertEqual(2, len(result["events"])) + with self.assertRaises(AssertionError): + recipes.audit_unlock_log(log, {"basepower:normal_only"}, {"basepower:normal_only"}) + log.write_text("") + with self.assertRaises(AssertionError): + recipes.audit_unlock_log(log, {"basepower:normal_only"}, set()) + + def test_mode_pruning_never_accepts_unrelated_errors_or_bad_recipe_ids(self): + log = self.root / "console.log" + for message in ("[Server thread/ERROR] [minecraft/ServerRecipeBook]: Other failure", + "[Server thread/ERROR] [minecraft/ServerRecipeBook]: Tried to load improperly formatted recipe: bad removed now.", + "[Server thread/ERROR] [minecraft/LootTableManager]: Failed to reload data packs", + "NoSuchMethodError: native failure", "java.lang.IllegalStateException: native failure", + "[Server thread/FATAL] [minecraft/ServerRecipeBook]: Other failure", + "[Server thread/\x1b[31mERROR\x1b[0m] [minecraft/LootTableManager]: Hidden color error"): + log.write_text(message) + with self.subTest(message=message), self.assertRaises(AssertionError): + recipes.audit_unlock_log(log, set(), set()) + + def test_debug_pruning_requires_the_same_exact_packet_difference(self): + log = self.root / "debug-tech.log" + message = "[31Oct2026 01:00:00] [Server thread/ERROR] [net.minecraft.stats.ServerRecipeBook/]: Tried to load unrecognized recipe: basepower:normal_only removed now." + log.write_text(message) + self.assertEqual(["basepower:normal_only"], + recipes.audit_unlock_log(log, {"basepower:normal_only"}, set())["observed_removed_ids"]) + log.write_text(message.replace("/ERROR]", "/FATAL]")) + with self.assertRaises(AssertionError): + recipes.audit_unlock_log(log, {"basepower:normal_only"}, set()) + + +WORLD_NAME = recipes.WORLD + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_redstone_reload.py b/test-rig/migration/test_native117_redstone_reload.py new file mode 100644 index 00000000..2cc13e31 --- /dev/null +++ b/test-rig/migration/test_native117_redstone_reload.py @@ -0,0 +1,208 @@ +"""Independent negative controls for the cold-reload redstone operating oracle.""" + +import copy +from pathlib import Path +import struct +import unittest +from unittest.mock import Mock, patch + +from native117_redstone_reload import definitions, prepare_seed_terrain, validate_readiness, validate_reload, validate_seed + + +def row(case, progress, held=False, energy=None, targeted=True): + energy = 500 - 24 * progress if energy is None else energy + x, y, z = case["pos"] + bits = struct.unpack(">i", struct.pack(">f", energy))[0] + states = ','.join('{Name:"minecraft:obsidian"}' for _ in range(9)) + items = '{Slot:0b,id:"minecraft:dirt",Count:7b,tag:{AdvWorksWitness:"' + case["id"] + '"}},{Slot:1b,id:"minecraft:apple",Count:13b}' + nbt = ('{id:"baseelectric:electric_drill",x:%d,y:%d,z:%d,Energy:[I;%d],progress:%ds,' + 'Items:[%s],targetX:%d,targetY:%d,targetZ:%d,TargetStates:[%s]}') % ( + x, y, z, bits, progress, items, x, y, z - 1, states) + if not targeted: + nbt = nbt[:nbt.index(',targetX:')] + '}' + return dict(case, block="baseelectric:electric_drill", active=True, + state="Block{baseelectric:electric_drill}[active=true,facing=north]", + signal=held, holdBlock="minecraft:redstone_block" if held else "minecraft:air", + energy=energy, progress=progress, nbt=nbt, + targets=[dict(pos=[x + dx, y + dy, z - 1], state="Block{minecraft:obsidian}") + for dx in range(-1, 2) for dy in range(-1, 2)]) + + +def documents(): + initial = [row(case, 0, targeted=False) for case in definitions()] + for value in initial: + value.update(active=False, state=value["state"].replace("active=true", "active=false")) + trace = [] + for i in range(1, 10): + values = [] + for case in definitions(): + acquired = next(t for t in range(101, 109) if (t + case["offset"]) % 8 == 0) + values.append(row(case, max(0, 100 + i - acquired), targeted=100 + i >= acquired)) + trace.append(dict(index=i, tickStartWorldTime=99 + i, worldTime=100 + i, cases=values)) + before = trace[-1]["cases"] + fixture = dict(schema=1, processToken="seed-process", constructionCallsThisProcess=9, + initialEnergy=500, initial=dict(worldTime=100, cases=initial), seedTrace=trace, + preHold=copy.deepcopy(before), worldTime=109, cases=copy.deepcopy(before)) + for value in fixture["cases"]: + value.update(signal=value["held"], holdBlock="minecraft:redstone_block" if value["held"] else "minecraft:air") + restored = dict(worldTime=200, cases=copy.deepcopy(fixture["cases"])) + ticks = [] + previous = copy.deepcopy(fixture["cases"]) + for i in range(1, 17): + rows = [] + for case in definitions(): + if case["held"]: + old = next(r for r in fixture["cases"] if r["id"] == case["id"]) + value = row(case, 0, True, energy=old["energy"]) + value.update(active=False, state=value["state"].replace("active=true", "active=false")) + else: + old = previous[-1] + powered = old["energy"] > 24 + value = row(case, old["progress"] + (1 if powered else -1), energy=old["energy"] - (24 if powered else 0)) + if value["energy"] <= 24: + value.update(active=False, state=value["state"].replace("active=true", "active=false")) + rows.append(value) + ticks.append(dict(index=i, tickStartWorldTime=199 + i, worldTime=200 + i, cases=rows)) + previous = rows + report = dict(schema=1, processToken="restart-process", constructionCallsThisProcess=0, + observationTicks=16, restored=restored, ticks=ticks, held=True) + fixture["readiness"] = readiness("seed", 100) + report["readiness"] = readiness("restart", 200) + return fixture, report + + +def readiness(phase, release_time): + rows = [dict(id=case["id"], pos=case["pos"], blockTicking=True, tickableListed=True, + blockAndEntitiesLoaded=True, entityTicking=True) for case in definitions()] + pending = copy.deepcopy(rows) + pending[-1].update(blockTicking=False, tickableListed=False, blockAndEntitiesLoaded=False, entityTicking=False) + return [dict(phase=phase, worldTime=release_time - 1, cases=pending), + dict(phase=phase, worldTime=release_time, cases=rows)] + + +class RedstoneReloadOracleTests(unittest.TestCase): + def test_seed_preflight_only_tickets_terrain_for_all_nine_witnesses(self): + server = Mock() + runtime = Path("unused") + with patch("native117_redstone_reload.await_ready", return_value={"status": "PASS"}) as ready: + self.assertEqual(9, len(prepare_seed_terrain(server, runtime))) + self.assertEqual(9, server.send.call_count) + self.assertTrue(all(row.args[0].startswith("forceload add ") for row in server.send.call_args_list)) + self.assertEqual("forceload add 60525 59999 60527 60001", server.send.call_args.args[0]) + ready.assert_called_with(server, runtime, (3782, 3749, 3782, 3750), "redstone-seed-unheld") + + def assert_rejected(self, change): + fixture, report = documents() + change(fixture, report) + with self.assertRaises(AssertionError): + validate_reload(fixture, report) + + def test_all_eight_offsets_and_finite_unheld_control(self): + fixture, report = documents() + self.assertEqual(set(range(8)), {r["offset"] for r in fixture["cases"] if r["held"]}) + self.assertEqual("PASS", validate_seed(fixture)["status"]) + self.assertEqual("PASS", validate_reload(fixture, report)["status"]) + + def test_first_tick_active_transient_is_not_ignored(self): + self.assert_rejected(lambda f, r: r["ticks"][0]["cases"][0].update(active=True)) + + def test_one_tick_debit_fails_even_if_later_refunded(self): + self.assert_rejected(lambda f, r: r["ticks"][0]["cases"][0].update(energy=428)) + + def test_source_cache_bug_with_consistent_raw_nbt_is_rejected(self): + def actual_debit(fixture, report): + seed = fixture["cases"][0] + report["ticks"][0]["cases"][0] = row(definitions()[0], seed["progress"] + 1, + True, energy=seed["energy"] - 24) + self.assert_rejected(actual_debit) + + def test_active_transient_with_consistent_raw_blockstate_is_rejected(self): + def actual_flag(fixture, report): + value = report["ticks"][0]["cases"][0] + value.update(active=True, state=value["state"].replace("active=false", "active=true")) + self.assert_rejected(actual_flag) + + def test_progress_above_previous_not_only_above_seed_is_rejected(self): + self.assert_rejected(lambda f, r: r["ticks"][1]["cases"][0].update(progress=1)) + + def test_unheld_frozen_false_positive_is_rejected(self): + def freeze(fixture, report): + for tick in report["ticks"]: + tick["cases"][-1] = copy.deepcopy(fixture["cases"][-1]) + self.assert_rejected(freeze) + + def test_missing_duplicate_or_wrong_phase_witness_is_rejected(self): + self.assert_rejected(lambda f, r: r["ticks"][0]["cases"].pop()) + self.assert_rejected(lambda f, r: r["ticks"][0]["cases"].append(r["ticks"][0]["cases"][0])) + self.assert_rejected(lambda f, r: r["ticks"][0]["cases"][0].update(offset=7)) + + def test_missing_repeated_or_gapped_tick_is_rejected(self): + self.assert_rejected(lambda f, r: r["ticks"].pop()) + self.assert_rejected(lambda f, r: r["ticks"][1].update(worldTime=201)) + self.assert_rejected(lambda f, r: r["ticks"][1].update(tickStartWorldTime=202, worldTime=203)) + + def test_reconstruction_same_process_and_nonquiescence_are_rejected(self): + self.assert_rejected(lambda f, r: r.update(constructionCallsThisProcess=1)) + self.assert_rejected(lambda f, r: r.update(processToken=f["processToken"])) + self.assert_rejected(lambda f, r: r.update(held=False)) + + def test_restored_nbt_must_match_entire_seed_not_only_resources(self): + self.assert_rejected(lambda f, r: r["restored"]["cases"][0].update(nbt=f["cases"][0]["nbt"][:-1] + ',Hidden:1}')) + self.assert_rejected(lambda f, r: r["restored"]["cases"][0].update(nbt=f["cases"][0]["nbt"].replace('Count:13b', 'Count:12b'))) + + def test_real_signal_target_and_stationary_position_are_checked(self): + self.assert_rejected(lambda f, r: r["ticks"][0]["cases"][0].update(signal=False)) + self.assert_rejected(lambda f, r: r["ticks"][0]["cases"][0]["targets"][0].update(state="Block{minecraft:air}")) + self.assert_rejected(lambda f, r: r["ticks"][0]["cases"][0].update(pos=[0, 80, 0])) + + def test_seed_needs_natural_mid_target_work_and_original_finite_charge(self): + fixture, _ = documents() + for change in (lambda f: f.update(seedTrace=[]), lambda f: f.update(initialEnergy=501), + lambda f: f["cases"][0].update(progress=0), + lambda f: f["seedTrace"][-1]["cases"][0].update(energy=500)): + bad = copy.deepcopy(fixture) + change(bad) + with self.assertRaises(AssertionError): validate_seed(bad) + + def test_both_native_ticking_and_actual_tickable_registration_are_required(self): + for field in ("blockTicking", "tickableListed", "blockAndEntitiesLoaded", "entityTicking"): + def change(fixture, report): + report["readiness"][-1]["cases"][-1][field] = False + self.assert_rejected(change) + self.assert_rejected(lambda f, r: r.update(readiness=[])) + self.assert_rejected(lambda f, r: f.update(readiness=[])) + + def test_readiness_is_bounded_and_not_resampled_until_operating_verdict_passes(self): + data = dict(readiness=readiness("seed", 100)) + validate_readiness(data, "seed", 100) + for change in (lambda d: d["readiness"].append(copy.deepcopy(d["readiness"][-1])), + lambda d: d["readiness"][0]["cases"][-1].update(blockTicking=True, tickableListed=True, + blockAndEntitiesLoaded=True, entityTicking=True), + lambda d: d["readiness"][-1].update(worldTime=101), + lambda d: d.update(readiness=d["readiness"] * 41)): + bad = copy.deepcopy(data) + change(bad) + with self.assertRaises(AssertionError): validate_readiness(bad, "seed", 100) + + def test_probe_never_drives_ticks_or_loads_expected_nbt(self): + source = (Path(__file__).parents[1] / "src/main/java/zone/moddev/mc/advantageworks/RedstoneReloadProbe.java").read_text() + for forbidden in (".tickUpdate(", ".powerUpdate(", ".tick();", ".load("): + self.assertNotIn(forbidden, source) + self.assertIn("NativeBlockEntityTicks.drain(current)", source) + self.assertIn("NativeBlockEntityTicks.restore(current, suspended)", source) + self.assertIn("isPositionTicking(new net.minecraft.world.level.ChunkPos(witness.pos).toLong())", source) + self.assertIn("NativeBlockEntityTicks.contains(world, tile)", source) + tickers = (Path(__file__).parents[1] / "src/main/java/zone/moddev/mc/advantageworks/NativeBlockEntityTicks.java").read_text() + self.assertIn("active(world).clear();", tickers) + self.assertIn("pending(world).clear();", tickers) + self.assertIn("world.addBlockEntityTicker(entry);", tickers) + for forbidden in (".tick(", ".load(", ".save("): + self.assertNotIn(forbidden, tickers) + self.assertIn('else if (ready("seed"))', source) + self.assertIn('else if (ready("restart"))', source) + self.assertIn("StandardOpenOption.CREATE_NEW", source) + mod = (Path(__file__).parents[1] / "src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java").read_text() + self.assertEqual(1, mod.count("RedstoneReloadProbe.register();")) + + +if __name__ == "__main__": unittest.main() diff --git a/test-rig/migration/test_native117_refresh.py b/test-rig/migration/test_native117_refresh.py new file mode 100644 index 00000000..d641989e --- /dev/null +++ b/test-rig/migration/test_native117_refresh.py @@ -0,0 +1,25 @@ +from pathlib import Path +import tempfile +import unittest + +from matrix import digest +from native117_refresh import snapshot_name + + +class RefreshProvenanceTests(unittest.TestCase): + def test_only_exact_archived_snapshot_is_selected(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + first = root / "migration-source.json" + second = root / "migration-restarted.json" + first.write_text("source") + second.write_text("different") + self.assertEqual(snapshot_name(root, digest(first)), first.name) + with self.assertRaisesRegex(AssertionError, "No retained"): + snapshot_name(root, "0" * 64) + second.write_text("source") + self.assertEqual(snapshot_name(root, digest(first)), second.name) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_resource_audit.py b/test-rig/migration/test_native117_resource_audit.py new file mode 100644 index 00000000..d609d170 --- /dev/null +++ b/test-rig/migration/test_native117_resource_audit.py @@ -0,0 +1,305 @@ +"""Mutation controls use the retained migration/connected row and NBT schemas.""" + +import base64 +import copy +import gzip +import hashlib +import io +import json +from pathlib import Path +import struct +import tempfile +import unittest + +import nbtlib + +from native117_resource_audit import BATTERIES, SLOT_LIMITS, canonical_resources, compare_resources, parse_legacy_snbt + + +def encode(row, snbt): + stream = io.BytesIO() + nbtlib.File(nbtlib.parse_nbt(snbt)).write(stream) + row["nbt"] = snbt + row["nbtBase64"] = base64.b64encode(gzip.compress(stream.getvalue())).decode("ascii") + return row + + +def witness(block="minecraft:chest", snbt="{Items:[]}", x=143): + return encode(dict(id="fixture-" + str(x), station="C-ELECTRIC", kind="battery", + name=block, actualName=block, x=x, y=80, z=47, + properties={}, witness="fixture-" + str(x), objectIdentity=123), snbt) + + +def bits(amount): + return struct.unpack(">i", struct.pack(">f", amount))[0] + + +class LegacySnbtGrammarTests(unittest.TestCase): + def test_nested_legacy_lists_preserve_quoted_text(self): + value = parse_legacy_snbt('{Items:[0:{id:"minecraft:dirt",Count:2b,tag:{Name:"[0:not an index]",Lore:[0:"first",1:"second"]}}]}') + self.assertEqual(2, int(value["Items"][0]["Count"])) + self.assertEqual("[0:not an index]", str(value["Items"][0]["tag"]["Name"])) + self.assertEqual(["first", "second"], value["Items"][0]["tag"]["Lore"].unpack()) + + def test_modern_number_lists_still_parse(self): + self.assertEqual([0, 1, 2], parse_legacy_snbt('[0,1,2]').unpack()) + + def test_legacy_int_array_trailing_comma_is_preserved(self): + self.assertEqual([1168297984], parse_legacy_snbt('{Energy:[1168297984,]}')["Energy"].unpack()) + + def test_mixed_skipped_duplicate_and_trailing_indices_fail(self): + for text in ('[0:1,2]', '[1:1]', '[0:1,0:2]', '[0:1] extra'): + with self.assertRaises(ValueError): parse_legacy_snbt(text) + + +def sample(): + return [ + witness("baseelectric:electric_battery_array", + '{Energy:[I;1174011904],Items:[{Slot:0b,id:"baseelectric:lithium_battery",Count:1b,tag:{energy:8000.0f}}]}'), + witness("basesteam:steam_boiler_coal", + '{Energy:[I;1088421888,0],BurnTime:802,BurnTimeTotal:1600,Items:[{Slot:0b,id:"minecraft:coal",Count:31b}],Tank:{FluidName:"minecraft:water",Amount:3992}}', 65), + witness("baseelectric:steam_powered_generator", + '{Energy:[I;1147797504,0],Items:[],steam:29.452f}', 141), + ] + + +class ResourceAuditTest(unittest.TestCase): + def check(self, before, after, status="FAIL", **kwargs): + result = compare_resources(before, after, **kwargs) + self.assertEqual(status, result["status"], result) + return result + + def test_actual_row_schemas_accept_arrays_and_connected_wrappers(self): + before = sample() + wrapped = dict(held=True, requestedTicks=0, advancedTicks=0, + gameTime=807, buildCallsThisProcess=0, snapshot=copy.deepcopy(before)) + self.check(before, wrapped, "PASS") + self.assertEqual(3, compare_resources(wrapped, before)["positions"]) + + def test_read_only_path_api_does_not_change_inputs(self): + with tempfile.TemporaryDirectory() as directory: + a, b = Path(directory) / "migration-converted.json", Path(directory) / "connected-loaded.json" + a.write_text(json.dumps(sample()), encoding="utf-8") + b.write_text(json.dumps(dict(snapshot=sample(), held=True)), encoding="utf-8") + hashes = [hashlib.sha256(path.read_bytes()).hexdigest() for path in (a, b)] + self.check(a, b, "PASS") + self.assertEqual(hashes, [hashlib.sha256(path.read_bytes()).hexdigest() for path in (a, b)]) + + def test_inputs_and_override_maps_are_not_mutated(self): + before = sample() + after = copy.deepcopy(before) + backup = copy.deepcopy(before) + limits = {"baseelectric:electric_battery_array": 8} + self.check(before, after, "PASS", slot_limits=limits) + self.assertEqual(before, backup) + self.assertEqual(after, backup) + self.assertEqual(limits, {"baseelectric:electric_battery_array": 8}) + + def test_root_nonresource_native_metadata_is_not_blanket_nbt_equality(self): + before = [witness(snbt='{Items:[],Lock:""}')] + after = [witness(snbt='{Items:[],CustomName:"native",DataVersion:2730,ForgeData:{AdvConnectedWitness:"same",cache:42},initDone:1b,Facing:2b,progress:7s,BurnTimeTotal:1600}')] + after[0].update(objectIdentity=999, properties={"facing": "north"}) + self.check(before, after, "PASS") + + def test_target_only_energy_and_extra_positive_channel_fail(self): + before = [witness("baseelectric:electric_furnace", "{Items:[]}")] + self.check(before, [witness("baseelectric:electric_furnace", "{Items:[],Energy:[I;%d]}" % bits(100))]) + self.check([witness(snbt="{Energy:[I;%d,0]}" % bits(1))], + [witness(snbt="{Energy:[I;%d,%d]}" % (bits(1), bits(2)))]) + + def test_optional_zero_energy_arrays_and_scalars_are_inert(self): + self.check([witness(snbt="{}")], + [witness(snbt="{Items:[],Energy:[I;0,0,0],Electricity:0.0f,BurnTime:0,steam:0.0f}")], "PASS") + self.check([witness(snbt="{Energy:[I;%d]}" % bits(1))], + [witness(snbt="{Energy:[I;%d,0,0]}" % bits(1))], "PASS") + + def test_each_known_scalar_reserve_detects_target_only_additions(self): + for field in ("Electricity", "energy", "steam", "soil", "Temperature", "BurnTime", "burnTime", "Fuel", "fuel"): + with self.subTest(field=field): + self.check([witness(snbt="{}")], [witness(snbt="{%s:1}" % field)]) + + def test_target_only_positive_named_tanks_fail(self): + for field in ("Tank", "TankIn", "TankOut", "InputTank", "OutputTank", "WaterTank", "FuelTank", "AuxTank"): + with self.subTest(field=field): + self.check([witness(snbt="{}")], + [witness(snbt='{%s:{FluidName:"minecraft:water",Amount:1}}' % field)]) + + def test_exact_zero_named_fluid_and_native_empty_representations_match(self): + for field in ("Tank", "TankIn", "TankOut", "InputTank"): + with self.subTest(field=field): + self.check([witness(snbt='{%s:{FluidName:"basepower:crude_oil",Amount:0}}' % field)], + [witness(snbt='{%s:{Empty:1b}}' % field)], "PASS") + self.check([witness(snbt="{}")], [witness(snbt='{%s:{Empty:0b}}' % field)], "PASS") + + def test_positive_fluid_cannot_be_normalized_to_empty(self): + self.check([witness(snbt='{Tank:{FluidName:"water",Amount:1}}')], + [witness(snbt='{Tank:{Empty:1b}}')]) + + def test_fluid_identity_amount_and_payload_are_exact(self): + before = [witness(snbt='{Tank:{FluidName:"water",Amount:25,Tag:{grade:1}}}')] + for tank in ('{FluidName:"lava",Amount:25,Tag:{grade:1}}', + '{FluidName:"water",Amount:24,Tag:{grade:1}}', + '{FluidName:"water",Amount:25,Tag:{grade:2}}'): + with self.subTest(tank=tank): self.check(before, [witness(snbt="{Tank:" + tank + "}")]) + + def test_known_fluid_namespace_normalizations_are_retained(self): + for old, new in (("water", "minecraft:water"), ("crude_oil", "basepower:crude_oil")): + self.check([witness(snbt='{Tank:{FluidName:"%s",Amount:25}}' % old)], + [witness(snbt='{Tank:{FluidName:"%s",Amount:25}}' % new)], "PASS", legacy_source=True) + + def test_target_only_inventory_and_quantity_or_slot_mutations_fail(self): + empty = [witness(snbt="{}")] + one = [witness(snbt='{Items:[{Slot:0b,id:"minecraft:coal",Count:1b}]}')] + self.check(empty, one) + for slot, item, count in ((0, "minecraft:coal", 2), (1, "minecraft:coal", 1), (0, "minecraft:diamond", 1)): + self.check(one, [witness(snbt='{Items:[{Slot:%db,id:"%s",Count:%db}]}' % (slot, item, count))]) + + def test_inventory_and_witness_order_do_not_change_per_position_budgets(self): + a = witness(snbt='{Items:[{Slot:0b,id:"minecraft:coal",Count:1b},{Slot:3b,id:"minecraft:stone",Count:2b}]}') + b = witness(snbt='{Items:[{Slot:3b,id:"minecraft:stone",Count:2b},{Slot:0b,id:"minecraft:coal",Count:1b}]}') + other = witness(x=144) + self.check([a, other], [other, b], "PASS") + + def test_equal_global_total_cannot_hide_energy_moved_between_positions(self): + before = [witness(snbt="{Energy:[I;%d]}" % bits(5), x=1), witness(snbt="{Energy:[I;%d]}" % bits(7), x=2)] + after = [witness(snbt="{Energy:[I;%d]}" % bits(7), x=1), witness(snbt="{Energy:[I;%d]}" % bits(5), x=2)] + result = self.check(before, after) + self.assertEqual(2, len(result["differences"])) + + def test_missing_new_duplicate_or_moved_witnesses_fail(self): + before = [witness(), witness(x=144)] + for after in (before[:1], before + [witness(x=145)], before + [copy.deepcopy(before[0])], [witness(), witness(x=145)]): + self.check(before, after) + + def test_changed_block_identity_cannot_hide_behind_empty_resources(self): + self.check([witness()], [witness("minecraft:hopper")]) + + def test_missing_known_container_nbt_is_not_zero_stock_evidence(self): + row = witness() + row.pop("nbt"); row.pop("nbtBase64") + self.check([row], [row]) + + def test_duplicate_negative_and_out_of_range_slots_fail(self): + for items in ('[{Slot:0b,id:"minecraft:coal",Count:1b},{Slot:0b,id:"minecraft:coal",Count:1b}]', + '[{Slot:-1b,id:"minecraft:coal",Count:1b}]', + '[{Slot:27b,id:"minecraft:coal",Count:1b}]', + '[{id:"minecraft:coal",Count:1b}]'): + row = witness(snbt="{Items:" + items + "}") + self.check([row], [row]) + + def test_constructor_proven_furnace_and_other_slot_limits(self): + for block, limit in (("baseelectric:electric_furnace", 10), + ("baseelectric:electric_crusher", 9), + ("baseelectric:electric_drill", 6), + ("basesteam:steam_furnace", 7), + ("basesteam:steam_drill", 5)): + self.assertEqual(limit, SLOT_LIMITS[block]) + valid = [witness(block, '{Items:[{Slot:%db,id:"minecraft:coal",Count:1b}]}' % (limit - 1))] + invalid = [witness(block, '{Items:[{Slot:%db,id:"minecraft:coal",Count:1b}]}' % limit)] + self.check(valid, valid, "PASS") + self.check(invalid, invalid) + + def test_caller_can_add_unknown_registry_container_limit(self): + rows = [witness("example:container", '{Items:[{Slot:2b,id:"minecraft:coal",Count:1b}]}')] + self.check(rows, rows, "PASS") + self.check(rows, rows, slot_limits={"example:container": 2}) + + def test_invalid_stack_quantities_fail_even_when_both_sides_match(self): + for count in (-1, 0, 65): + rows = [witness(snbt='{Items:[{Slot:0b,id:"minecraft:coal",Count:%d}]}' % count)] + self.check(rows, rows) + + def test_known_legacy_damage_bucket_and_empty_tag_representations_match(self): + old = [witness(snbt='{id:"Chest",Items:[{Slot:0b,id:"forge:bucketfilled",Count:1b,Damage:0s,tag:{FluidName:"crude_oil",Amount:1000}}]}')] + new = [witness(snbt='{id:"minecraft:chest",Items:[{Slot:0b,id:"basepower:crude_oil_bucket",Count:1b,tag:{}}]}')] + self.check(old, new, "PASS", legacy_source=True) + self.check(old, old, "FAIL", legacy_source=True) + damaged = [witness(snbt='{Items:[{Slot:0b,id:"basesteam:musket",Count:1b,Damage:5s}]}')] + modern = [witness(snbt='{Items:[{Slot:0b,id:"basesteam:musket",Count:1b,tag:{Damage:5}}]}')] + self.check(damaged, modern, "PASS") + + def test_migration_does_not_rewrite_player_supplied_item_names(self): + def named_musket(identity, name): + return [witness(snbt='{Items:[{Slot:0b,id:"%s",Count:1b,tag:{display:{Name:"%s"}}}]}' + % (identity, name))] + old = named_musket("steamadvantage:musket", "steamadvantage:musket") + self.check(old, named_musket("basesteam:musket", "steamadvantage:musket"), "PASS", legacy_source=True) + self.check(old, named_musket("basesteam:musket", "basesteam:musket"), "FAIL", legacy_source=True) + + def test_enchantments_use_explicit_world_registry_maps(self): + old = [witness(snbt='{Items:[{Slot:0b,id:"basesteam:musket",Count:1b,tag:{ench:[{id:11s,lvl:2s}]}}]}')] + new = [witness(snbt='{Items:[{Slot:0b,id:"basesteam:musket",Count:1b,tag:{Enchantments:[{id:"basesteam:spreadshot",lvl:2s}]}}]}')] + self.check(old, new, "PASS", before_enchantments={11: "basesteam:spreadshot"}) + self.check(old, new, before_enchantments={11: "minecraft:binding_curse"}) + + def test_battery_defaults_match_native_full_charge_but_charge_creation_fails(self): + for item, capacity in BATTERIES.items(): + old = [witness(snbt='{Items:[{Slot:0b,id:"%s",Count:1b}]}' % item)] + full = [witness(snbt='{Items:[{Slot:0b,id:"%s",Count:1b,tag:{energy:%df}}]}' % (item, capacity))] + self.check(old, full, "PASS") + zero = [witness(snbt='{Items:[{Slot:0b,id:"%s",Count:1b,tag:{energy:0.0f}}]}' % item)] + self.check(zero, full) + invalid = [witness(snbt='{Items:[{Slot:0b,id:"%s",Count:1b,tag:{energy:%df}}]}' % (item, capacity + 1))] + self.check(invalid, invalid) + + def test_legacy_battery_stack_counts_are_preserved_not_repaired(self): + original = [witness(snbt='{Items:[{Slot:0b,id:"baseelectric:lithium_battery",Count:3b,tag:{energy:123.0f}}]}')] + self.check(original, copy.deepcopy(original), "PASS") + for count, charge in ((1,123), (4,123), (3,124)): + changed = [witness(snbt='{Items:[{Slot:0b,id:"baseelectric:lithium_battery",Count:%db,tag:{energy:%df}}]}' % (count, charge))] + self.check(original, changed) + + def test_lossy_legacy_byte_summaries_require_matching_binary_and_keep_real_values(self): + row = witness(snbt='{ForgeCaps:{custom:{Items:[],Fuel:2}},growth:[B;4b,-2b,7b]}') + row["nbt"] = '{ForgeCaps:{custom:{Items:[],Fuel:2}},growth:[3 bytes]}' + self.check([row], [copy.deepcopy(row)], "PASS") + for text in ('{ForgeCaps:{custom:{Items:[],Fuel:2}},growth:[2 bytes]}', + '{ForgeCaps:{custom:{Items:[],Fuel:3}},growth:[3 bytes]}'): + bad = copy.deepcopy(row); bad["nbt"] = text + self.check([bad], [bad]) + missing = copy.deepcopy(row); missing.pop("nbtBase64") + self.check([missing], [missing]) + + def test_negative_and_nonfinite_resource_encodings_fail_closed(self): + for snbt in ("{Energy:[I;%d]}" % bits(-1), "{Energy:[I;%d]}" % bits(float("nan")), + "{Energy:[I;%d]}" % bits(float("inf")), "{steam:-1.0f}", + '{Tank:{FluidName:"water",Amount:-1}}', '{Tank:{}}', + '{Tank:{Empty:1b,FluidName:"water",Amount:1}}'): + rows = [witness(snbt=snbt)] + self.check(rows, rows) + + def test_resource_fields_inside_capability_compounds_are_not_ignored(self): + self.check([witness(snbt="{}")], [witness(snbt="{ForgeCaps:{example:{Energy:[I;%d]}}}" % bits(5))]) + + def test_conflicting_base64_and_text_payloads_are_not_silently_selected(self): + row = witness(snbt="{Energy:[I;%d]}" % bits(5)) + row["nbt"] = "{Energy:[I;0]}" + result = self.check([row], [row]) + self.assertTrue(result["failures"]) + + def test_snbt_float_rounding_uses_declared_binary_type_not_tolerance(self): + row = witness(snbt="{steam:29.452f}") + self.check([row], [row], "PASS") + text_only = copy.deepcopy(row) + text_only.pop("nbtBase64") + self.check([row], [text_only], "PASS") + changed = witness(snbt="{steam:29.453f}") + self.check([row], [changed]) + double = witness(snbt="{steam:29.452d}") + self.check([row], [double]) + + def test_empty_or_unsupported_documents_fail_instead_of_vacuous_pass(self): + for document in ([], {}, {"snapshot": []}, {"cases": []}): + self.check(document, document) + + def test_snbt_only_operation_position_rows_can_be_compared_after_explicit_extraction(self): + row = witness(snbt='{TankIn:{FluidName:"crude_oil",Amount:0},TankOut:{FluidName:"refined_oil",Amount:20}}') + row["block"] = row.pop("actualName") + row.pop("nbtBase64") + other = copy.deepcopy(row) + other["nbt"] = '{TankIn:{Empty:1b},TankOut:{FluidName:"refined_oil",Amount:20}}' + self.check([row], [other], "PASS") + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_respawn.py b/test-rig/migration/test_native117_respawn.py new file mode 100644 index 00000000..e2f9808e --- /dev/null +++ b/test-rig/migration/test_native117_respawn.py @@ -0,0 +1,87 @@ +import copy +import unittest +from unittest.mock import patch +import json +from pathlib import Path +import tempfile +import os + +from native117_respawn import native_respawn_verdict, publish_request +from native117_audit_respawn import replay + + +class NativeRespawnAdapterTest(unittest.TestCase): + def test_tick_reader_never_observes_definition_truncation(self): + with tempfile.TemporaryDirectory() as temp: + path = Path(temp) / "dimension-fixture.json" + old = {"dimension": -1, "respawn": {"bedMode": "forced", "keepInventory": True}} + path.write_text(json.dumps(old)) + if os.name == "nt": + import ctypes + from ctypes import wintypes + kernel = ctypes.WinDLL("kernel32", use_last_error=True) + kernel.CreateFileW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, + wintypes.LPVOID, wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE] + kernel.CreateFileW.restype = wintypes.HANDLE + kernel.CloseHandle.argtypes = [wintypes.HANDLE] + handle = kernel.CreateFileW(str(path), 0x80000000, 1, None, 3, 128, None) + if handle == wintypes.HANDLE(-1).value: + raise ctypes.WinError(ctypes.get_last_error()) + try: + binding = publish_request(Path(temp), "respawn-keep-bed", True, "bed") + finally: + kernel.CloseHandle(handle) + else: + with path.open("rb"): + binding = publish_request(Path(temp), "respawn-keep-bed", True, "bed") + self.assertEqual(old, json.loads(path.read_text())) + self.assertTrue((Path(temp) / binding["path"]).is_file()) + + def test_failed_request_write_keeps_the_root_fixture_unchanged(self): + with tempfile.TemporaryDirectory() as temp: + path = Path(temp) / "dimension-fixture.json" + old = {"dimension": 1, "respawn": {"bedMode": "missing", "keepInventory": False}} + path.write_text(json.dumps(old)) + original = Path.open + def failed_request(target, *args, **kwargs): + if target.parent.name == "respawn-requests" and args and args[0] == "xb": + raise OSError("disk write failed") + return original(target, *args, **kwargs) + with patch.object(Path, "open", failed_request): + with self.assertRaisesRegex(OSError, "disk write failed"): + publish_request(Path(temp), "respawn-drop-missing", False, "missing") + self.assertEqual(old, json.loads(path.read_text())) + self.assertEqual([], list((Path(temp) / "respawn-requests").iterdir())) + + def test_only_exact_native_handler_is_translated(self): + for handler, expected in (("ServerPlayNetHandler.PERFORM_RESPAWN", "NetHandlerPlayServer.PERFORM_RESPAWN"), + ("Other.PERFORM_RESPAWN", "Other.PERFORM_RESPAWN"), + ("NetHandlerPlayServer.PERFORM_RESPAWN", "NetHandlerPlayServer.PERFORM_RESPAWN")): + original = {"respawn": {"handler": handler}, "retain": {"Inventory": [17], "bed": "missing"}} + before = copy.deepcopy(original) + with patch("native117_respawn.respawn_verdict", return_value={"status": "TEST"}) as oracle: + self.assertEqual({"status": "TEST"}, native_respawn_verdict(original, {1: "enchantment"}, {"radius": 10})) + adapted = copy.deepcopy(before) + adapted["respawn"]["handler"] = expected + oracle.assert_called_once_with(adapted, {1: "enchantment"}, {"radius": 10}) + self.assertEqual(before, original) + + def test_aborted_campaign_never_becomes_a_pass(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + for name in ("results.json", "source-fingerprints.json"): + (root / name).write_text(json.dumps({})) + manifest = root / "archived-inputs.json" + manifest.write_text("{}") + with self.assertRaisesRegex(ValueError, "explicit pinned target manifest"): + replay(root) + with patch("native117_audit_respawn.pinned_profile") as pin: + result = replay(root, manifest) + pin.assert_called_once_with(manifest.resolve(), "target117") + self.assertEqual("FAIL", result["status"]) + self.assertIn("Campaign summary missing", result["findings"][0]) + self.assertFalse((root / "summary.json").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_respawn_requests.py b/test-rig/migration/test_native117_respawn_requests.py new file mode 100644 index 00000000..e4a1b027 --- /dev/null +++ b/test-rig/migration/test_native117_respawn_requests.py @@ -0,0 +1,215 @@ +import copy +import hashlib +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import native117_respawn as requests +import native117_audit_respawn as audit + + +class NativeRespawnRequestTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.runtime = Path(self.temp.name) + self.definition = {'dimension': 1, 'playerName': 'Witness', + 'playerUuid': 'e036830e-303d-3ac3-bd23-062b72eed895', + 'geometry': {'x': 176}, 'respawn': {'retained': 'base-only'}} + self.base = self.runtime / 'dimension-fixture.json' + self.base.write_text(json.dumps(self.definition) + '\n', encoding='utf-8') + self.original = self.base.read_bytes() + + def publish(self, label='respawn-keep-bed', keep=True, mode='bed'): + return requests.publish_request(self.runtime, label, keep, mode) + + def record(self, binding): + definition = copy.deepcopy(self.definition) + definition['respawn'].update(keepInventory=binding['keepInventory'], bedMode=binding['bedMode']) + return {'definition': definition, 'operation': 'respawn', 'respawnRequest': binding, + 'respawn': {'keepInventory': binding['keepInventory'], 'bedMode': binding['bedMode']}} + + def test_request_is_closed_and_hash_bound_without_touching_root(self): + binding = self.publish() + path = self.runtime / binding['path'] + payload = json.loads(path.read_bytes()) + self.assertEqual({'protocol', 'id', 'baseFixtureSha256', 'keepInventory', 'bedMode'}, set(payload)) + self.assertEqual('native117-respawn-request-v1', payload['protocol']) + self.assertEqual('respawn-keep-bed', payload['id']) + self.assertIs(True, payload['keepInventory']) + self.assertEqual('bed', payload['bedMode']) + self.assertEqual(hashlib.sha256(self.original).hexdigest().upper(), payload['baseFixtureSha256']) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest().upper(), binding['sha256']) + self.assertEqual(self.original, self.base.read_bytes()) + self.assertEqual(binding, requests.verify_request(self.runtime, payload['id'], self.record(binding), binding)) + + def test_publishing_does_not_replace_any_existing_file(self): + with self.base.open('rb'), patch.object(Path, 'replace', side_effect=AssertionError('No file replacement')): + self.publish() + self.assertEqual(self.original, self.base.read_bytes()) + + def test_duplicate_case_is_refused_even_when_bytes_would_match(self): + first = self.publish() + raw = (self.runtime / first['path']).read_bytes() + with self.assertRaises(FileExistsError): + self.publish() + self.assertEqual(raw, (self.runtime / first['path']).read_bytes()) + self.assertEqual(self.original, self.base.read_bytes()) + + def test_labels_options_and_boolean_types_must_agree(self): + for label, keep, mode in (('../escape', True, 'bed'), ('respawn-keep-bed.json', True, 'bed'), + ('respawn-drop-bed', True, 'bed'), ('respawn-keep-forced', True, 'bed'), + ('respawn-keep-bed', 1, 'bed'), ('respawn-keep-bed', True, 'other')): + with self.subTest(label=label, keep=keep, mode=mode), self.assertRaises((ValueError, AssertionError)): + self.publish(label, keep, mode) + self.assertEqual(self.original, self.base.read_bytes()) + + def test_changed_base_fixture_invalidates_published_request(self): + binding = self.publish() + self.base.write_bytes(self.original + b' ') + with self.assertRaises((ValueError, AssertionError)): + requests.verify_request(self.runtime, binding['id'], self.record(binding), binding) + + def test_changed_request_bytes_are_not_accepted_by_native_ack(self): + binding = self.publish() + path = self.runtime / binding['path'] + path.write_bytes(path.read_bytes() + b' ') + with self.assertRaises((ValueError, AssertionError)): + requests.verify_request(self.runtime, binding['id'], self.record(binding), binding) + + def test_unknown_and_duplicate_request_keys_are_rejected(self): + binding = self.publish() + path = self.runtime / binding['path'] + original = path.read_bytes() + payload = json.loads(original) + payload['dimension'] = -1 + for raw in (json.dumps(payload).encode(), original.rstrip()[:-1] + b', "keepInventory": false}'): + with self.subTest(raw=raw): + path.write_bytes(raw) + with self.assertRaises((ValueError, AssertionError)): + requests.verify_request(self.runtime, binding['id'], self.record(binding), binding) + + def test_native_ack_cannot_claim_another_request_or_option(self): + binding = self.publish() + for key, value in (('sha256', '0' * 64), ('id', 'respawn-drop-bed'), ('path', '../outside.json'), + ('protocol', 'other-v1'), ('baseFixtureSha256', 'F' * 64), + ('keepInventory', 1), ('bedMode', 'forced')): + record = self.record(copy.deepcopy(binding)) + record['respawnRequest'][key] = value + with self.subTest(key=key), self.assertRaises((ValueError, AssertionError)): + requests.verify_request(self.runtime, binding['id'], record, binding) + + def test_native_definition_only_merges_the_two_validated_options(self): + binding = self.publish() + for mutate in (lambda value: value['definition'].update(dimension=-1), + lambda value: value['definition'].update(playerUuid='other'), + lambda value: value['definition']['respawn'].update(retained='changed'), + lambda value: value['respawn'].update(keepInventory=False), + lambda value: value['respawn'].update(bedMode='missing')): + record = self.record(binding) + mutate(record) + with self.assertRaises((ValueError, AssertionError)): + requests.verify_request(self.runtime, binding['id'], record, binding) + + def test_request_directory_requires_exact_regular_case_files(self): + binding = self.publish() + self.assertEqual({binding['id']: binding}, requests.verify_request_files(self.runtime, [binding['id']])) + extra = self.runtime / 'respawn-requests/orphan.json' + extra.write_text('{}') + with self.assertRaises((ValueError, AssertionError)): + requests.verify_request_files(self.runtime, [binding['id']]) + extra.unlink() + with self.assertRaises((ValueError, AssertionError)): + requests.verify_request_files(self.runtime, [binding['id'], 'respawn-drop-forced']) + + def test_runner_publishes_before_command_and_never_rewrites_root(self): + source = Path(requests.__file__).read_text() + self.assertNotIn('publish_definition(', source) + self.assertNotIn('definition["respawn"] =', source) + self.assertLess(source.index('binding = publish_request('), source.index('server.send("advdimensions respawn ')) + + def test_independent_auditor_checks_request_inventory_not_only_status(self): + self.assertTrue(callable(getattr(audit, 'request_integrity', None))) + source = Path(audit.__file__).read_text() + self.assertIn('request_integrity(root, sources, results)', source) + self.assertIn('requestProtocol', source) + + def campaign(self): + root = self.runtime / 'campaign' + root.mkdir() + source = self.runtime / 'protected-source' + source.mkdir() + (source / 'dimension-fixture.json').write_bytes(self.original) + runtime = root / 'original110' + runtime.mkdir() + (runtime / 'dimension-fixture.json').write_bytes(self.original) + index = {'protocol': requests.REQUEST_PROTOCOL, 'cases': {}} + results = {} + for label in requests.CASE_LABELS: + keep, mode = requests.case_options(label) + binding = requests.publish_request(runtime, label, keep, mode) + record = self.record(binding) + path = runtime / ('dimensions-' + label + '.json') + path.write_text(json.dumps(record)) + case = 'original110/' + label + index['cases'][case] = {'request': binding, 'recordSha256': requests.digest(path)} + results[case] = {'status': 'PASS'} + evidence = runtime / 'respawn-evidence' / label + evidence.mkdir(parents=True) + (evidence / 'request.json').write_bytes((runtime / binding['path']).read_bytes()) + (root / requests.REQUEST_INDEX).write_text(json.dumps(index)) + sources = {'original110': {'path': str(source), 'baseFixtureSha256': requests.digest(source / 'dimension-fixture.json')}} + return root, runtime, sources, results, index + + def test_independent_inventory_verifies_all_twelve_case_bindings(self): + root, runtime, sources, results, index = self.campaign() + proof = audit.request_integrity(root, sources, results) + self.assertEqual([], proof['findings']) + self.assertEqual(12, proof['expectedCases']) + self.assertEqual(12, proof['verifiedCases']) + self.assertEqual(set(results), set(proof['bindings'])) + + def test_independent_inventory_rejects_extra_request_files(self): + root, runtime, sources, results, index = self.campaign() + (runtime / 'respawn-requests/orphan.json').write_text('{}') + self.assertTrue(audit.request_integrity(root, sources, results)['findings']) + + def test_independent_inventory_rejects_changed_root_and_missing_case(self): + root, runtime, sources, results, index = self.campaign() + (runtime / 'dimension-fixture.json').write_bytes(self.original + b' ') + results.pop(next(iter(results))) + self.assertTrue(audit.request_integrity(root, sources, results)['findings']) + + def test_independent_inventory_rejects_an_altered_request_evidence_copy(self): + root, runtime, sources, results, index = self.campaign() + label = requests.CASE_LABELS[0] + path = runtime / 'respawn-evidence' / label / 'request.json' + path.write_bytes(path.read_bytes() + b' ') + proof = audit.request_integrity(root, sources, results) + self.assertTrue(proof['findings']) + self.assertEqual(11, proof['verifiedCases']) + + def test_independent_inventory_rejects_native_record_changes_even_if_rehashed(self): + root, runtime, sources, results, index = self.campaign() + label = requests.CASE_LABELS[0] + path = runtime / ('dimensions-' + label + '.json') + record = json.loads(path.read_bytes()) + record['definition']['playerUuid'] = 'another-player' + path.write_text(json.dumps(record)) + index['cases']['original110/' + label]['recordSha256'] = requests.digest(path) + (root / requests.REQUEST_INDEX).write_text(json.dumps(index)) + proof = audit.request_integrity(root, sources, results) + self.assertTrue(proof['findings']) + self.assertEqual(11, proof['verifiedCases']) + + def test_empty_request_inventory_and_legacy_protocol_cannot_qualify(self): + (self.runtime / requests.REQUEST_INDEX).write_text(json.dumps({'protocol': 'legacy', 'cases': {}})) + proof = audit.request_integrity(self.runtime, {}, {}) + self.assertTrue(proof['findings']) + self.assertEqual(0, proof['verifiedCases']) + + +if __name__ == '__main__': + unittest.main() diff --git a/test-rig/migration/test_native117_save_io.py b/test-rig/migration/test_native117_save_io.py new file mode 100644 index 00000000..1374fb57 --- /dev/null +++ b/test-rig/migration/test_native117_save_io.py @@ -0,0 +1,95 @@ +import io +import json +from pathlib import Path +import struct +import tempfile +import unittest +import zlib + +import nbtlib + +from matrix import WORLD, fingerprint, write_json +from native117_save_io import audit_save, capture_save, require_same_settings + + +class NativeSaveIoTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.runtime = Path(self.temp.name) + self.world = self.runtime / WORLD + (self.world / 'region').mkdir(parents=True) + settings = nbtlib.Compound({ + 'seed': nbtlib.Long(42), 'generate_features': nbtlib.Byte(1), 'bonus_chest': nbtlib.Byte(0), + 'dimensions': nbtlib.Compound({key: nbtlib.Compound({ + 'type': nbtlib.String(key), 'generator': nbtlib.Compound({ + 'type': nbtlib.String('minecraft:flat'), 'settings': nbtlib.Compound()})}) + for key in ('minecraft:overworld', 'minecraft:the_nether', 'minecraft:the_end')})}) + nbtlib.File({'Data': nbtlib.Compound({'WorldGenSettings': settings})}, gzipped=True).save(self.world / 'level.dat') + self.rows = [dict(id='boundary', x=12, y=0, z=0, actualName='minecraft:stone', properties={})] + write_json(self.runtime / 'migration-test.json', self.rows) + self.chunk() + + def chunk(self, old_packing=False, name='minecraft:stone'): + palette = nbtlib.List[nbtlib.Compound]([ + nbtlib.Compound({'Name': nbtlib.String('minecraft:air' if i == 0 else name if i == 1 else 'fixture:block_' + str(i))}) + for i in range(17)]) + words = [0] * (320 if old_packing else 342) + if not old_packing: + words[1] = 1 # Entry twelve begins a new padded long, not bit sixty. + chunk = nbtlib.File({'DataVersion': nbtlib.Int(2730), 'Level': nbtlib.Compound({ + 'xPos': nbtlib.Int(0), 'zPos': nbtlib.Int(0), 'Sections': nbtlib.List[nbtlib.Compound]([ + nbtlib.Compound({'Y': nbtlib.Byte(0), 'Palette': palette, 'BlockStates': nbtlib.LongArray(words)})])})}) + stream = io.BytesIO() + chunk.write(stream) + payload = b'\x02' + zlib.compress(stream.getvalue()) + record = struct.pack('>I', len(payload)) + payload + sectors = (len(record) + 4095) // 4096 + header = ((2 << 8) | sectors).to_bytes(4, 'big') + bytes(8192 - 4) + (self.world / 'region/r.0.0.mca').write_bytes(header + record + bytes(sectors * 4096 - len(record))) + + def test_native_padded_save_is_retained_and_replayed_without_world_writes(self): + before = fingerprint(self.world) + receipt = capture_save(self.runtime, 'test') + self.assertEqual('PASS', receipt['status']) + self.assertEqual(1, receipt['witnesses']) + self.assertEqual(1, receipt['paddedSections']) + self.assertEqual(42, receipt['worldGenSettings']['seed']) + self.assertEqual(receipt, audit_save(self.runtime, receipt)) + self.assertEqual(before, fingerprint(self.world)) + + def test_old_cross_long_packing_fails_in_actual_saved_chunk_path(self): + self.chunk(old_packing=True) + with self.assertRaisesRegex(ValueError, 'padded length'): + capture_save(self.runtime, 'test') + + def test_palette_identity_mismatch_is_not_normalized(self): + self.chunk(name='minecraft:dirt') + with self.assertRaisesRegex(AssertionError, 'Saved block differs'): + capture_save(self.runtime, 'test') + + def test_missing_region_cannot_qualify_as_air(self): + (self.world / 'region/r.0.0.mca').unlink() + with self.assertRaises(FileNotFoundError): + capture_save(self.runtime, 'test') + + def test_retained_chunk_tampering_and_duplicate_capture_fail(self): + receipt = capture_save(self.runtime, 'test') + with self.assertRaises(FileExistsError): + capture_save(self.runtime, 'test') + path = self.runtime / receipt['root'] / next(name for name in receipt['files'] if name.endswith('.chunk')) + path.write_bytes(b'altered') + with self.assertRaisesRegex(AssertionError, 'evidence changed'): + audit_save(self.runtime, receipt) + + def test_complete_native_settings_not_just_seed_are_restart_invariants(self): + first = capture_save(self.runtime, 'test') + changed = json.loads(json.dumps(first)) + changed['worldGenSettings']['bonus_chest'] = 1 + with self.assertRaisesRegex(AssertionError, 'WorldGenSettings changed'): + require_same_settings(first, changed) + self.assertIsNone(require_same_settings(first, first)) + + +if __name__ == '__main__': + unittest.main() diff --git a/test-rig/migration/test_native117_sources.py b/test-rig/migration/test_native117_sources.py new file mode 100644 index 00000000..5ca85784 --- /dev/null +++ b/test-rig/migration/test_native117_sources.py @@ -0,0 +1,329 @@ +import copy +from contextlib import contextmanager +import json +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +import connected +import native115 +import native117_sources as sources +from matrix import WORLD, digest, fingerprint, write_json +from native117 import RIG +from native117_campaign import ACCEPTED_POWER114, REQUIRED, require_routes +from native117_sources import ACCEPTED_POWER115, SOURCE115, pending_routes, require_source115_receipt +from native117_sources116 import SOURCE116 +from test_connected import advanced, entries, nbt, sample + + +class Native115SourceReceiptTests(unittest.TestCase): + def setUp(self): + root = RIG / "build/native117-source-unit" + root.mkdir(parents=True, exist_ok=True) + self.temporary = tempfile.TemporaryDirectory(dir=root) + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + + def receipt(self, kind="frozen"): + source = self.root / "stopped-native115" + (source / WORLD).mkdir(parents=True) + (source / WORLD / "level.dat").write_bytes(b"stopped source; never launched") + manifest = self.root / "target115.json" + write_json(manifest, {}) + record = dict(status="PASS", scope="DISPOSABLE_NATIVE115_SOURCE_ONLY", acceptedPower115=ACCEPTED_POWER115, + stoppedCleanly=True, sourceRuntime=str(source.resolve()), runtimeFingerprint=fingerprint(source), + worldFingerprint=fingerprint(source / WORLD), target115Manifest=str(manifest), + target115ManifestSha256=digest(manifest), blocks=355, tiles=332, + preservationStatus="PASS", requestedTicks=[512, 512], advancedTicks=[512, 512]) + path = self.root / "receipt.json" + write_json(path, record) + row = dict(id="native115/full" if kind == "frozen" else "native115/original110", kind=kind, + source=str(source), sourceStatus="GENERATED_DISPOSABLE_115_VERIFIED", + source115Receipt=str(path), source115ReceiptSha256=digest(path)) + return source, path, record, row + + def test_sixteen_frozen_and_twenty_connected_routes_are_required(self): + self.assertEqual((16, 20), (len(REQUIRED["frozen"]), len(REQUIRED["connected"]))) + document = dict(format=1, acceptedPower114=ACCEPTED_POWER114, + routes=[dict(id=identity, kind=kind) for kind, identities in REQUIRED.items() for identity in identities]) + require_routes(document) + for kind in REQUIRED: + for identity in SOURCE115[kind] | SOURCE116[kind]: + value = copy.deepcopy(document) + value["routes"] = [row for row in value["routes"] if row["id"] != identity] + with self.subTest(kind=kind, identity=identity), self.assertRaises(AssertionError): + require_routes(value) + + def test_pending_routes_cannot_be_promoted_by_old_destination_pass(self): + for row in pending_routes(): + row["status"] = "PASS" + with self.assertRaisesRegex(AssertionError, "stopped-source receipt"): + require_source115_receipt(row) + + def test_exact_stopped_source_passes_and_changed_world_fails(self): + source, _, _, row = self.receipt() + with patch("native117_sources.native115.pinned_profile"): + require_source115_receipt(row) + (source / WORLD / "level.dat").write_bytes(b"different source") + with self.assertRaisesRegex(AssertionError, "fingerprint"): + require_source115_receipt(row) + + def test_source_acceptance_needs_scope_merge_and_clean_stop(self): + _, path, original, row = self.receipt() + for field, value in (("scope", "native115-destination-PASS"), ("acceptedPower115", "0" * 40), + ("stoppedCleanly", False), ("status", "UNQUALIFIED")): + record = dict(original, **{field: value}) + write_json(path, record) + row["source115ReceiptSha256"] = digest(path) + with self.subTest(field=field), patch("native117_sources.native115.pinned_profile"), self.assertRaises(AssertionError): + require_source115_receipt(row) + + def test_comprehensive_counts_cannot_drop_one_tile(self): + _, path, record, row = self.receipt() + record["tiles"] = 331 + write_json(path, record) + row["source115ReceiptSha256"] = digest(path) + with patch("native117_sources.native115.pinned_profile"), self.assertRaisesRegex(AssertionError, "355 blocks and 332 tiles"): + require_source115_receipt(row) + + def test_both_connected_boots_need_exact_native_ticks(self): + _, path, original, row = self.receipt("connected") + with patch("native117_sources.native115.pinned_profile"): + require_source115_receipt(row) + for field, value in (("requestedTicks", [512]), ("advancedTicks", [512, 511]), ("preservationStatus", "FAIL")): + write_json(path, dict(original, **{field: value})) + row["source115ReceiptSha256"] = digest(path) + with self.subTest(field=field), self.assertRaisesRegex(AssertionError, "512-tick"): + require_source115_receipt(row) + + def test_manifest_change_is_not_approved_by_receipt_status(self): + _, _, record, row = self.receipt() + Path(record["target115Manifest"]).write_text("changed") + with self.assertRaisesRegex(AssertionError, "manifest changed"): + require_source115_receipt(row) + + +class Native115SourceGeneratorTests(unittest.TestCase): + def setUp(self): + root = RIG / 'build/native117-source-unit' + root.mkdir(parents=True, exist_ok=True) + self.temp = tempfile.TemporaryDirectory(dir=root) + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.seed = self.root / 'seed with spaces' + (self.seed / 'libraries').mkdir(parents=True) + (self.seed / 'libraries/library.jar').write_bytes(b'code only') + (self.seed / native115.LAUNCHER).write_bytes(b'pinned launcher') + self.manifest = self.root / 'archived F.json' + write_json(self.manifest, {'target115': {'seed': str(self.seed), 'launcher': native115.LAUNCHER, + 'launcher_sha256': digest(self.seed / native115.LAUNCHER), 'jars': []}}) + self.rows = [] + for kind, name in [('frozen', 'original110-chained-113')] + [('connected', n) for n in sources.HISTORICAL]: + source = self.root / 'protected fixtures' / (kind + '-' + name) + (source / WORLD).mkdir(parents=True) + (source / WORLD / 'level.dat').write_bytes(b'immutable source bytes') + snapshot = 'migration-restarted.json' if kind == 'frozen' else 'source' + if kind == 'frozen': + rows = [dict(id=str(n), nbtBase64='retained tile bytes') if n < 332 else dict(id=str(n)) for n in range(355)] + write_json(source / snapshot, rows) + write_json(source / 'migration-fixture.json', rows) + else: + write_json(source / 'connected-fixture.json', connected.fixture(name)) + write_json(source / 'migration-fixture.json', connected.fixture(name)['blocks']) + write_json(source / 'connected-source.json', advanced(sample())) + write_json(source / 'migration-source.json', advanced(sample())['snapshot']) + hashes = self.root / ('hashes-' + kind + '-' + name + '.json') + write_json(hashes, fingerprint(source / WORLD)) + source_manifest = self.root / ('pins-' + kind + '-' + name + '.json') + write_json(source_manifest, {}) + selected = source / (snapshot if kind == 'frozen' else 'migration-source.json') + self.rows.append(dict(id='legacy/' + name, kind=kind, source=str(source), sourceKey=name, + sourceManifest=str(source_manifest), sourceManifestSha256=digest(source_manifest), + sourceHashes=str(hashes), sourceHashesSha256=digest(hashes), + sourceSnapshot=snapshot, sourceSnapshotSha256=digest(selected))) + self.routes = self.root / 'approved routes.json' + write_json(self.routes, dict(routes=self.rows)) + self.args = SimpleNamespace(source_routes=self.routes, target115_manifest=self.manifest, + output=self.root / 'new sources with spaces', java8=self.root / 'java.exe', port=25575, validate_only=False) + + @contextmanager + def pinned(self): + with patch.object(sources, 'ARCHIVED115_F_SHA256', digest(self.manifest)), \ + patch.object(sources, 'ARCHIVED115_ROUTES_SHA256', digest(self.routes)), \ + patch.object(native115, 'pinned_profile', return_value=(self.seed, native115.LAUNCHER, [])), \ + patch('native115_connected.verify_source'): + yield + + def plan(self): + with self.pinned(): + return sources.preflight_source115(self.args) + + def stops(self): + return [dict(label=label, returncode=0, commands=['save-all flush', 'stop']) for label in ('converted', 'restarted')] + + def frozen_result(self, runtime): + runtime.mkdir(parents=True) + (runtime / WORLD).mkdir() + (runtime / WORLD / 'level.dat').write_bytes(b'unit saved native115 world') + rows = [dict(id=str(n), nbtBase64='retained tile bytes') if n < 332 else dict(id=str(n)) for n in range(355)] + write_json(runtime / 'migration-restarted.json', rows) + write_json(runtime / 'migration-fixture.json', rows) + write_json(runtime / 'state-comparison.json', {gate: dict(status='PASS', witnesses=dict(blocks=355, tiles=332)) + for gate in ('converted', 'restarted', 'idempotence')}) + for label in ('converted', 'restarted'): + (runtime / ('console-' + label + '.log')).write_text( + 'Starting minecraft server version 1.15.2\n' + 'Forge mod loading, version 31.2.57, for MC 1.15.2 with MCP 20200515.085601\n' + 'Saved the game\nStopping server\nSaving worlds\nAll chunks are saved\n') + + def test_preflight_selects_six_copied_source_recipes_without_creation_or_launch(self): + with patch('subprocess.Popen', side_effect=AssertionError('No lease used in tests')): + plan = self.plan() + self.assertEqual(set().union(*sources.SOURCE115.values()), {row['id'] for row in plan['sources']}) + self.assertFalse(self.args.output.exists()) + + def test_wrong_archived_F_hash_fails_before_any_output(self): + with self.pinned(), patch.object(sources, 'ARCHIVED115_F_SHA256', '0' * 64), self.assertRaisesRegex(AssertionError, 'archived F'): + sources.preflight_source115(self.args) + self.assertFalse(self.args.output.exists()) + + def test_wrong_archived_source_plan_hash_fails_before_any_output(self): + with self.pinned(), patch.object(sources, 'ARCHIVED115_ROUTES_SHA256', '0' * 64), self.assertRaisesRegex(AssertionError, 'source plan'): + sources.preflight_source115(self.args) + self.assertFalse(self.args.output.exists()) + + def test_comprehensive_331_tiles_is_not_a_source(self): + source = Path(self.rows[0]['source']) + path = source / self.rows[0]['sourceSnapshot'] + rows = json.loads(path.read_text()) + rows[331].pop('nbtBase64') + write_json(path, rows) + self.rows[0]['sourceSnapshotSha256'] = digest(path) + write_json(self.routes, dict(routes=self.rows)) + with self.assertRaisesRegex(AssertionError, '355 blocks and 332 tiles'): + self.plan() + self.assertFalse(self.args.output.exists()) + + def test_changed_world_is_rejected_before_output(self): + (Path(self.rows[0]['source']) / WORLD / 'level.dat').write_bytes(b'changed') + with self.assertRaisesRegex(AssertionError, 'world fingerprint'): + self.plan() + + def test_duplicate_source_recipe_is_rejected(self): + write_json(self.routes, dict(routes=self.rows + [self.rows[0]])) + with self.assertRaisesRegex(AssertionError, 'exactly once'): + self.plan() + + def test_output_cannot_overlap_canonical_source(self): + self.args.output = Path(self.rows[0]['source']) / 'new-output' + with self.assertRaisesRegex(ValueError, 'overlaps a protected input'): + self.plan() + + def test_seed_or_source_changes_after_preflight_fail(self): + plan = self.plan() + (self.seed / 'libraries/library.jar').write_bytes(b'changed code') + with self.assertRaisesRegex(AssertionError, 'seed code'): + sources.verify_source115_inputs(plan) + + def test_valid_frozen_result_requires_real_two_boot_clean_stops_and_counts(self): + runtime = self.root / 'disposable-result' + self.frozen_result(runtime) + settings = SimpleNamespace(output=runtime) + actual, snapshot, fixture, proof = sources.source115_result('frozen', settings, self.stops()) + self.assertEqual((runtime, 'migration-restarted.json', None), (actual, snapshot, fixture)) + self.assertEqual((355, 332, True), (proof['blocks'], proof['tiles'], proof['stoppedCleanly'])) + + def test_nonzero_missing_save_or_missing_second_stop_cannot_qualify(self): + runtime = self.root / 'disposable-result' + self.frozen_result(runtime) + for stops in (self.stops()[:1], [dict(row, returncode=1) for row in self.stops()], + [dict(row, commands=['stop']) for row in self.stops()]): + with self.subTest(stops=stops), self.assertRaisesRegex(AssertionError, 'cleanly saved and stopped'): + sources.source115_result('frozen', SimpleNamespace(output=runtime), stops) + + def test_success_exit_does_not_hide_missing_semantic_gate(self): + runtime = self.root / 'disposable-result' + self.frozen_result(runtime) + write_json(runtime / 'state-comparison.json', {'converted': dict(status='PASS')}) + with self.assertRaisesRegex(AssertionError, 'semantic'): + sources.source115_result('frozen', SimpleNamespace(output=runtime), self.stops()) + + def test_registry_errors_are_never_filtered_from_source_receipt(self): + runtime = self.root / 'disposable-result' + self.frozen_result(runtime) + with (runtime / 'console-converted.log').open('a') as stream: + stream.write('[main/ERROR] retained failure\n') + with self.assertRaisesRegex(AssertionError, 'Native runtime errors'): + sources.source115_result('frozen', SimpleNamespace(output=runtime), self.stops()) + + def connected_result(self): + output = self.root / 'connected-result' + runtime = output / 'target115' + self.frozen_result(runtime) + write_json(runtime / 'connected-fixture.json', connected.fixture('original110')) + first = advanced(sample()) + first.update(requestedTicks=512, advancedTicks=512) + second = copy.deepcopy(first) + rows = entries(second) + nbt(rows['C-CONVEYOR-input'], '{Items:[{id:"minecraft:dirt",Count:1726}]}') + nbt(rows['C-CONVEYOR-output'], '{Items:[{id:"minecraft:dirt",Count:2b}]}') + for station, count in (('C-STEAM', 190), ('C-ELECTRIC', 62)): + nbt(rows[station + '-furnace'], '{Items:[{id:"minecraft:iron_ore",Count:%d},{id:"minecraft:iron_ingot",Count:2b}]}' % count) + nbt(rows['C-STEAM-crusher'], '{Items:[{id:"minecraft:cobblestone",Count:62b},{id:"minecraft:gravel",Count:2b}]}') + for station, count, item in (('C-SDRILL', 2, 'gravel'), ('C-EDRILL', 18, 'cobblestone')): + targets = [e for e in second['snapshot'] if e['station'] == station and e['kind'].startswith('target-')] + for target in targets[:count]: + target['actualName'] = 'minecraft:air' + nbt(rows[station + '-drill'], '{Items:[{id:"minecraft:%s",Count:%db}]}' % (item, count)) + for label, before, after in (('converted', sample(), first), ('restarted', first, second)): + loaded = copy.deepcopy(before) + loaded.update(requestedTicks=0, advancedTicks=0) + write_json(runtime / ('connected-' + label + '-loaded.json'), loaded) + write_json(runtime / ('connected-' + label + '-worked.json'), after) + gates = {label + '_' + gate: dict(status='PASS') for label in ('converted', 'restarted') + for gate in ('preservation', 'operation', 'native_boot')} + gates['real_output_across_restarts'] = dict(status='PASS') + write_json(output / 'native115-connected-results.json', dict(status='PASS', sourcePreservation=dict(status='PASS'), results=gates)) + return SimpleNamespace(output=output), runtime + + def test_connected_raw_two_interval_proof_uses_unchanged_oracle(self): + settings, runtime = self.connected_result() + actual, snapshot, key, proof = sources.source115_result('connected', settings, self.stops()) + self.assertEqual((runtime, 'restarted-worked', 'original110'), (actual, snapshot, key)) + self.assertEqual(([512, 512], [512, 512]), (proof['requestedTicks'], proof['advancedTicks'])) + + def test_claimed_pass_cannot_hide_511_raw_ticks(self): + settings, runtime = self.connected_result() + path = runtime / 'connected-restarted-worked.json' + document = json.loads(path.read_text()) + document['advancedTicks'] = 511 + write_json(path, document) + with self.assertRaisesRegex(AssertionError, '512-tick'): + sources.source115_result('connected', settings, self.stops()) + + def test_claimed_pass_cannot_hide_a_rebuilt_machine_or_empty_gate_set(self): + settings, runtime = self.connected_result() + path = runtime / 'connected-restarted-worked.json' + document = json.loads(path.read_text()) + document['buildCallsThisProcess'] = 1 + write_json(path, document) + with self.assertRaisesRegex(AssertionError, 'construction is forbidden'): + sources.source115_result('connected', settings, self.stops()) + write_json(settings.output / 'native115-connected-results.json', dict(status='PASS', sourcePreservation=dict(status='PASS'), results={})) + with self.assertRaisesRegex(AssertionError, 'operating gates'): + sources.source115_result('connected', settings, self.stops()) + + def test_frozen_process_adapter_refuses_a_canonical_launch_before_popen(self): + settings = SimpleNamespace(output=self.root / 'new-copy', port=25575) + def bad_protocol(arguments): + native115.start_snapshot(Path(self.rows[0]['source']), Path('java.exe'), native115.LAUNCHER, 'converted') + with patch.object(native115, 'run', side_effect=bad_protocol), patch('subprocess.Popen') as launch: + with self.assertRaisesRegex(AssertionError, 'NEW disposable'): + sources.run_source115(settings, 'frozen') + launch.assert_not_called() + + +if __name__ == '__main__': + unittest.main() diff --git a/test-rig/migration/test_native117_sources116.py b/test-rig/migration/test_native117_sources116.py new file mode 100644 index 00000000..225a3878 --- /dev/null +++ b/test-rig/migration/test_native117_sources116.py @@ -0,0 +1,512 @@ +"""Stopped-source receipt controls. All worlds here are synthetic unit fixtures. + +No Minecraft process may run in this suite; real source generation is separately +authorized and cannot be replaced by these tests or an earlier destination PASS. +""" + +import copy +from contextlib import contextmanager +import json +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch +import zipfile + +import nbtlib +import connected +import native116 +import native116_code +import native117_sources116 as sources +from native116_resource_audit import SLOT_LIMITS +from matrix import WORLD, digest, fingerprint, write_json +from test_connected import advanced, entries, nbt, sample +from test_semantic_machine_audit import pair + + +class Source116Tests(unittest.TestCase): + def setUp(self): + parent = sources.RIG / "build/native117-source116-unit" + parent.mkdir(parents=True, exist_ok=True) + self.temp = tempfile.TemporaryDirectory(dir=parent) + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.no_process = patch("subprocess.Popen", side_effect=AssertionError("No Minecraft process in source116 unit tests")) + self.no_process.start() + self.addCleanup(self.no_process.stop) + self.seed = self.root / "seed with spaces" + (self.seed / "libraries").mkdir(parents=True) + (self.seed / "libraries/pinned.jar").write_bytes(b"unit native library") + (self.seed / native116.LAUNCHER).write_bytes(b"unit native launcher") + self.code = dict(files=native116_code.code_files(self.seed)) + write_json(self.seed / native116_code.INVENTORY, self.code) + self.jars = [] + for identity in ("advantageworkstest", "poweradvantage", "steamadvantage", "electricadvantage", "basemetals", "orespawn"): + jar = self.root / "pinned mods" / (identity + ".jar") + jar.parent.mkdir(exist_ok=True) + with zipfile.ZipFile(jar, "w") as archive: + archive.writestr("META-INF/mods.toml", '[[mods]]\nmodId="%s"\nversion="1.0.0.116051"\n' % identity) + self.jars.append(dict(id=identity, version="1.0.0.116051", path=str(jar), sha256=digest(jar))) + self.manifest = self.root / "accepted native116.json" + self.target = dict(seed=str(self.seed), launcher=native116.LAUNCHER, + launcher_sha256=digest(self.seed / native116.LAUNCHER), jars=self.jars, serverCode=self.code) + write_json(self.manifest, dict(target116=self.target)) + self.java = self.root / "jdk8/bin/java.exe" + self.java.parent.mkdir(parents=True) + self.java.write_bytes(b"unit java; never execute") + (self.java.parent.parent / "release").write_text('JAVA_VERSION="1.8.0_502"\n') + self.rows = [] + for kind, name in [("frozen", "original110-chained-113")] + [("connected", name) for name in sources.HISTORICAL]: + source = self.root / "protected fixtures" / (kind + "-" + name) + self.world(source) + (source / "mods").mkdir() + write_json(source / "input-jars.json", []) + if kind == "frozen": + snapshot = "migration-restarted.json" + rows = self.frozen_rows() + write_json(source / snapshot, rows) + write_json(source / "migration-fixture.json", rows) + else: + snapshot = "source" + write_json(source / "connected-fixture.json", connected.fixture(name)) + write_json(source / "migration-fixture.json", connected.fixture(name)["blocks"]) + write_json(source / "connected-cold.json", sample()) + worked = advanced(sample()) + write_json(source / "connected-source.json", worked) + write_json(source / "migration-source.json", worked["snapshot"]) + hashes = self.root / (kind + "-" + name + "-world.json") + write_json(hashes, fingerprint(source / WORLD)) + pins = self.root / (kind + "-" + name + "-pins.json") + write_json(pins, {name: dict(seed=str(self.seed), launcher=native116.LAUNCHER, jars=[])}) + path = source / (snapshot if kind == "frozen" else "migration-source.json") + self.rows.append(dict(id="legacy/" + name, kind=kind, source=str(source), sourceKey=name, + sourceManifest=str(pins), sourceManifestSha256=digest(pins), sourceHashes=str(hashes), + sourceHashesSha256=digest(hashes), sourceSnapshot=snapshot, sourceSnapshotSha256=digest(path), + sourceFixtureKey=None)) + self.routes = self.root / "archived Ready.json" + write_json(self.routes, dict(format=1, routes=self.rows)) + self.args = SimpleNamespace(source_routes=self.routes, target116_manifest=self.manifest, java8=self.java, + output=self.root / "NEW disposable sources", port=25588, validate_only=False) + + def world(self, runtime): + (runtime / WORLD).mkdir(parents=True) + nbtlib.File({"Data": nbtlib.Compound({"DataVersion": nbtlib.Int(2586)})}).save(runtime / WORLD / "level.dat", gzipped=True) + + def frozen_rows(self): + rows = [] + for index in range(355): + row = dict(id=str(index), name="minecraft:stone", actualName="minecraft:stone", x=index, y=64, z=0, properties={}) + if index < 332: + nbt(row, '{id:"minecraft:chest",Items:[]}') + rows.append(row) + return rows + + @contextmanager + def pinned(self): + with patch.object(sources, "TARGET116_SHA256", digest(self.manifest)), \ + patch.object(sources, "SOURCE_ROUTES_SHA256", digest(self.routes)): + yield + + def plan(self): + with self.pinned(): + return sources.prepare(self.args) + + def stage(self, runtime): + import shutil + self.world(runtime) + shutil.copytree(self.seed / "libraries", runtime / "libraries") + shutil.copy2(self.seed / native116.LAUNCHER, runtime / native116.LAUNCHER) + write_json(runtime / native116_code.INVENTORY, self.code) + (runtime / "mods").mkdir() + for row in self.jars: + shutil.copy2(row["path"], runtime / "mods" / Path(row["path"]).name) + write_json(runtime / "input-jars.json", [dict(file=Path(row["path"]).name, sha256=row["sha256"]) for row in self.jars]) + + def stops(self, runtime, kind): + result = [] + for label, pid in (("converted", 111), ("restarted", 222)): + logs = {} + for prefix in ("console", "latest", "debug"): + name = prefix + "-" + label + ".log" + (runtime / name).write_text( + "Starting minecraft server version 1.16.5\n" + "Forge mod loading, version 36.2.34, for MC 1.16.5 with MCP 20210115.111550\n" + + label + "\nSaved the game\nStopping server\nSaving worlds\nAll chunks are saved\n") + logs[name] = digest(runtime / name) + result.append(dict(label=label, pid=pid, returncode=0, commands=sources._commands(kind, label), logs=logs)) + return result + + def frozen_result(self): + runtime = self.root / "new frozen runtime" + self.stage(runtime) + source = Path(self.rows[0]["source"]) + (runtime / "migration-fixture.json").write_bytes((source / "migration-fixture.json").read_bytes()) + for label in ("converted", "restarted"): + write_json(runtime / ("migration-" + label + ".json"), self.frozen_rows()) + report = {gate: dict(status="PASS", witnesses=dict(blocks=355, tiles=332)) + for gate in ("converted", "restarted", "idempotence")} + write_json(runtime / "state-comparison.json", report) + write_json(runtime / "native116-provenance.json", dict(nativeSaveFormat={label: {"label": label} + for label in ("converted", "restarted")})) + settings = sources.source116_settings(self.args, self.rows[0], runtime) + return settings, runtime, self.stops(runtime, "frozen") + + @contextmanager + def saved_format(self): + # These are receipt-layer tests. Real native chunk decoding has its own + # negative-control suite; audit_save must still be called for both boots. + with patch.object(sources.native116_save_io, "audit_save") as audit, \ + patch.object(sources.native116_save_io, "require_same_settings") as same: + yield audit, same + + def connected_result(self): + output = self.root / "connected result" + runtime = output / "target116" + self.stage(runtime) + recipe = self.rows[-1] + source = Path(recipe["source"]) + definition = connected.fixture("original110") + write_json(runtime / "connected-fixture.json", definition) + write_json(runtime / "migration-fixture.json", definition["blocks"]) + initial, first = pair() + for document in (initial, first): + for row in document["snapshot"]: + row["properties"] = {} + if row["actualName"] in SLOT_LIMITS and "nbt" not in row: + nbt(row, "{Items:[]}") + row.update(witness=row["id"], objectIdentity=len(row["id"])) + first.update(gameTime=612, requestedTicks=512, advancedTicks=512) + # The canonical source is unit data, never a saved gameplay fixture. + write_json(source / "migration-source.json", initial["snapshot"]) + second = copy.deepcopy(first) + second.update(gameTime=1124) + rows = entries(second) + items = ','.join('{Slot:%db,id:"minecraft:dirt",Count:%db}' % (slot, 62 if slot == 26 else 64) for slot in range(27)) + nbt(rows["C-CONVEYOR-input"], '{Items:[' + items + ']}') + nbt(rows["C-CONVEYOR-output"], '{Items:[{Slot:0b,id:"minecraft:dirt",Count:2b}]}') + nbt(rows["C-STEAM-furnace"], '{Items:[{Slot:0b,id:"minecraft:coal",Count:32b},' + '{Slot:1b,id:"minecraft:iron_ore",Count:62b},{Slot:2b,id:"minecraft:iron_ore",Count:64b},' + '{Slot:3b,id:"minecraft:iron_ore",Count:64b},{Slot:4b,id:"minecraft:iron_ingot",Count:2b}]}') + for key, raw, product in (("C-ELECTRIC-furnace", "iron_ore", "iron_ingot"), ("C-STEAM-crusher", "cobblestone", "gravel")): + nbt(rows[key], '{Items:[{Slot:0b,id:"minecraft:%s",Count:62b},{Slot:1b,id:"minecraft:%s",Count:2b}]}' % (raw, product)) + for station, product, count in (("C-SDRILL", "gravel", 2), ("C-EDRILL", "cobblestone", 18)): + targets = [row for row in second["snapshot"] if row["station"] == station and row["kind"].startswith("target-")] + for row in targets[:count]: + row["actualName"] = "minecraft:air" + nbt(rows[station + "-drill"], '{Items:[{Slot:0b,id:"minecraft:%s",Count:%db}]}' % (product, count)) + for label, old, worked in (("converted", initial, first), ("restarted", first, second)): + loaded = copy.deepcopy(old) + loaded.update(requestedTicks=0, advancedTicks=0) + for suffix, document in (("loaded", loaded), ("worked", worked)): + write_json(runtime / ("connected-" + label + "-" + suffix + ".json"), document) + write_json(runtime / ("migration-" + label + "-" + suffix + ".json"), document["snapshot"]) + gates = {label + "_" + gate: dict(status="PASS") for label in ("converted", "restarted") + for gate in ("preservation", "exact_resources", "operation", "completed_work", "native_boot")} + gates["real_output_across_restarts"] = dict(status="PASS") + write_json(output / "native116-connected-results.json", dict(status="PASS", sourcePreservation=dict(status="PASS"), + results=gates, sourceRuntimeBefore=fingerprint(source), sourceRuntimeAfter=fingerprint(source), + nativeSaveFormat={label: {"label": label} for label in ("converted", "restarted")})) + settings = sources.source116_settings(self.args, recipe, output) + return settings, runtime, self.stops(runtime, "connected") + + def test_pending_exact_six_cannot_be_approved_by_destination_pass(self): + rows = sources.pending_routes() + self.assertEqual(6, len(rows)) + self.assertEqual({"native116/full", *("native116/" + name for name in sources.HISTORICAL)}, {row["id"] for row in rows}) + for row in rows: + with self.subTest(id=row["id"]), self.assertRaisesRegex(AssertionError, "stopped-source receipt"): + sources.verify_source_receipt(dict(row, status="PASS")) + + def test_prepare_is_read_only_and_preserves_exact_recipe_authority(self): + before = fingerprint(self.root) + plan = self.plan() + self.assertEqual(before, fingerprint(self.root)) + self.assertFalse(self.args.output.exists()) + self.assertEqual(6, len(plan["sources"])) + self.assertEqual(self.rows, [row["recipe"] for row in plan["sources"]]) + self.assertEqual(sources.ACCEPTED_POWER116, plan["acceptedPower116"]) + self.assertIn(str(Path(sources.__file__).resolve()), plan["protocolFingerprint"]) + + def test_real_authority_constants_are_exact(self): + self.assertEqual("D191DADA303689D6575ABC2BE63DD5439499A048F433E84FB0CDEA598D1483BB", sources.TARGET116_SHA256) + self.assertEqual("c310d0ca63ad9a8408dd4bba75172afafa5c71b7", sources.ACCEPTED_POWER116) + + def test_prepare_rejects_wrong_manifest_before_writes(self): + with self.assertRaisesRegex(AssertionError, "final accepted"): + sources.prepare(self.args) + self.assertFalse(self.args.output.exists()) + + def test_prepare_rejects_changed_archived_plan(self): + with self.pinned(), patch.object(sources, "SOURCE_ROUTES_SHA256", "0" * 64), self.assertRaisesRegex(AssertionError, "Ready source plan"): + sources.prepare(self.args) + + def test_prepare_rejects_duplicate_or_missing_recipe(self): + for rows in (self.rows + [self.rows[0]], self.rows[1:]): + write_json(self.routes, dict(routes=rows)) + with self.subTest(count=len(rows)), self.assertRaisesRegex(AssertionError, "exactly once"): + self.plan() + + def test_prepare_rejects_changed_world(self): + (Path(self.rows[0]["source"]) / WORLD / "changed.dat").write_bytes(b"loss") + with self.assertRaisesRegex(AssertionError, "world fingerprint"): + self.plan() + + def test_prepare_rejects_wrong_snapshot_hash(self): + path = Path(self.rows[0]["source"]) / self.rows[0]["sourceSnapshot"] + path.write_text("[]") + with self.assertRaisesRegex(AssertionError, "source snapshot"): + self.plan() + + def test_prepare_rejects_resealed_354_blocks_or_331_tiles(self): + for change in ("block", "tile"): + rows = self.frozen_rows() + if change == "block": + rows.pop() + else: + rows[331].pop("nbtBase64") + path = Path(self.rows[0]["source"]) / self.rows[0]["sourceSnapshot"] + write_json(path, rows) + self.rows[0]["sourceSnapshotSha256"] = digest(path) + write_json(self.routes, dict(routes=self.rows)) + with self.subTest(change=change), self.assertRaisesRegex(AssertionError, "355 blocks and 332 tiles"): + self.plan() + + def test_prepare_rejects_output_outside_build_and_existing_output(self): + for output in (sources.RIG.parent / "forbidden", self.root): + self.args.output = output + with self.subTest(output=str(output)), self.assertRaises((ValueError, FileExistsError)): + self.plan() + + def test_prepare_rejects_output_under_canonical_source(self): + self.args.output = Path(self.rows[0]["source"]) / "output" + with self.assertRaisesRegex(ValueError, "overlaps a protected input"): + self.plan() + + def test_prepare_rejects_target_java16(self): + (self.java.parent.parent / "release").write_text('JAVA_VERSION="16.0.2"\n') + with self.assertRaisesRegex(ValueError, "Java 8"): + self.plan() + + def test_prepare_canonical_lineage_cannot_change_authorities(self): + self.args.routes = self.root / "canonical117.json" + rows = copy.deepcopy(self.rows) + rows[-1]["sourceKey"] = "different" + write_json(self.args.routes, dict(routes=rows)) + with self.assertRaisesRegex(AssertionError, "archived authority"): + self.plan() + + def test_protocol_baseline_is_checked(self): + with patch.dict(sources.RUNNER_BASELINE, {"native116.py": "0" * 64}), self.assertRaisesRegex(AssertionError, "accepted baseline"): + self.plan() + + def test_settings_keep_java8_snapshot_lineage_and_512_ticks(self): + settings = sources.source116_settings(self.args, self.rows[-1], self.args.output) + self.assertEqual((512, "source", "original110", self.java.resolve()), + (settings.ticks, settings.source_checkpoint, settings.source_key, settings.java8)) + + def test_after_preflight_mutation_is_rejected(self): + plan = self.plan() + (self.seed / "libraries/pinned.jar").write_bytes(b"mutated") + with self.assertRaisesRegex(AssertionError, "sealed inventory"): + sources.verify_source116_inputs(plan) + + def test_after_preflight_original_runtime_mutation_is_rejected(self): + plan = self.plan() + (Path(self.rows[0]["source"]) / "unrelated-state").write_bytes(b"mutated") + with self.assertRaisesRegex(AssertionError, "canonical source runtime"): + sources.verify_source116_inputs(plan) + + def test_frozen_recomputes_raw_semantics_and_audits_both_native_saves(self): + settings, runtime, stops = self.frozen_result() + with self.saved_format() as (audit, same): + actual, snapshot, fixture, proof = sources.source116_result("frozen", settings, stops) + self.assertEqual((runtime, "migration-restarted.json", None), (actual, snapshot, fixture)) + self.assertEqual((355, 332, True), (proof["blocks"], proof["tiles"], proof["stoppedCleanly"])) + self.assertEqual(2, audit.call_count) + self.assertEqual(1, same.call_count) + + def test_failed_native_save_audit_is_not_a_pass(self): + settings, _, stops = self.frozen_result() + with patch.object(sources.native116_save_io, "audit_save", side_effect=AssertionError("native save corrupt")), self.assertRaisesRegex(AssertionError, "native save corrupt"): + sources.source116_result("frozen", settings, stops) + + def test_success_exit_does_not_hide_missing_report_gate(self): + settings, runtime, stops = self.frozen_result() + write_json(runtime / "state-comparison.json", dict(converted=dict(status="PASS"))) + with self.assertRaisesRegex(AssertionError, "semantic comparison"): + sources.source116_result("frozen", settings, stops) + + def test_claimed_pass_cannot_hide_lost_tile_or_changed_block(self): + settings, runtime, stops = self.frozen_result() + path = runtime / "migration-restarted.json" + for mutation in ("tile", "block"): + rows = self.frozen_rows() + if mutation == "tile": + rows[331].pop("nbtBase64") + else: + rows[-1]["actualName"] = "minecraft:air" + write_json(path, rows) + with self.subTest(mutation=mutation), self.assertRaisesRegex(AssertionError, "332 tiles|state/resources"): + sources.source116_result("frozen", settings, stops) + + def test_inventory_and_jar_tampering_are_rejected(self): + settings, runtime, stops = self.frozen_result() + (runtime / "mods/extra.jar").write_bytes(b"unexpected") + with self.assertRaisesRegex(AssertionError, "final accepted"): + sources.source116_result("frozen", settings, stops) + + def test_fixture_cannot_be_rebuilt(self): + settings, runtime, stops = self.frozen_result() + (runtime / "migration-fixture.json").write_text("[]") + with self.assertRaisesRegex(AssertionError, "rebuilt or changed"): + sources.source116_result("frozen", settings, stops) + + def test_stops_require_two_separate_successful_processes_and_exact_commands(self): + settings, _, stops = self.frozen_result() + for changed in (stops[:1], [dict(row, returncode=1) for row in stops], + [dict(row, commands=["stop"]) for row in stops], + [dict(row, pid=111) for row in stops]): + with self.subTest(stops=changed), self.assertRaisesRegex(AssertionError, "stop|process"): + sources.source116_result("frozen", settings, changed) + + def test_missing_empty_and_changed_logs_all_fail(self): + settings, runtime, stops = self.frozen_result() + path = runtime / "debug-restarted.log" + original = path.read_bytes() + for mutation in ("missing", "empty", "changed"): + path.write_bytes(original) + if mutation == "missing": + path.unlink() + else: + path.write_bytes(b"" if mutation == "empty" else b"changed") + with self.subTest(mutation=mutation), self.assertRaises((AssertionError, FileNotFoundError)): + sources.source116_result("frozen", settings, stops) + + def test_all_three_logs_need_native_header_clean_logs_and_shutdown(self): + settings, runtime, stops = self.frozen_result() + path = runtime / "debug-restarted.log" + original = path.read_text() + for bad in (original.replace("1.16.5", "1.17.1"), original + "[main/ERROR] failure\n", + original + "Missing mappings\n", original.replace("All chunks are saved", "")): + path.write_text(bad) + altered = copy.deepcopy(stops) + altered[-1]["logs"][path.name] = digest(path) + with self.subTest(bad=bad[-60:]), self.assertRaises(AssertionError): + sources.source116_result("frozen", settings, altered) + + def test_connected_two_exact_intervals_recompute_unchanged_oracles(self): + settings, runtime, stops = self.connected_result() + with self.saved_format() as (audit, _): + actual, snapshot, fixture, proof = sources.source116_result("connected", settings, stops) + self.assertEqual((runtime, "restarted-worked", "original110"), (actual, snapshot, fixture)) + self.assertEqual(([512, 512], [512, 512]), (proof["requestedTicks"], proof["advancedTicks"])) + self.assertEqual(2, audit.call_count) + + def test_claimed_connected_pass_cannot_hide_511_ticks_or_a_rebuild(self): + settings, runtime, stops = self.connected_result() + path = runtime / "connected-restarted-worked.json" + original = json.loads(path.read_text()) + for key, value in (("advancedTicks", 511), ("buildCallsThisProcess", 1), ("gameTime", 612)): + write_json(path, dict(original, **{key: value})) + with self.subTest(key=key), self.assertRaisesRegex(AssertionError, "512-tick|construction"): + sources.source116_result("connected", settings, stops) + + def test_connected_claimed_pass_cannot_hide_empty_or_failed_semantic_gates(self): + settings, _, stops = self.connected_result() + path = settings.output / "native116-connected-results.json" + original = json.loads(path.read_text()) + for changed in ({}, dict(original["results"], converted_completed_work=dict(status="INCOMPLETE"))): + write_json(path, dict(original, results=changed)) + with self.assertRaisesRegex(AssertionError, "operating gates"): + sources.source116_result("connected", settings, stops) + + def test_connected_migration_snapshot_must_equal_observed_snapshot(self): + settings, runtime, stops = self.connected_result() + write_json(runtime / "migration-restarted-worked.json", []) + with self.assertRaisesRegex(AssertionError, "snapshots disagree"): + sources.source116_result("connected", settings, stops) + + def test_adapter_refuses_canonical_frozen_launch_before_popen(self): + settings = sources.source116_settings(self.args, self.rows[0], self.args.output) + def wrong(arguments): + sources.native116.start_snapshot(Path(self.rows[0]["source"]), self.java, native116.LAUNCHER, "converted") + with patch.object(sources.native116, "run", wrong), self.assertRaisesRegex(AssertionError, "NEW disposable"): + sources.run_source116(settings, "frozen") + + def test_adapter_refuses_canonical_connected_launch_before_popen(self): + settings = sources.source116_settings(self.args, self.rows[-1], self.args.output) + def wrong(arguments): + sources.native116_connected.NativeConnectedServer(Path(self.rows[-1]["source"]), self.java, native116.LAUNCHER, "converted", lambda label: None) + with patch.object(sources.native116_connected, "run", wrong), self.assertRaisesRegex(AssertionError, "NEW disposable"): + sources.run_source116(settings, "connected") + + def test_validate_only_never_creates_output_or_calls_runner(self): + self.args.validate_only = True + with self.pinned(), patch.object(sources, "run_source116", side_effect=AssertionError("no lease")): + self.assertEqual(0, sources.run(self.args)) + self.assertFalse(self.args.output.exists()) + + def receipt(self): + settings, runtime, stops = self.frozen_result() + plan = self.plan() + selected = plan["sources"][0] + provenance = self.root / "source116-input-provenance.json" + write_json(provenance, plan) + with self.saved_format(): + _, snapshot, fixture, proof = sources.source116_result("frozen", settings, stops) + path = self.root / "source116-receipt.json" + receipt = dict(status="PASS", scope=sources.SCOPE, acceptedPower116=sources.ACCEPTED_POWER116, + id=selected["id"], kind=selected["kind"], sourceRuntime=str(runtime.resolve()), + runtimeFingerprint=fingerprint(runtime), worldFingerprint=fingerprint(runtime / WORLD), + sourceRecipe=selected["recipe"], target116Manifest=str(self.manifest.resolve()), + target116ManifestSha256=digest(self.manifest), java8=str(self.java.resolve()), port=self.args.port, + inputProvenance=str(provenance.resolve()), inputProvenanceSha256=digest(provenance), **proof) + write_json(path, receipt) + row = sources._source_row(self.root, selected, runtime, snapshot, fixture, self.manifest) + row.update(sourceStatus=sources.SOURCE_STATUS, source116Receipt=str(path), source116ReceiptSha256=digest(path)) + return path, receipt, row + + def test_receipt_replays_raw_evidence_and_rejects_later_world_change(self): + _, _, row = self.receipt() + with self.pinned(), self.saved_format(): + self.assertEqual("PASS", sources.verify_source_receipt(row)["status"]) + (Path(row["source"]) / WORLD / "changed").write_bytes(b"changed") + with self.assertRaisesRegex(AssertionError, "fingerprint"): + sources.verify_source_receipt(row) + + def test_receipt_cannot_change_scope_baseline_stop_or_route(self): + path, original, row = self.receipt() + for key, value in (("scope", "DESTINATION_PASS"), ("acceptedPower116", "0" * 40), + ("stoppedCleanly", False), ("id", "native116/wrong"), ("status", "UNQUALIFIED")): + write_json(path, dict(original, **{key: value})) + changed = dict(row, source116ReceiptSha256=digest(path)) + with self.subTest(key=key), self.pinned(), self.saved_format(), self.assertRaises(AssertionError): + sources.verify_source_receipt(changed) + + def test_receipt_cannot_change_checkpoint_or_fixture_key(self): + _, _, row = self.receipt() + for key, value in (("sourceSnapshot", "migration-converted.json"), ("sourceFixtureKey", "wrong"), ("sourceKey", "legacy")): + with self.subTest(key=key), self.pinned(), self.saved_format(), self.assertRaises(AssertionError): + sources.verify_source_receipt(dict(row, **{key: value})) + + def test_changed_receipt_cannot_be_accepted_with_old_hash(self): + path, _, row = self.receipt() + path.write_text("{}") + with self.assertRaisesRegex(AssertionError, "source116 receipt"): + sources.verify_source_receipt(row) + + def test_failed_runner_keeps_fail_receipt_without_promoting_routes(self): + with self.pinned(), patch.object(sources, "run_source116", return_value=(1, [])), self.assertRaisesRegex(AssertionError, "protocol failed"): + sources.run(self.args) + bundle = json.loads((self.args.output / "source116-receipts.json").read_text()) + self.assertEqual(("FAIL", []), (bundle["status"], bundle["receipts"])) + receipt = json.loads((self.args.output / "native116-full/source116-receipt.json").read_text()) + self.assertEqual("FAIL", receipt["status"]) + self.assertFalse(receipt["stoppedCleanly"]) + routes = json.loads((self.args.output / "source116-routes.json").read_text())["routes"] + self.assertTrue(all(row["sourceStatus"].startswith("REQUIRES_APPROVED") for row in routes)) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_startup_diagnostics.py b/test-rig/migration/test_native117_startup_diagnostics.py new file mode 100644 index 00000000..0fd93db2 --- /dev/null +++ b/test-rig/migration/test_native117_startup_diagnostics.py @@ -0,0 +1,137 @@ +import json +from pathlib import Path +import subprocess +import tempfile +import unittest +from unittest.mock import Mock, patch + +import native117_code +import native117_startup_diagnostics as diagnostics + + +class StartupDiagnosticsTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.java = self.root / "java.exe" + self.jcmd = self.root / "jcmd.exe" + self.java.touch() + self.jcmd.touch() + self.process = Mock(pid=12345) + self.process.poll.return_value = None + self.failure = TimeoutError("original startup deadline") + + def test_live_process_gets_only_read_only_bounded_thread_dump(self): + with patch.object(diagnostics.subprocess, "run", return_value=Mock(returncode=0, stdout=b"raw dump")) as run: + result = diagnostics.observe(self.root, self.java, self.process, "boot-1", self.failure) + self.assertEqual(result["status"], "STARTUP_FAILURE_NOT_QUALIFIED") + self.assertFalse(result["startupRetry"]) + self.assertFalse(result["productionMutation"]) + self.assertEqual(result["threadDumpStatus"], "CAPTURED") + self.assertEqual(Path(result["threadDump"]).read_bytes(), b"raw dump") + self.assertEqual(run.call_args.args[0], [str(self.jcmd), "12345", "Thread.print", "-l"]) + self.assertEqual(run.call_args.kwargs["timeout"], 20) + self.process.kill.assert_not_called() + self.process.terminate.assert_not_called() + self.process.wait.assert_not_called() + + def test_exited_process_never_invokes_jcmd(self): + self.process.poll.return_value = 1 + with patch.object(diagnostics.subprocess, "run") as run: + result = diagnostics.observe(self.root, self.java, self.process, "exited", self.failure) + self.assertEqual(result["threadDumpStatus"], "PROCESS_ALREADY_EXITED") + run.assert_not_called() + + def test_missing_jcmd_never_substitutes_another_executable(self): + self.jcmd.unlink() + with patch.object(diagnostics.subprocess, "run") as run: + result = diagnostics.observe(self.root, self.java, self.process, "missing", self.failure) + self.assertEqual(result["threadDumpStatus"], "JCMD_UNAVAILABLE") + run.assert_not_called() + + def test_jcmd_timeout_retains_partial_bytes_without_retry(self): + with patch.object(diagnostics.subprocess, "run", side_effect=subprocess.TimeoutExpired(["jcmd"], 20, output=b"partial")) as run: + result = diagnostics.observe(self.root, self.java, self.process, "slow", self.failure) + self.assertEqual(result["threadDumpStatus"], "JCMD_TIMED_OUT") + self.assertEqual(Path(result["threadDump"]).read_bytes(), b"partial") + self.assertEqual(run.call_count, 1) + + def test_failed_jcmd_is_not_a_success_or_startup_pass(self): + with patch.object(diagnostics.subprocess, "run", return_value=Mock(returncode=1, stdout=b"attach failed")): + result = diagnostics.observe(self.root, self.java, self.process, "failed", self.failure) + self.assertEqual(result["threadDumpStatus"], "JCMD_FAILED") + self.assertEqual(result["threadDumpExitCode"], 1) + self.assertEqual(result["status"], "STARTUP_FAILURE_NOT_QUALIFIED") + + def test_existing_dump_is_never_overwritten(self): + path = self.root / "startup-thread-existing.txt" + path.write_bytes(b"retained") + with patch.object(diagnostics.subprocess, "run") as run: + with self.assertRaises(FileExistsError): + diagnostics.observe(self.root, self.java, self.process, "existing", self.failure) + self.assertEqual(path.read_bytes(), b"retained") + run.assert_not_called() + + def test_unsafe_label_fails_before_process_or_file_access(self): + with self.assertRaises(ValueError): + diagnostics.observe(self.root, self.java, self.process, "../escape", self.failure) + self.process.poll.assert_not_called() + + def test_primary_and_cleanup_failures_are_both_retained(self): + path = diagnostics.retain(self.root, "both", dict(failure="primary"), RuntimeError("cleanup")) + record = json.loads(path.read_text()) + self.assertEqual(record["failure"], "primary") + self.assertEqual(record["cleanupFailure"]["message"], "cleanup") + with self.assertRaises(FileExistsError): + diagnostics.retain(self.root, "both", dict(failure="replacement")) + + def test_retention_rejects_path_escape(self): + with self.assertRaises(ValueError): + diagnostics.retain(self.root, "../escape", {}) + + def launch_failure(self, failure, cleanup=None, observation=None): + logging = self.root / "libraries/net/minecraftforge/fmlloader/1.17.1-37.1.1/fmlloader-1.17.1-37.1.1.jar" + logging.parent.mkdir(parents=True, exist_ok=True) + logging.touch() + def close(server): + server.log.close() + if cleanup: + raise cleanup + with patch.object(native117_code.subprocess, "Popen", return_value=self.process) as launch, \ + patch.object(native117_code.threading, "Thread"), \ + patch.object(native117_code.NativeLaunch, "wait", side_effect=failure) as wait, \ + patch.object(native117_code.NativeLaunch, "close", autospec=True, side_effect=close) as stop, \ + patch.object(diagnostics, "observe", side_effect=observation, return_value={}) as observe, \ + patch.object(diagnostics, "retain") as retain: + try: + native117_code.NativeLaunch(self.root, self.java, "native-args", "startup") + except BaseException as error: + self.assertIs(error, failure) + else: + self.fail("Startup failure became success") + self.assertEqual(launch.call_count, 1) + wait.assert_called_once_with("Done (", 300) + self.assertEqual(stop.call_count, 1) + return observe, retain + + def test_cleanup_error_does_not_mask_original_timeout(self): + cleanup = RuntimeError("failed shutdown") + observe, retain = self.launch_failure(self.failure, cleanup) + self.assertEqual(observe.call_count, 1) + self.assertIs(retain.call_args.args[3], cleanup) + self.assertIn("Startup cleanup also failed", " ".join(getattr(self.failure, "__notes__", ()))) + + def test_diagnostic_error_does_not_mask_original_timeout(self): + observe, retain = self.launch_failure(self.failure, observation=RuntimeError("capture failed")) + retain.assert_not_called() + self.assertIn("Startup diagnostic observation failed", " ".join(getattr(self.failure, "__notes__", ()))) + + def test_user_cancellation_is_not_delayed_by_a_thread_dump(self): + observe, retain = self.launch_failure(KeyboardInterrupt()) + observe.assert_not_called() + retain.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_steam_control_audit.py b/test-rig/migration/test_native117_steam_control_audit.py new file mode 100644 index 00000000..31fef5c0 --- /dev/null +++ b/test-rig/migration/test_native117_steam_control_audit.py @@ -0,0 +1,22 @@ +"""Run every historical mutation control against the 1.17 adapter.""" + +from unittest.mock import patch +import unittest + +import native117_steam_control_audit as native +import test_steam_control_audit as historical_tests + + +class Native117SteamControlAuditTests(historical_tests.SteamControlAuditTests): + def setUp(self): + replacement = patch.object(historical_tests, 'audit', native) + replacement.start() + self.addCleanup(replacement.stop) + line = patch.object(historical_tests, 'LINE', '117') + line.start() + self.addCleanup(line.stop) + super().setUp() + + +if __name__ == '__main__': + unittest.main() diff --git a/test-rig/migration/test_native117_stopped_entities.py b/test-rig/migration/test_native117_stopped_entities.py new file mode 100644 index 00000000..7dc85723 --- /dev/null +++ b/test-rig/migration/test_native117_stopped_entities.py @@ -0,0 +1,158 @@ +"""Synthetic stop-file/retention controls, not native shutdown runtime proof.""" + +import copy +import hashlib +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from matrix import WORLD +import native117_stopped_entities as stopped +from test_native117_entity_storage import entity, entry, put + + +class StoppedEntitiesAuditTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.runtime = self.root / "runtime" + self.world = self.runtime / WORLD + self.world.mkdir(parents=True) + (self.runtime / "advantage-migration.allow").write_text("marked synthetic unit fixture") + self.doc = dict(format=1, status="CAPTURED", minecraftVersion="1.17.1", dataVersion=2730, + phase="FMLServerStoppingEvent", bootId="12345678-1234-1234-1234-123456789abc", + worldName="AdvantageWorksMigration", worldDirectory=str(self.world), entityCount=0, + enumeration="ServerLevel.getAllEntities", dimensions={}) + for key, (identity, _) in stopped.DIMENSIONS.items(): + self.doc["dimensions"][key] = dict(dimension=identity, worldDimensionKey=key, gameTime=10, entityCount=0, entities=[]) + self.index = 0 + + def stage(self, key="minecraft:overworld", words=(1, 2, 3, 4)): + actor = entity(words) + row = entry(actor, uuid=stopped.entity_storage._identity(actor), id="minecraft:chest_minecart", worldDimensionKey=key) + record = self.doc["dimensions"][key] + record["entities"].append(row) + record["entityCount"] += 1 + self.doc["entityCount"] += 1 + put(self.world / stopped.DIMENSIONS[key][1], (0, 0), [actor]) + + def audit(self): + self.index += 1 + self.evidence = self.root / ("evidence-" + str(self.index)) + self.source = self.runtime / stopped.SNAPSHOT + self.source.write_text(json.dumps(self.doc), encoding="utf-8") + return stopped.audit_stopped(self.runtime, self.evidence) + + def test_retains_exact_stopped_bytes_and_uses_actual_raw_entity_reader(self): + self.stage() + before = copy.deepcopy(self.doc) + result = self.audit() + data = self.source.read_bytes() + self.assertEqual("PASS", result["status"]) + self.assertEqual(data, (self.evidence / stopped.SNAPSHOT).read_bytes()) + self.assertEqual(hashlib.sha256(data).hexdigest().upper(), result["snapshotSha256"]) + self.assertEqual((1, 1, 1), (result["auditedDimensions"], result["snapshotRecords"], result["matchedEntities"])) + self.assertEqual("SKIP_EMPTY_STOP_SNAPSHOT", result["dimensions"]["minecraft:the_nether"]["status"]) + self.assertEqual(before, self.doc) + + def test_all_three_nonempty_dimensions_are_audited(self): + for index, key in enumerate(stopped.DIMENSIONS): + self.stage(key, (index + 1, 2, 3, 4)) + self.assertEqual(3, self.audit()["auditedDimensions"]) + + def test_all_empty_is_not_an_entity_proof_pass(self): + result = self.audit() + self.assertEqual("NO_NONEMPTY_DIMENSIONS", result["status"]) + self.assertEqual(0, result["matchedEntities"]) + + def test_pending_failed_and_live_phase_records_fail(self): + for key, value in (("status", "PENDING_STOP"), ("status", "FAILED"), ("phase", "live-snapshot"), + ("dataVersion", 2724), ("dataVersion", True), ("enumeration", "fixture-projection")): + before = copy.deepcopy(self.doc) + self.doc[key] = value + with self.subTest(key=key, value=value), self.assertRaises(ValueError): + self.audit() + self.doc = before + + def test_missing_dimension_wrong_identity_and_counts_fail(self): + baseline = copy.deepcopy(self.doc) + self.doc["dimensions"].pop("minecraft:the_end") + with self.assertRaises(ValueError): self.audit() + self.doc = copy.deepcopy(baseline) + self.doc["dimensions"]["minecraft:the_nether"]["dimension"] = 0 + with self.assertRaises(ValueError): self.audit() + self.doc = copy.deepcopy(baseline) + self.doc["entityCount"] = 1 + with self.assertRaises(AssertionError): self.audit() + self.doc = copy.deepcopy(baseline) + self.doc["dimensions"]["minecraft:overworld"]["entityCount"] = 1 + with self.assertRaises(ValueError): self.audit() + + def test_cross_dimension_duplicate_uuid_is_not_ignored(self): + self.stage() + self.stage("minecraft:the_nether") + with self.assertRaisesRegex(AssertionError, "across dimensions"): + self.audit() + + def test_wrong_world_and_malformed_boot_identity_fail(self): + original = self.doc["worldDirectory"] + self.doc["worldDirectory"] = str(self.root / "unrelated-world") + with self.assertRaisesRegex(AssertionError, "another world"): + self.audit() + self.doc["worldDirectory"] = original + self.doc["bootId"] = "not-a-uuid" + with self.assertRaises(ValueError): self.audit() + + def test_changed_raw_inventory_fails_without_a_tick_drift_allowance(self): + self.stage() + changed = entity() + changed["Items"][0]["Count"] = type(changed["Items"][0]["Count"])(6) + put(self.world, (0, 0), [changed]) + with self.assertRaisesRegex(AssertionError, "Complete saved entity NBT"): + self.audit() + self.assertEqual("FAIL", json.loads((self.evidence / "receipt.json").read_text())["status"]) + + def test_immutable_copy_and_existing_evidence_cannot_be_overwritten(self): + self.stage() + self.audit() + retained = (self.evidence / stopped.SNAPSHOT).read_bytes() + self.source.write_text("new boot overwrote runtime output") + self.assertEqual(retained, (self.evidence / stopped.SNAPSHOT).read_bytes()) + with self.assertRaises(FileExistsError): + stopped.audit_stopped(self.runtime, self.evidence) + + def test_stop_file_change_during_audit_fails(self): + self.stage() + original = stopped.entity_storage.audit + + def overwrite(*args): + result = original(*args) + self.source.write_text("reused by another boot") + return result + + with patch.object(stopped.entity_storage, "audit", side_effect=overwrite), \ + self.assertRaisesRegex(AssertionError, "changed during"): + self.audit() + + def test_runtime_marker_and_matching_native_type_attestation_are_required(self): + self.stage() + self.doc["dimensions"]["minecraft:overworld"]["entities"][0]["id"] = "minecraft:pig" + with self.assertRaisesRegex(AssertionError, "type attestation"): + self.audit() + (self.runtime / "advantage-migration.allow").unlink() + with self.assertRaisesRegex(ValueError, "not explicitly marked"): + self.audit() + + def test_duplicate_json_keys_cannot_turn_pending_into_captured(self): + self.source = self.runtime / stopped.SNAPSHOT + data = json.dumps(self.doc) + self.source.write_text('{"status":"PENDING_STOP",' + data[1:]) + with self.assertRaisesRegex(ValueError, "Duplicate stopping snapshot JSON key"): + stopped.audit_stopped(self.runtime, self.root / "duplicate-json-evidence") + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_tagged_commands.py b/test-rig/migration/test_native117_tagged_commands.py new file mode 100644 index 00000000..e53b7470 --- /dev/null +++ b/test-rig/migration/test_native117_tagged_commands.py @@ -0,0 +1,26 @@ +import unittest + +from native117_commands import native + + +class TaggedFixtureCommands(unittest.TestCase): + def test_typed_witness_payload_is_preserved_exactly(self): + items = ( + 'minecraft:dirt{AdvWorksWitness:"conveyor-north"}', + 'minecraft:diamond{Nested:{Items:[{id:"minecraft:stone",Count:2b}]}}', + 'minecraft:stone{display:{Name:\'{"text":"A test witness"}\'}}', + ) + for item in items: + with self.subTest(item=item): + self.assertEqual('item replace block -14 8 230 container.26 with ' + item + ' 7', + native('replaceitem block -14 8 230 container.26 ' + item + ' 7')) + + def test_malformed_payload_is_not_forwarded_to_the_server(self): + for item in ('minecraft:dirt{Count:2b', 'minecraft:dirt{Count:}', + 'minecraft:dirt{Count:2b} unexpected', 'minecraft:dirt[Count=2]'): + with self.subTest(item=item), self.assertRaises(ValueError): + native('replaceitem block 1 2 3 container.0 ' + item + ' 7') + + +if __name__ == '__main__': + unittest.main() diff --git a/test-rig/migration/test_native117_ticket_networks.py b/test-rig/migration/test_native117_ticket_networks.py new file mode 100644 index 00000000..4fa094bf --- /dev/null +++ b/test-rig/migration/test_native117_ticket_networks.py @@ -0,0 +1,98 @@ +"""Native layout safety and ticket-halo geometry, without accepting oracle failures.""" + +import copy +import json +import unittest +from unittest.mock import patch + +import extended_lifecycle +from native117_partial import NativePartialRun +from native117_ticket_networks import (NativeTicketNetworkRun, bridge_fixtures, extended_fixtures, + fill_commands, halo_chunks, linear_fixtures) + + +class NativeTicketFixtureTests(unittest.TestCase): + def test_historical_drill_fixtures_are_identical(self): + blocks = {"steamadvantage:steam_drill": "basesteam:steam_drill", + "electricadvantage:electric_drill": "baseelectric:electric_drill"} + self.assertEqual([dict(f, block=blocks[f["block"]]) for f in extended_lifecycle.fixtures() if "route" in f], + extended_fixtures()[:18]) + + def test_all_native_bounds_and_paths_are_contained_and_do_not_overlap(self): + for definition in (linear_fixtures(), bridge_fixtures(), extended_fixtures()): + for index, f in enumerate(definition): + for p in f["witnesses"]: + self.assertTrue(all(a <= n <= b for a, n, b in zip(f["min"], p, f["max"]))) + for other in definition[index + 1:]: + self.assertTrue(any(f["max"][a] < other["min"][a] or other["max"][a] < f["min"][a] for a in range(3))) + + def test_retained_ticket_is_outside_selected_halo(self): + for f in linear_fixtures(): + for group, other in (("supply", "machine"), ("machine", "supply")): + self.assertTrue(set(map(tuple, f["groups"][group + "halo"])).isdisjoint(map(tuple, f["groups"][other]))) + core = f["groups"][group][0] + all_chunks = {(x, z) for x in range(f["min"][0] >> 4, (f["max"][0] >> 4) + 1) + for z in range(f["min"][2] >> 4, (f["max"][2] >> 4) + 1)} + retained = all_chunks - set(map(tuple, f["groups"][group + "halo"])) + self.assertTrue(all(max(abs(x-core[0]), abs(z-core[1])) > 2 for x, z in retained)) + + def test_bridge_halo_does_not_remove_endpoint_ticket(self): + for f in bridge_fixtures(): + self.assertTrue(set(map(tuple, f["groups"]["bridgehalo"])).isdisjoint(map(tuple, f["groups"]["endpoints"]))) + self.assertEqual(f["groups"]["bridgehalo"], halo_chunks(f, "bridge")) + + def test_linear_path_is_native_neighbor_connected_and_128_blocks_long(self): + for f in linear_fixtures(): + path = [f["witnesses"][0], *f["pipes"], f["witnesses"][4]] + self.assertEqual(len(path), 129) + self.assertTrue(all(sum(abs(x-y) for x, y in zip(a, b)) == 1 for a, b in zip(path, path[1:]))) + self.assertEqual(len(set(map(tuple, f["witnesses"]))), len(f["witnesses"])) + + def test_bridge_is_one_connected_u_without_shortcut(self): + for f in bridge_fixtures(): + path = [f["witnesses"][0], *f["pipes"], f["witnesses"][1]] + self.assertTrue(all(sum(abs(x-y) for x, y in zip(a, b)) == 1 for a, b in zip(path, path[1:]))) + self.assertEqual(len(set(map(tuple, path))), len(path)) + self.assertEqual(len(f["pipes"]), 259) + + def test_clearing_is_split_and_exactly_confined_to_declared_bounds(self): + for f in linear_fixtures() + bridge_fixtures(): + command = "fill " + " ".join(map(str, f["min"] + f["max"])) + " minecraft:air replace" + covered = set() + for piece in fill_commands([f], command): + p = piece.split() + low, high = list(map(int, p[1:4])), list(map(int, p[4:7])) + volume = 1 + for a, b in zip(low, high): + volume *= b - a + 1 + self.assertLessEqual(volume, 32768) + cells = {(x, z) for x in range(low[0], high[0] + 1) for z in range(low[2], high[2] + 1)} + self.assertTrue(covered.isdisjoint(cells)) + covered |= cells + self.assertEqual(len(covered), 160 * 32) + outside = command.replace(str(f["min"][0]), "0", 1) + self.assertIsNone(fill_commands([f], outside)) + + def test_release_observes_before_releasing_halo_then_core(self): + runner = NativeTicketNetworkRun(None, None, None, linear_fixtures()) + before = {"raw": "before"} + with patch.object(runner, "_raw_probe", side_effect=[before, {}, {"raw": "after"}]) as raw: + runner.probe("release-supply", "released") + self.assertEqual([call.args[0] for call in raw.call_args_list], + ["snapshot", "release-supplyhalo", "release-supply"]) + self.assertIs(runner._released_groups["supply"]["before"], before) + + def test_load_restores_only_requested_halo_before_core_readiness(self): + runner = NativeTicketNetworkRun(None, None, None, linear_fixtures()) + runner._released_groups["supply"] = {} + runner._released_groups["machine"] = {} + with patch.object(runner, "_raw_probe") as raw, patch.object(runner, "_load_ready", return_value={}) as ready: + runner.probe("load-supply", "loaded") + self.assertEqual(raw.call_args.args[0], "load-supplyhalo") + ready.assert_called_once_with("load-supply", "loaded") + self.assertNotIn("supply", runner._released_groups) + self.assertIn("machine", runner._released_groups) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_ticket_precondition.py b/test-rig/migration/test_native117_ticket_precondition.py new file mode 100644 index 00000000..719d033a --- /dev/null +++ b/test-rig/migration/test_native117_ticket_precondition.py @@ -0,0 +1,237 @@ +"""Pure, worldless ticket-model controls; no synthetic records are live evidence.""" + +import copy +import json +from pathlib import Path +import tempfile +import unittest + +from native117_ticket_precondition import (FORCED_LEVEL, FULL_LEVEL, HOLDER_RADIUS, + LAST_HOLDER_LEVEL, isolation_precondition, read_ticket_snapshot, require_isolation) + + +DIMENSION = "minecraft:overworld" + + +def ticket(pos, level=31, kind="forced", active=True, **extra): + return dict(pos=list(pos), type=kind, level=level, active=active, **extra) + + +def snapshot(records): + return dict(format=1, dimension=DIMENSION, ticketMapComplete=True, + ticketGeometry=dict(fullLevel=33, entityTickingLevel=31, maxDistance=11, + lastHolderLevel=44, holderRadius=13, + observedForcedLevels=sorted({r["level"] for r in records + if r["active"] and r["type"] == "forced"})), + tickets=records) + + +def planned_release(definition, groups, halo=False): + """Model the source fixture's native load/release operations, not a live map.""" + all_chunks = {(x, z) for f in definition + for x in range(f["min"][0] >> 4, (f["max"][0] >> 4) + 1) + for z in range(f["min"][2] >> 4, (f["max"][2] >> 4) + 1)} + removed, witnesses = set(), set() + for f in definition: + for group in groups: + if group not in f.get("groups", {}): + continue + cores = set(map(tuple, f["groups"][group])) + witnesses.update(cores) + removed.update(map(tuple, f["groups"][group + "halo"]) if halo else cores) + return snapshot([ticket(p) for p in sorted(all_chunks - removed)]), sorted(witnesses) + + +class TicketPreconditionTests(unittest.TestCase): + def test_verified_native_thresholds(self): + self.assertEqual((FORCED_LEVEL, FULL_LEVEL, LAST_HOLDER_LEVEL, HOLDER_RADIUS), (31, 33, 44, 13)) + + def test_original_adjacent_z_layout_is_red_for_all_24_partial_and_12_order_cases(self): + from extended_lifecycle import fixtures + partial_failures, order_failures = 0, 0 + for profile in ("fresh117", "original110-chained113-native117"): + for group in ("machine", "supply", "supply", "machine"): + value, witnesses = planned_release(fixtures(), [group]) + result = isolation_precondition(value, witnesses, DIMENSION) + with self.subTest(profile=profile, partial=group): + self.assertEqual(result["status"], "BLOCKED") + self.assertEqual({tuple(r["witness"]) for r in result["blockers"]}, set(witnesses)) + with self.assertRaisesRegex(AssertionError, "isolation precondition failed"): + require_isolation(value, witnesses, DIMENSION) + partial_failures += len(witnesses) + for groups in (("machine", "supply"), ("supply", "machine")): + value, witnesses = planned_release(fixtures(), groups) + result = isolation_precondition(value, witnesses, DIMENSION) + with self.subTest(profile=profile, order=groups): + self.assertEqual(result["status"], "BLOCKED") + # The unremoved z=129 row alone is sufficient to block every endpoint. + self.assertEqual({tuple(r["witness"]) for r in result["blockers"] + if r["ticketPos"][1] == 129 and r["distance"] == 1}, set(witnesses)) + order_failures += len(witnesses) // 2 + self.assertEqual((partial_failures, order_failures), (24, 12)) + + def test_holder_separated_layout_is_green_globally_for_partial_and_both_orders(self): + from native117_holder_networks import extended_fixtures + for groups in (("supply",), ("machine",), ("supply", "machine"), ("machine", "supply")): + with self.subTest(groups=groups): + value, witnesses = planned_release(extended_fixtures(), groups, halo=True) + result = require_isolation(value, witnesses, DIMENSION) + self.assertEqual(result["status"], "ISOLATED") + self.assertEqual(result["scope"], "STATIC_TICKET_PRECONDITION_ONLY_NOT_RUNTIME_PASS") + + def test_holder_bridge_geometry_is_green(self): + from native117_holder_networks import bridge_fixtures + value, witnesses = planned_release(bridge_fixtures(), ["bridge"], halo=True) + self.assertEqual(require_isolation(value, witnesses, DIMENSION)["status"], "ISOLATED") + + def test_full_halo_only_is_not_sufficient_for_physical_holder_unload(self): + from native117_ticket_networks import linear_fixtures + value, witnesses = planned_release(linear_fixtures(), ["supply"], halo=True) + result = isolation_precondition(value, witnesses, DIMENSION) + self.assertEqual(result["status"], "BLOCKED") + self.assertTrue(any(r["effectiveLevel"] > 33 for r in result["blockers"])) + + def test_holder_boundary_13_inclusive_14_exclusive(self): + for distance, expected in ((12, "BLOCKED"), (13, "BLOCKED"), (14, "ISOLATED")): + with self.subTest(distance=distance): + self.assertEqual(isolation_precondition(snapshot([ticket([distance, 0])]), [[0, 0]], DIMENSION)["status"], expected) + + def test_full_boundary_is_distinct_from_holder_boundary(self): + for distance, expected in ((2, True), (3, False), (13, False)): + result = isolation_precondition(snapshot([ticket([distance, 0])]), [[0, 0]], DIMENSION) + self.assertEqual(result["blockers"][0]["fullDependency"], expected) + + def test_diagonal_distance_is_chebyshev_not_manhattan(self): + result = isolation_precondition(snapshot([ticket([13, -13])]), [[0, 0]], DIMENSION) + self.assertEqual(result["status"], "BLOCKED") + self.assertEqual(result["blockers"][0]["distance"], 13) + self.assertEqual(result["blockers"][0]["effectiveLevel"], 44) + + def test_negative_chunks_and_cross_origin_distance(self): + self.assertEqual(isolation_precondition(snapshot([ticket([-14, -14])]), [[-1, -1]], DIMENSION)["status"], "BLOCKED") + self.assertEqual(isolation_precondition(snapshot([ticket([-15, -15])]), [[-1, -1]], DIMENSION)["status"], "ISOLATED") + self.assertEqual(isolation_precondition(snapshot([ticket([-7, -7])]), [[6, 6]], DIMENSION)["status"], "BLOCKED") + + def test_other_active_ticket_types_and_levels_are_never_filtered(self): + for kind in ("player", "start", "portal", "unknown", "othermod:loader"): + for level in (0, 22, 31, 33, 44): + radius = 44 - level + with self.subTest(kind=kind, level=level): + self.assertEqual(isolation_precondition(snapshot([ticket([radius, -radius], level, kind)]), [[0, 0]], DIMENSION)["status"], "BLOCKED") + self.assertEqual(isolation_precondition(snapshot([ticket([radius + 1, 0], level, kind)]), [[0, 0]], DIMENSION)["status"], "ISOLATED") + + def test_foreign_fixture_ticket_can_block_an_otherwise_green_holder_layout(self): + from native117_holder_networks import linear_fixtures + value, witnesses = planned_release(linear_fixtures(), ["machine"], halo=True) + self.assertEqual(require_isolation(value, witnesses, DIMENSION)["status"], "ISOLATED") + value["tickets"].append(ticket(witnesses[0], 33, "unrelated_mod:loader")) + result = isolation_precondition(value, witnesses, DIMENSION) + self.assertEqual(result["status"], "BLOCKED") + self.assertTrue(any(r["ticketType"] == "unrelated_mod:loader" for r in result["blockers"])) + + def test_explicitly_inactive_tickets_do_not_influence_but_no_timeout_is_inferred(self): + self.assertEqual(isolation_precondition(snapshot([ticket([0, 0], active=False)]), [[0, 0]], DIMENSION)["status"], "ISOLATED") + self.assertEqual(isolation_precondition(snapshot([ticket([0, 0], 33, "unknown")]), [[0, 0]], DIMENSION)["status"], "BLOCKED") + + def test_force_ticks_false_does_not_disable_chunk_influence(self): + for force in (False, True): + self.assertEqual(isolation_precondition(snapshot([ticket([0, 0], forceTicks=force)]), [[0, 0]], DIMENSION)["status"], "BLOCKED") + + def test_all_blockers_are_reported_even_multiple_records_at_one_chunk(self): + records = [ticket([0, 0]), ticket([0, 0], 33, "portal"), ticket([100, 100])] + result = isolation_precondition(snapshot(records), [[0, 0]], DIMENSION) + self.assertEqual([r["ticketIndex"] for r in result["blockers"]], [0, 1]) + + def test_weaker_than_last_holder_ticket_is_not_a_blocker(self): + self.assertEqual(isolation_precondition(snapshot([ticket([0, 0], 45, "light")]), [[0, 0]], DIMENSION)["status"], "ISOLATED") + + def test_inputs_and_returned_positions_do_not_alias(self): + value, witnesses = snapshot([ticket([0, 0])]), [[1, 1]] + before = copy.deepcopy((value, witnesses)) + result = isolation_precondition(value, witnesses, DIMENSION) + result["blockers"][0]["ticketPos"][0] = 123 + result["witnesses"][0][0] = 456 + self.assertEqual((value, witnesses), before) + + def test_complete_empty_map_is_only_a_static_clear_precondition(self): + result = require_isolation(snapshot([]), [[0, 0]], DIMENSION) + self.assertEqual(result["status"], "ISOLATED") + self.assertNotIn("qualification", result) + + def test_incomplete_g_level_summary_is_rejected_not_invented_into_records(self): + value = snapshot([]) + del value["tickets"] + with self.assertRaises(ValueError): + isolation_precondition(value, [[0, 0]], DIMENSION) + + def test_malformed_and_incomplete_snapshot_metadata_fail_closed(self): + for field, bad in (("format", True), ("format", 2), ("ticketMapComplete", False), + ("ticketMapComplete", 1), ("dimension", "overworld"), + ("tickets", {}), ("ticketGeometry", {})): + value = snapshot([]) + value[field] = bad + with self.subTest(field=field, bad=bad), self.assertRaises(ValueError): + isolation_precondition(value, [[0, 0]], DIMENSION) + for field, bad in (("fullLevel", 32), ("lastHolderLevel", 43), ("holderRadius", 2), + ("entityTickingLevel", True), ("observedForcedLevels", [31])): + value = snapshot([]) + value["ticketGeometry"][field] = bad + with self.subTest(geometry=field), self.assertRaises(ValueError): + isolation_precondition(value, [[0, 0]], DIMENSION) + + def test_malformed_ticket_records_fail_closed_instead_of_being_skipped(self): + good = ticket([0, 0]) + malformed = [None, {}, {**good, "unexpected": 1}] + malformed += [{k: v for k, v in good.items() if k != missing} for missing in good] + malformed += [{**good, field: bad} for field, bad in ( + ("pos", [0]), ("pos", [False, 0]), ("pos", [0.0, 0]), + ("pos", [2 ** 31, 0]), ("pos", [0, -(2 ** 31) - 1]), + ("type", ""), ("type", None), ("level", -1), ("level", True), + ("level", 31.0), ("level", 2 ** 31), ("level", 32), + ("active", 1), ("forceTicks", 0))] + for record in malformed: + value = snapshot([]) + value["tickets"] = [record] + with self.subTest(record=record), self.assertRaises(ValueError): + isolation_precondition(value, [[0, 0]], DIMENSION) + + def test_forced_observation_must_match_active_ticket_records(self): + value = snapshot([ticket([20, 20])]) + for observed in ([], [True], [31, 31], [32]): + value["ticketGeometry"]["observedForcedLevels"] = observed + with self.subTest(observed=observed), self.assertRaises(ValueError): + isolation_precondition(value, [[0, 0]], DIMENSION) + + def test_invalid_witnesses_and_wrong_dimension_fail_closed(self): + for witnesses in ([], None, "0,0", [[0]], [[True, 0]], [[0, 0], [0, 0]]): + with self.subTest(witnesses=witnesses), self.assertRaises(ValueError): + isolation_precondition(snapshot([]), witnesses, DIMENSION) + with self.assertRaises(ValueError): + isolation_precondition(snapshot([]), [[0, 0]], "minecraft:the_nether") + for dimension in ("", "minecraft:", ":overworld", "Minecraft:overworld", "minecraft:a b", "a:b:c"): + with self.subTest(dimension=dimension), self.assertRaises(ValueError): + isolation_precondition(snapshot([]), [[0, 0]], dimension) + + def test_reader_preserves_explicit_global_ticket_records(self): + value = snapshot([ticket([-7, 3]), ticket([100, -100], 33, "foreign:loader")]) + with tempfile.TemporaryDirectory(prefix="native117-ticket-precondition-") as directory: + path = Path(directory) / "native-ticket-map.json" + path.write_text(json.dumps(value), encoding="utf-8") + self.assertEqual(read_ticket_snapshot(path), value) + + def test_reader_rejects_duplicate_json_fields_and_nonfinite_numbers(self): + encoded = json.dumps(snapshot([ticket([0, 0])])) + invalid = [encoded.replace('"format": 1', '"format": 1, "format": 1'), + encoded.replace('"level": 31', '"level": 31, "level": 45'), + encoded.replace('"level": 31', '"level": NaN'), + encoded.replace('"level": 31', '"level": Infinity'), '{'] + with tempfile.TemporaryDirectory(prefix="native117-ticket-precondition-") as directory: + path = Path(directory) / "malformed.json" + for text in invalid: + path.write_text(text, encoding="utf-8") + with self.subTest(text=text), self.assertRaises(ValueError): + read_ticket_snapshot(path) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native117_transformer_order.py b/test-rig/migration/test_native117_transformer_order.py new file mode 100644 index 00000000..e4fda355 --- /dev/null +++ b/test-rig/migration/test_native117_transformer_order.py @@ -0,0 +1,340 @@ +"""Strict observer negative controls; synthetic fixtures here are unit data, not runtime proof.""" + +import unittest +import copy +import json +from pathlib import Path +import tempfile +from unittest.mock import patch + +import transformer_order as historical +import native117_audit_order as audit +import native117_transformer_order as native +import native117_observation_plan as plans +from matrix import digest, write_json +from native117_code import INVENTORY, code_files, verify_code + + +class NativeTransformerOrderAdapterTest(unittest.TestCase): + def test_audit_uses_native_actor_decoder_before_strict_replay(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + for name, value in { + "results.json": {"original110/natural": {}}, + "source-fingerprints.json": {}, + "probe-build.json": {"patched_sha256": "hash"}, + "summary.json": {"patchedLibrarySha256": "hash"}, + }.items(): + (root / name).write_text(json.dumps(value)) + mixed = root / "original110-mixed-input" + mixed.mkdir() + (mixed / "semantic-source.json").write_text("{}") + with patch.object(audit, "digest", return_value="hash"), \ + patch.object(historical, "assert_staged"), \ + patch.object(audit, "capture_actors", create=True, + side_effect=RuntimeError("native decoder reached")) as capture, \ + patch.object(audit.actors, "capture", + side_effect=AssertionError("historical decoder bypasses native UUIDs")): + with self.assertRaisesRegex(RuntimeError, "native decoder reached"): + audit.replay(root) + runtime = root / "original110-natural" + capture.assert_called_once_with(runtime, "converted", runtime / "ordering-evidence/converted") + + def test_packaged_platform_is_not_a_dev_cache_or_historical_guess(self): + self.assertIsNot(historical.order_evidence.__code__, native.native_order_evidence.__code__) + self.assertEqual("net/minecraft/world/level/storage/LevelStorageSource", native.SUFFIXES["world_info"][0]) + self.assertEqual("net/minecraft/world/level/chunk/storage/ChunkStorage", native.SUFFIXES["chunk_status"][0]) + self.assertEqual("net/minecraft/server/level/WorldGenRegion", native.SUFFIXES["worldgen_guard"][0]) + self.assertEqual("coremods-5.0.1.jar", native.COREMODS.name) + self.assertEqual(native.COREMODS.as_posix(), plans.COREMODS) + self.assertEqual(("asm-9.1.jar", "asm-tree-9.1.jar", "modlauncher-9.0.7.jar", "gson-2.8.0.jar"), + tuple(path.rsplit("/", 1)[1] for path in native.BRIDGE_LIBRARIES)) + self.assertEqual("native117-transformer-order-probe", plans.BRIDGE_SOURCE.parent.name) + self.assertNotEqual(native.GUARD_METHODS["bm"], native.GUARD_METHODS["pa"]) + + +def hook(mod, name, target, descriptor): + return dict(method="nativeMethod" + target, index=0, owner=native.OWNERS[mod], name=name, descriptor=descriptor) + + +class NativeOrderEvidenceNegativeControls(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + names = [mod + "_legacy_" + suffix for suffix in native.SUFFIXES for mod in ("basemetals", "poweradvantage")] + names += [native.ENTITY_NAME] + self.events = [dict(event="registration", bridge="native117-v1", mode="bm-first", natural=names[:], forced=names[:])] + for suffix, (class_name, _) in native.SUFFIXES.items(): + first, final = [], [] + for mod in ("bm", "pa"): + if suffix == "world_info": + calls = [hook(mod, "prepareLegacyWorld", native.WORLD_METHOD, "(Ljava/io/File;)V")] + elif suffix == "chunk_status": + calls = [hook(mod, "prepareLegacyChunk", native.CHUNK_METHOD, "(" + native.NBT + ")V"), + hook(mod, "finalizeLegacyChunk", native.CHUNK_METHOD, native.ENTITY_METHOD)] + if mod == "pa": + calls += [hook(mod, "prepareLegacyEntities", native.CHUNK_METHOD, "(" + native.NBT + ")V"), + hook(mod, "copyChunkForMigration", "(Lnet/minecraft/world/level/ChunkPos;)" + native.NBT, + native.ENTITY_METHOD)] + else: + call = ("(Lnet/minecraft/server/level/WorldGenRegion;Lnet/minecraft/core/BlockPos;)Z" + if mod == "bm" else native.GUARD_METHODS["bm"]) + calls = [hook(mod, "shouldBlockWorldgenWrite", native.GUARD_METHODS[mod], call)] + if mod == "bm": + first = calls + else: + # Prefixes prepend, return hooks append, and the two guards occupy different methods. + final = [entry for entry in calls if entry["name"] != "finalizeLegacyChunk"] + first + final += [entry for entry in calls if entry["name"] == "finalizeLegacyChunk"] + if suffix == "chunk_status": + final = [calls[0], first[0], calls[2], calls[3], first[1], calls[1]] + self.events += [self.event(class_name, "basemetals_legacy_" + suffix, [], first), + self.event(class_name, "poweradvantage_legacy_" + suffix, first, final)] + entity_hooks = [hook("pa", name, native.ENTITY_METHOD, native.ENTITY_METHOD) + for name in ("prepareEntityChunkForMigration", "finalizeLegacyChunk")] + self.events.append(self.event(native.ENTITY_CLASS, native.ENTITY_NAME, [], entity_hooks)) + + @staticmethod + def event(class_name, name, before, after): + return dict(event="application", **{"class": class_name}, transformer=name, + delegate="net.minecraftforge.coremod.transformer.CoreModClassTransformer", + labels=["basemetals" if name.startswith("basemetals_") else "poweradvantage", name], + before=copy.deepcopy(before), after=copy.deepcopy(after)) + + def save(self): + for index, event in enumerate(self.events, 1): + event["sequence"] = index + if event["event"] != "application": + continue + for phase in ("before", "after"): + path = self.root / "transformer-order-bytecode" / (str(index) + "-" + phase + ".class") + path.parent.mkdir(exist_ok=True) + # Deliberately JSON unit data. Only the mocked decoder accepts these bytes. + write_json(path, {"class": event["class"], "hooks": event[phase]}) + event[phase + "Dump"] = path.relative_to(self.root).as_posix() + event[phase + "Sha256"] = digest(path) + (self.root / "transformer-order-events.jsonl").write_text("\n".join(json.dumps(event) for event in self.events)) + + def prove(self, mode="bm-first"): + self.save() + with patch.object(native, "inspection_command", return_value=["unit-decoder-only"]), \ + patch.object(native, "inspect_dump", side_effect=lambda command, path: json.loads(path.read_text())): + return native.native_order_evidence(self.root, mode) + + def test_true_pairs_and_unpaired_entity_storage_are_distinguished(self): + proof = self.prove() + self.assertEqual("PASS", proof["status"]) + self.assertFalse(proof["classes"]["separate_entity_storage"]["paired"]) + self.assertEqual([native.ENTITY_NAME], proof["classes"]["separate_entity_storage"]["application_order"]) + guard = proof["classes"]["worldgen_guard"]["emitted_hook_order"]["shouldBlockWorldgenWrite"] + self.assertEqual([["bm"], ["pa"]], sorted(guard.values())) + + def test_natural_order_uses_the_observed_natural_registration(self): + self.events[0]["mode"] = "natural" + self.assertEqual("PASS", self.prove("natural")["status"]) + + def test_reverse_order_has_reversed_prefixes_and_appended_returns(self): + self.events[0]["mode"] = "pa-first" + for index, suffix in enumerate(native.SUFFIXES): + bm_index = index * 2 + names = self.events[0]["forced"] + names[bm_index], names[bm_index + 1] = names[bm_index + 1], names[bm_index] + offset = bm_index + 1 + bm, pa = self.events[offset:offset + 2] + all_hooks = copy.deepcopy(pa["after"]) + pa["before"] = [] + pa["after"] = [entry for entry in all_hooks if native.hook_owner(entry) == "pa"] + bm["before"] = copy.deepcopy(pa["after"]) + if suffix == "world_info": + bm["after"] = list(reversed(all_hooks)) + elif suffix == "chunk_status": + def entry(mod, name): + return next(value for value in all_hooks if native.hook_owner(value) == mod and value["name"] == name) + bm["after"] = [entry("bm", "prepareLegacyChunk"), entry("pa", "prepareLegacyChunk"), + entry("pa", "prepareLegacyEntities"), entry("pa", "copyChunkForMigration"), + entry("pa", "finalizeLegacyChunk"), entry("bm", "finalizeLegacyChunk")] + else: + bm["after"] = all_hooks + self.events[offset:offset + 2] = [pa, bm] + self.assertEqual("PASS", self.prove("pa-first")["status"]) + + def test_registration_alone_fails(self): + self.events = self.events[:1] + with self.assertRaisesRegex(AssertionError, "class applications"): + self.prove() + + def test_wrong_application_order_fails(self): + self.events[1], self.events[2] = self.events[2], self.events[1] + with self.assertRaisesRegex(AssertionError, "application order"): + self.prove() + + def test_wrong_forced_order_fails(self): + self.events[0]["forced"] = list(reversed(self.events[0]["forced"])) + with self.assertRaisesRegex(AssertionError, "requested real pairs"): + self.prove() + + def test_missing_injection_fails(self): + self.events[1]["after"] = [] + self.events[2]["before"] = [] + with self.assertRaisesRegex(AssertionError, "injection"): + self.prove() + + def test_duplicate_callback_fails(self): + self.events.append(copy.deepcopy(self.events[1])) + with self.assertRaisesRegex(AssertionError, "application order"): + self.prove() + + def test_wrong_emitted_order_fails(self): + self.events[2]["after"].reverse() + with self.assertRaisesRegex(AssertionError, "emitted hook order"): + self.prove() + + def test_disconnected_nodes_fail(self): + self.events[2]["before"] = [] + with self.assertRaisesRegex(AssertionError, "same evolving class"): + self.prove() + + def test_duplicate_injection_fails(self): + self.events[1]["after"].append(copy.deepcopy(self.events[1]["after"][0])) + self.events[2]["before"] = copy.deepcopy(self.events[1]["after"]) + with self.assertRaisesRegex(AssertionError, "injection"): + self.prove() + + def test_entity_storage_is_required(self): + self.events.pop() + with self.assertRaisesRegex(AssertionError, "class applications"): + self.prove() + + def test_entity_hook_noop_fails(self): + self.events[-1]["after"] = [] + with self.assertRaisesRegex(AssertionError, "injection"): + self.prove() + + def test_invented_entity_partner_fails(self): + self.events[-1]["after"].append(hook("bm", "prepareEntityChunkForMigration", native.ENTITY_METHOD, native.ENTITY_METHOD)) + with self.assertRaisesRegex(AssertionError, "Invented Base Metals"): + self.prove() + + def test_missing_real_delegate_identity_fails(self): + self.events[1]["delegate"] = "SyntheticTransformer" + with self.assertRaisesRegex(AssertionError, "real CoreMods"): + self.prove() + + def test_wrong_native_guard_method_fails(self): + self.events[5]["after"][0]["method"] = "wrong" + native.GUARD_METHODS["pa"] + self.events[6]["before"] = copy.deepcopy(self.events[5]["after"]) + with self.assertRaisesRegex(AssertionError, "descriptor or target"): + self.prove() + + def test_wrong_nbt_descriptor_fails(self): + self.events[-1]["after"][0]["descriptor"] = "()V" + with self.assertRaisesRegex(AssertionError, "descriptor or target"): + self.prove() + + def test_entity_preparation_cannot_jump_ahead_of_either_chunk_hook(self): + hooks = self.events[4]["after"] + entity = next(entry for entry in hooks if entry["name"] == "prepareLegacyEntities") + hooks.remove(entity) + hooks.insert(0, entity) + with self.assertRaisesRegex(AssertionError, "must follow both chunk"): + self.prove() + + def test_json_claims_cannot_replace_native_bytecode_or_custody(self): + self.save() + with self.assertRaises(FileNotFoundError): + native.native_order_evidence(self.root, "bm-first") + with patch.object(native, "inspection_command", return_value=["unit-decoder-only"]), \ + patch.object(native, "inspect_dump", return_value={"class": native.ENTITY_CLASS, "hooks": []}): + with self.assertRaisesRegex(AssertionError, "actual bytecode"): + native.native_order_evidence(self.root, "bm-first") + + def test_changed_bytecode_fails(self): + self.save() + (self.root / self.events[1]["afterDump"]).write_bytes(b"changed") + with patch.object(native, "inspection_command", return_value=["unit-decoder-only"]), \ + patch.object(native, "inspect_dump", side_effect=lambda command, path: json.loads(path.read_text())): + with self.assertRaisesRegex(AssertionError, "hash changed"): + native.native_order_evidence(self.root, "bm-first") + + def test_no_registration_sequence_replay(self): + self.save() + self.events[-1]["sequence"] = 1 + (self.root / "transformer-order-events.jsonl").write_text("\n".join(json.dumps(event) for event in self.events)) + with self.assertRaisesRegex(AssertionError, "unordered"): + native.native_order_evidence(self.root, "bm-first") + + +class NativeObservationPlanControls(unittest.TestCase): + def setUp(self): + import zipfile + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.seed = self.root / "seed" + original = self.seed / plans.COREMODS + original.parent.mkdir(parents=True) + self.observer = self.root / "observer.jar" + with zipfile.ZipFile(original, "w") as archive: + archive.writestr(historical.ENGINE, b"original-engine-unit-fixture") + archive.writestr("unrelated", b"unaltered") + with zipfile.ZipFile(self.observer, "w") as archive: + archive.writestr(historical.ENGINE, b"observed-engine-unit-fixture") + archive.writestr("unrelated", b"unaltered") + for name in ("TransformerOrderBridge.class", "TransformerOrderBridge$Observed.class"): + archive.writestr("qualification/order/" + name, b"unit-fixture-not-runtime-proof") + self.dependency = self.seed / "libraries/asm-unit.jar" + self.dependency.write_bytes(b"unit-dependency") + write_json(self.seed / INVENTORY, {"files": code_files(self.seed), "format": 1, "minecraft": "1.17.1", "forge": "37.1.1"}) + self.baseline = digest(self.seed / INVENTORY) + self.proof = self.root / "build.json" + write_json(self.proof, dict(historical.validate_patch(original, self.observer), + sourceSha256=digest(plans.BRIDGE_SOURCE), dependencies={str(self.dependency): digest(self.dependency)})) + self.plan = self.root / "plan.json" + write_json(self.plan, plans.make_plan(self.seed, self.baseline, self.observer, digest(self.observer), + self.proof, digest(self.proof))) + self.sha = digest(self.plan) + + def test_observer_replaces_only_coremods_with_original_gate_negative_control(self): + runtime = self.root / "runtime" + proof = plans.stage_probe(runtime, self.plan, self.sha) + self.assertEqual("PASS_CODE_CUSTODY_ONLY_NO_MC", proof["status"]) + self.assertEqual("REJECTED_AS_EXPECTED", proof["baselineGateBeforeReseal"]) + self.assertEqual([plans.COREMODS], proof["changedPaths"]) + self.assertEqual(self.baseline, digest(self.seed / INVENTORY)) + verify_code(self.seed) + verify_code(runtime) + self.assertEqual(proof, plans.audit_runtime(runtime, self.plan, self.sha)) + + def test_added_code_after_reseal_fails(self): + runtime = self.root / "runtime" + plans.stage_probe(runtime, self.plan, self.sha) + (runtime / "rogue.jar").write_bytes(b"unlisted-code") + with self.assertRaisesRegex(AssertionError, "exact sealed inventory"): + plans.audit_runtime(runtime, self.plan, self.sha) + + def test_changed_original_seed_fails(self): + self.dependency.write_bytes(b"changed-dependency") + with self.assertRaisesRegex(AssertionError, "exact sealed inventory"): + plans.load_plan(self.plan, self.sha) + + def test_changed_observer_fails(self): + self.observer.write_bytes(b"changed-observer") + with self.assertRaisesRegex(AssertionError, "pinned observation input"): + plans.load_plan(self.plan, self.sha) + + def test_protected_seed_cannot_be_a_staging_target(self): + with self.assertRaisesRegex(ValueError, "overlaps a protected input"): + plans.stage_probe(self.seed / "runtime", self.plan, self.sha) + + def test_wrong_runtime_metadata_and_unknown_inventory_keys_fail(self): + inventory = plans.read(self.seed / INVENTORY) + for key, value in (("minecraft", "1.16.5"), ("forge", "37.1.2"), ("format", True), ("unexpected", "ignored")): + changed = copy.deepcopy(inventory) + changed[key] = value + with self.subTest(key=key), self.assertRaisesRegex(ValueError, "exact nonempty code inventory"): + plans.validate_inventory(changed) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_recipe_unlocks.py b/test-rig/migration/test_recipe_unlocks.py index f10bc2ea..e33c164f 100644 --- a/test-rig/migration/test_recipe_unlocks.py +++ b/test-rig/migration/test_recipe_unlocks.py @@ -1,26 +1,56 @@ import unittest from collections import Counter +from io import BytesIO +from pathlib import Path +import subprocess +from tempfile import TemporaryDirectory +from unittest.mock import patch +from zipfile import ZipFile -from recipe_unlocks import MATERIALS, fixture +import recipe_unlocks +from recipe_unlocks import MATERIALS + + +SOURCES = ( + ("PowerAdvantage", "poweradvantage", "d4ebfea7b64d9b1c56b5fa3b27ca9f53ba38d1dd"), + ("SteamAdvantage/SteamAdvantage", "steamadvantage", "9aa16f3c346dd8684993a88168ea281dbcf119e2"), + ("ElectricAdvantage/ElectricAdvantage", "electricadvantage", "6b674e3fdba5f0296d8074adbb630bd6b1c5b62d"), +) class RecipeUnlockRunnerTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + source = recipe_unlocks.POWER.parents[1] / "PA113" + folder = TemporaryDirectory() + cls.addClassCleanup(folder.cleanup) + destination = Path(folder.name) + for path, mod, commit in SOURCES: + data = "src/main/resources/data/" + mod + archive = subprocess.check_output(["git", "-C", str(source / path), + "archive", "--format=zip", commit, data + "/recipes", data + "/advancements/recipes"]) + with ZipFile(BytesIO(archive)) as files: + files.extractall(destination / path) + worktree = patch.object(recipe_unlocks, "POWER", destination / "PowerAdvantage") + worktree.start() + cls.addClassCleanup(worktree.stop) + def test_fixture_covers_crafting_and_smelting_but_not_crushing(self): - recipes = fixture()["recipes"] + recipes = recipe_unlocks.fixture()["recipes"] self.assertEqual(Counter(recipe["id"].split(":")[0] for recipe in recipes), {"poweradvantage": 35, "steamadvantage": 24, "electricadvantage": 50}) self.assertEqual(109, len({recipe["id"] for recipe in recipes})) self.assertTrue(all(":crushing_" not in recipe["id"] for recipe in recipes)) def test_every_case_exercises_inventory_predicates(self): - for recipe in fixture()["recipes"]: + for recipe in recipe_unlocks.fixture()["recipes"]: self.assertTrue(recipe["ingredients"], recipe["id"]) self.assertTrue(all(set(predicate) in ({"item"}, {"tag"}) for predicate in recipe["ingredients"].values()), recipe["id"]) def test_standalone_expansions_do_not_require_the_other_expansion(self): for extensions, count in (("none", 35), ("steam", 59), ("electric", 85)): - self.assertEqual(count, len(fixture(extensions)["recipes"])) + self.assertEqual(count, len(recipe_unlocks.fixture(extensions)["recipes"])) def test_human_kit_is_small_unique_and_uses_registered_id_shapes(self): self.assertEqual(27, len(MATERIALS)) diff --git a/test-rig/migration/test_steam_control_audit.py b/test-rig/migration/test_steam_control_audit.py index b2b62bde..ef88d513 100644 --- a/test-rig/migration/test_steam_control_audit.py +++ b/test-rig/migration/test_steam_control_audit.py @@ -15,7 +15,8 @@ import steam_control_audit as audit -LINE = Path(__file__).resolve().parents[3].name[2:] +# The native 1.17 tests replay these controls against their own adapter. +LINE = "116" def snapshot(position, block, data=None): @@ -47,6 +48,7 @@ def data(item, count, slot, reserve=0, work=None, furnace=None): def fixture(): base = audit.native(LINE) + steam = "basesteam" if LINE == "117" else "steamadvantage" ids = sorted({district + "/" + name for district, names in base.REQUIRED.items() for name in names} | audit.IDS) catalog = {"group": "all", "cases": []} if LINE in ("110", "112"): @@ -63,7 +65,7 @@ def at(x): if case_id == audit.COMPARATOR: before, after = [], [] for i, stock in enumerate((0, 1, 32, 64)): - tile = snapshot(at(3 + 6 * i), "steamadvantage:steam_crusher", + tile = snapshot(at(3 + 6 * i), steam + ":steam_crusher", data("minecraft:stone", stock, 0, work=0)) before.append(tile); after.append(deepcopy(tile)) row["count/" + str(3 + 6 * i) + "/4/8/minecraft:stone"] = stock @@ -72,14 +74,14 @@ def at(x): crusher = case_id in audit.CRUSHERS signal, initial = audit.CRUSHERS[case_id] if crusher else (audit.FURNACES[case_id], 40) if crusher: - block, item, slot = "steamadvantage:steam_crusher", "minecraft:stone", 0 + block, item, slot = steam + ":steam_crusher", "minecraft:stone", 0 work = 105 if signal or initial == 0 else 107 reserve = 0 if initial == 0 else 39.9375 if signal else 38.4375 before_data = data(item, 1, slot, initial, work=106) after_data = data(item, 1, slot, reserve, work=work) row["firstLoaded.progress"] = work else: - block, item, slot = "steamadvantage:steam_furnace", "minecraft:sand", 1 + block, item, slot = steam + ":steam_furnace", "minecraft:sand", 1 heat = 989.5 if signal else 1027.0 work = audit.f32(.25 + audit.f32(audit.f32(3.5e-6) * heat)) reserve = 40 if signal else 39.5 diff --git a/test-rig/platform-controls/NativeLeashKnotPlatformControl.java b/test-rig/platform-controls/NativeLeashKnotPlatformControl.java new file mode 100644 index 00000000..c994a269 --- /dev/null +++ b/test-rig/platform-controls/NativeLeashKnotPlatformControl.java @@ -0,0 +1,74 @@ +import com.google.gson.GsonBuilder; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.function.Supplier; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.profiling.InactiveProfiler; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.decoration.LeashFenceKnotEntity; +import net.minecraft.world.level.Level; + +/** No-Power, no-server native storage-eligibility control. */ +public final class NativeLeashKnotPlatformControl { + private static void field(ServerLevel world, String name, Object value) throws Exception { + Field field = Level.class.getDeclaredField(name); field.setAccessible(true); field.set(world, value); + } + private static String hash(Class type) throws Exception { + try (InputStream input = type.getResourceAsStream("/" + type.getName().replace('.', '/') + ".class")) { + if (input == null) throw new AssertionError("No native class bytes"); + StringBuilder result = new StringBuilder(); + for (byte value : MessageDigest.getInstance("SHA-256").digest(input.readAllBytes())) result.append(String.format("%02X", value & 255)); + return result.toString(); + } + } + public static void main(String[] args) throws Exception { + SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); + try { + Class.forName("zone.moddev.mc.poweradvantage.migration.LegacyEntityDataFixes"); + throw new AssertionError("Power must be absent from the native control"); + } catch (ClassNotFoundException expected) { System.out.println("POWER_LEGACY_CLASS_ABSENT true"); } + Class unsafe = Class.forName("sun.misc.Unsafe"); + Field instance = unsafe.getDeclaredField("theUnsafe"); instance.setAccessible(true); + ServerLevel world = (ServerLevel) unsafe.getMethod("allocateInstance", Class.class).invoke(instance.get(null), ServerLevel.class); + field(world, "dimension", Level.NETHER); field(world, "random", new Random(0)); + field(world, "profiler", (Supplier) () -> InactiveProfiler.INSTANCE); + LeashFenceKnotEntity knot = new LeashFenceKnotEntity(world, new BlockPos(154, 100, 8)); + UUID identity = knot.getUUID(); + CompoundTag before = knot.saveWithoutId(new CompoundTag()).copy(); + boolean accepted = knot.saveAsPassenger(new CompoundTag()); + CompoundTag observed = knot.saveWithoutId(new CompoundTag()); + if (knot.getClass() != LeashFenceKnotEntity.class || knot.getType() != EntityType.LEASH_KNOT + || EntityType.LEASH_KNOT.canSerialize() || accepted || !identity.equals(knot.getUUID()) + || !before.equals(observed) || knot.tickCount != 0 || knot.isRemoved()) throw new AssertionError("Native noSave contract differs"); + Map proof = new LinkedHashMap<>(); + proof.put("format", 1); proof.put("policy", "native117-leash-knot-nosave-v1"); + proof.put("minecraftVersion", SharedConstants.getCurrentVersion().getName()); + proof.put("dataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); proof.put("forgeVersion", "37.1.1"); + proof.put("nativeClass", knot.getClass().getName()); proof.put("registryId", EntityType.getKey(knot.getType()).toString()); + proof.put("canSerialize", false); proof.put("rootWriterAccepted", false); + proof.put("observationalUuidPreserved", true); proof.put("observationalStateUnchanged", true); + proof.put("noPower", true); proof.put("noServer", true); proof.put("noTicks", true); proof.put("nativeMethodsUnmodified", true); + Map classes = new LinkedHashMap<>(); + for (Class type : new Class[] {Entity.class, EntityType.class, LeashFenceKnotEntity.class}) classes.put(type.getName(), hash(type)); + proof.put("nativeClassSha256", classes); + Path root = Paths.get(args[0]); Files.createDirectory(root); + String json = new GsonBuilder().setPrettyPrinting().create().toJson(proof) + "\n"; + Files.writeString(root.resolve("contract.json"), json); + System.out.println("LEASH_KNOT_CONTROL " + json.replace('\n', ' ')); + System.out.println("LEASH_KNOT_CONTROL_DONE no_server=true no_ticks=true native_entity_methods_unmodified=true"); + } +} diff --git a/test-rig/platform-controls/NativeWolfPlatformControl.java b/test-rig/platform-controls/NativeWolfPlatformControl.java new file mode 100644 index 00000000..213e7a52 --- /dev/null +++ b/test-rig/platform-controls/NativeWolfPlatformControl.java @@ -0,0 +1,94 @@ +import com.google.gson.Gson; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.function.Supplier; +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.profiling.InactiveProfiler; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.animal.Wolf; +import net.minecraft.world.level.Level; +import net.minecraftforge.common.ForgeMod; +import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; + +/** Native load/save control: no Advantage classes, server, entity ticks or DFU. */ +public final class NativeWolfPlatformControl { + private static final UUID OWNER = UUID.fromString("a614e658-6270-33c3-9297-7ce80908964f"); + private static final UUID ID = UUID.fromString("a6ea8abd-6ab3-4e66-8ef5-d713aabbccdd"); + + private static void field(ServerLevel world, String name, Object value) throws Exception { + Field field = Level.class.getDeclaredField(name); field.setAccessible(true); field.set(world, value); + } + + private static ServerLevel context() throws Exception { + Class unsafe = Class.forName("sun.misc.Unsafe"); + Field instance = unsafe.getDeclaredField("theUnsafe"); instance.setAccessible(true); + ServerLevel world = (ServerLevel) unsafe.getMethod("allocateInstance", Class.class) + .invoke(instance.get(null), ServerLevel.class); + field(world, "dimension", Level.NETHER); + field(world, "random", new Random(0)); + field(world, "profiler", (Supplier) () -> InactiveProfiler.INSTANCE); + return world; + } + + private static Wolf wolf(ServerLevel world) { + Wolf wolf = new Wolf(EntityType.WOLF, world); wolf.setUUID(ID); return wolf; + } + + private static void control(Path root, ServerLevel world, boolean tame, float health) throws Exception { + String label = (tame ? "tame" : "wild") + "-health-" + (int) health; + Wolf first = wolf(world); + first.setTame(tame); + if (tame) first.setOwnerUUID(OWNER); + first.setHealth(health); + CompoundTag before = first.saveWithoutId(new CompoundTag()); + if (before.getFloat("Health") != health) throw new AssertionError("Incorrect source control health"); + Path input = root.resolve(label + "-before.dat"); NbtIo.writeCompressed(before, input.toFile()); + Wolf second = wolf(world); second.load(NbtIo.readCompressed(input.toFile())); + CompoundTag after = second.saveWithoutId(new CompoundTag()); + NbtIo.writeCompressed(after, root.resolve(label + "-after.dat").toFile()); + Wolf third = wolf(world); third.load(after.copy()); + CompoundTag restarted = third.saveWithoutId(new CompoundTag()); + NbtIo.writeCompressed(restarted, root.resolve(label + "-restart.dat").toFile()); + if (!ID.equals(second.getUUID()) || !ID.equals(third.getUUID())) throw new AssertionError("Control UUID changed"); + if (tame && (!OWNER.equals(second.getOwnerUUID()) || !OWNER.equals(third.getOwnerUUID()))) { + throw new AssertionError("Control owner changed"); + } + Map row = new LinkedHashMap<>(); + row.put("label", label); row.put("currentWorldDataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); + row.put("legacyDfuInvoked", false); row.put("directNativeEntityLoadSave", true); + row.put("beforeHealth", before.getFloat("Health")); row.put("afterHealth", after.getFloat("Health")); + row.put("restartHealth", restarted.getFloat("Health")); + row.put("uuidPreserved", true); row.put("ownerPreserved", !tame || OWNER.equals(second.getOwnerUUID())); + System.out.println("WOLF_CONTROL " + new Gson().toJson(row)); + } + + public static void main(String[] args) throws Exception { + SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); + Field attributes = ForgeMod.class.getDeclaredField("ATTRIBUTES"); attributes.setAccessible(true); + Method add = DeferredRegister.class.getDeclaredMethod("addEntries", RegistryEvent.Register.class); add.setAccessible(true); + add.invoke(attributes.get(null), new RegistryEvent.Register<>(ForgeRegistries.ATTRIBUTES.getRegistryName(), ForgeRegistries.ATTRIBUTES)); + try { + Class.forName("zone.moddev.mc.poweradvantage.migration.LegacyEntityDataFixes"); + throw new AssertionError("Advantage is present on the native control classpath"); + } catch (ClassNotFoundException expected) { System.out.println("POWER_LEGACY_CLASS_ABSENT true"); } + Path root = Paths.get(args[0]); Files.createDirectory(root); + System.out.println("NATIVE_WOLF_CODE " + Wolf.class.getProtectionDomain().getCodeSource().getLocation()); + ServerLevel world = context(); + control(root, world, true, 8); control(root, world, true, 20); control(root, world, false, 8); + System.out.println("WOLF_CONTROL_DONE no_server=true no_ticks=true native_entity_methods_unmodified=true"); + } +} diff --git a/test-rig/profiles/native117/dev-core-apocalyptic.json b/test-rig/profiles/native117/dev-core-apocalyptic.json new file mode 100644 index 00000000..9b563a8a --- /dev/null +++ b/test-rig/profiles/native117/dev-core-apocalyptic.json @@ -0,0 +1,58 @@ +{ + "name": "dev-core-apocalyptic", + "mode": "development", + "runtimeBase": "build/works-native117", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "shutdownTimeoutSeconds": 60, + "stationRunSeconds": 5, + "recipeMode": "APOCALYPTIC", + "sourceCommit": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "sourceVersion": "2.4.9.117011", + "lifecycleStations": [], + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.1.117011", + "file": "AdvantageWorksTestHarness-1.1.1.117011-deobf.jar", + "path": "build/native117-artifacts/2BCDF517E5F5C41019194275511E7148186CFAA5C77CDB0930B08BB217F5143D/AdvantageWorksTestHarness-1.1.1.117011-deobf.jar", + "sha256": "2BCDF517E5F5C41019194275511E7148186CFAA5C77CDB0930B08BB217F5143D", + "sourceRef": "f2f6af74ab59aab7562d9af5936a1f17e2adb534", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basesteam", + "version": "2.2.10.117011", + "file": "baseSteam-2.2.10.117011-deobf.jar", + "path": "build/native117-artifacts/AC76E42ABA4B56F258BE0977A2A77777201D3C889F163C39BFBE1839897A427A/baseSteam-2.2.10.117011-deobf.jar", + "sha256": "AC76E42ABA4B56F258BE0977A2A77777201D3C889F163C39BFBE1839897A427A", + "sourceRef": "c8e3febcd11c6854d8073c9c053c7c162cdd6687", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "baseelectric", + "version": "2.2.10.117011", + "file": "baseElectric-2.2.10.117011-deobf.jar", + "path": "build/native117-artifacts/37F787FBC04E5E93C5F123916690C788266E76C5E20534F66D4ADCF1CC07B3E6/baseElectric-2.2.10.117011-deobf.jar", + "sha256": "37F787FBC04E5E93C5F123916690C788266E76C5E20534F66D4ADCF1CC07B3E6", + "sourceRef": "cf3d4b0e2d1591b2ae8e5d638002ca016e46eac4", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + } + ], + "minecraftVersion": "1.17.1", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/dev-core-apocalyptic.json", + "forge": { + "version": "1.17.1-37.1.1", + "installerPath": "build/native117-artifacts/31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B/forge-1.17.1-37.1.1-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.17.1-37.1.1/forge-1.17.1-37.1.1-installer.jar", + "sha256": "31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B" + }, + "generatorSettings": "", + "inputStatus": "ARTIFACTS_AND_SOURCES_PINNED_QUALIFICATION_PENDING", + "powerDeobfSha256": "312FA3012CB57C91F5F22079DA4778D481BA45EF7B8A0587D9993899E1B6D08E" +} diff --git a/test-rig/profiles/native117/dev-core-tech-progression.json b/test-rig/profiles/native117/dev-core-tech-progression.json new file mode 100644 index 00000000..7f6a739a --- /dev/null +++ b/test-rig/profiles/native117/dev-core-tech-progression.json @@ -0,0 +1,58 @@ +{ + "name": "dev-core-tech-progression", + "mode": "development", + "runtimeBase": "build/works-native117", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "shutdownTimeoutSeconds": 60, + "stationRunSeconds": 5, + "recipeMode": "TECH_PROGRESSION", + "sourceCommit": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "sourceVersion": "2.4.9.117011", + "lifecycleStations": [], + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.1.117011", + "file": "AdvantageWorksTestHarness-1.1.1.117011-deobf.jar", + "path": "build/native117-artifacts/2BCDF517E5F5C41019194275511E7148186CFAA5C77CDB0930B08BB217F5143D/AdvantageWorksTestHarness-1.1.1.117011-deobf.jar", + "sha256": "2BCDF517E5F5C41019194275511E7148186CFAA5C77CDB0930B08BB217F5143D", + "sourceRef": "f2f6af74ab59aab7562d9af5936a1f17e2adb534", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basesteam", + "version": "2.2.10.117011", + "file": "baseSteam-2.2.10.117011-deobf.jar", + "path": "build/native117-artifacts/AC76E42ABA4B56F258BE0977A2A77777201D3C889F163C39BFBE1839897A427A/baseSteam-2.2.10.117011-deobf.jar", + "sha256": "AC76E42ABA4B56F258BE0977A2A77777201D3C889F163C39BFBE1839897A427A", + "sourceRef": "c8e3febcd11c6854d8073c9c053c7c162cdd6687", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "baseelectric", + "version": "2.2.10.117011", + "file": "baseElectric-2.2.10.117011-deobf.jar", + "path": "build/native117-artifacts/37F787FBC04E5E93C5F123916690C788266E76C5E20534F66D4ADCF1CC07B3E6/baseElectric-2.2.10.117011-deobf.jar", + "sha256": "37F787FBC04E5E93C5F123916690C788266E76C5E20534F66D4ADCF1CC07B3E6", + "sourceRef": "cf3d4b0e2d1591b2ae8e5d638002ca016e46eac4", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + } + ], + "minecraftVersion": "1.17.1", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/dev-core-tech-progression.json", + "forge": { + "version": "1.17.1-37.1.1", + "installerPath": "build/native117-artifacts/31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B/forge-1.17.1-37.1.1-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.17.1-37.1.1/forge-1.17.1-37.1.1-installer.jar", + "sha256": "31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B" + }, + "generatorSettings": "", + "inputStatus": "ARTIFACTS_AND_SOURCES_PINNED_QUALIFICATION_PENDING", + "powerDeobfSha256": "312FA3012CB57C91F5F22079DA4778D481BA45EF7B8A0587D9993899E1B6D08E" +} diff --git a/test-rig/profiles/native117/dev-core.json b/test-rig/profiles/native117/dev-core.json new file mode 100644 index 00000000..2858a1ea --- /dev/null +++ b/test-rig/profiles/native117/dev-core.json @@ -0,0 +1,57 @@ +{ + "name": "dev-core", + "mode": "development", + "runtimeBase": "build/works-native117", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "shutdownTimeoutSeconds": 60, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "sourceVersion": "2.4.9.117011", + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.1.117011", + "file": "AdvantageWorksTestHarness-1.1.1.117011-deobf.jar", + "path": "build/native117-artifacts/2BCDF517E5F5C41019194275511E7148186CFAA5C77CDB0930B08BB217F5143D/AdvantageWorksTestHarness-1.1.1.117011-deobf.jar", + "sha256": "2BCDF517E5F5C41019194275511E7148186CFAA5C77CDB0930B08BB217F5143D", + "sourceRef": "f2f6af74ab59aab7562d9af5936a1f17e2adb534", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basesteam", + "version": "2.2.10.117011", + "file": "baseSteam-2.2.10.117011-deobf.jar", + "path": "build/native117-artifacts/AC76E42ABA4B56F258BE0977A2A77777201D3C889F163C39BFBE1839897A427A/baseSteam-2.2.10.117011-deobf.jar", + "sha256": "AC76E42ABA4B56F258BE0977A2A77777201D3C889F163C39BFBE1839897A427A", + "sourceRef": "c8e3febcd11c6854d8073c9c053c7c162cdd6687", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "baseelectric", + "version": "2.2.10.117011", + "file": "baseElectric-2.2.10.117011-deobf.jar", + "path": "build/native117-artifacts/37F787FBC04E5E93C5F123916690C788266E76C5E20534F66D4ADCF1CC07B3E6/baseElectric-2.2.10.117011-deobf.jar", + "sha256": "37F787FBC04E5E93C5F123916690C788266E76C5E20534F66D4ADCF1CC07B3E6", + "sourceRef": "cf3d4b0e2d1591b2ae8e5d638002ca016e46eac4", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + } + ], + "minecraftVersion": "1.17.1", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/dev-core.json", + "forge": { + "version": "1.17.1-37.1.1", + "installerPath": "build/native117-artifacts/31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B/forge-1.17.1-37.1.1-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.17.1-37.1.1/forge-1.17.1-37.1.1-installer.jar", + "sha256": "31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B" + }, + "generatorSettings": "", + "inputStatus": "ARTIFACTS_AND_SOURCES_PINNED_QUALIFICATION_PENDING", + "powerDeobfSha256": "312FA3012CB57C91F5F22079DA4778D481BA45EF7B8A0587D9993899E1B6D08E" +} diff --git a/test-rig/profiles/native117/packaged-current.json b/test-rig/profiles/native117/packaged-current.json new file mode 100644 index 00000000..2d2dd417 --- /dev/null +++ b/test-rig/profiles/native117/packaged-current.json @@ -0,0 +1,86 @@ +{ + "name": "packaged-current", + "mode": "packaged", + "runtimeBase": "build/works-native117", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "shutdownTimeoutSeconds": 60, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "sourceVersion": "2.4.9.117011", + "forge": { + "version": "1.17.1-37.1.1", + "installerPath": "build/native117-artifacts/31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B/forge-1.17.1-37.1.1-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.17.1-37.1.1/forge-1.17.1-37.1.1-installer.jar", + "sha256": "31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B" + }, + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.1.117011", + "file": "AdvantageWorksTestHarness-1.1.1.117011.jar", + "path": "build/native117-artifacts/F7EBFD86550E7D595F508B6B696CEA7B52E83D011806C330F102C7245D224EAE/AdvantageWorksTestHarness-1.1.1.117011.jar", + "sha256": "F7EBFD86550E7D595F508B6B696CEA7B52E83D011806C330F102C7245D224EAE", + "sourceRef": "f2f6af74ab59aab7562d9af5936a1f17e2adb534", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basepower", + "version": "2.4.9.117011", + "file": "basePower-2.4.9.117011.jar", + "path": "build/native117-artifacts/C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F/basePower-2.4.9.117011.jar", + "sha256": "C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F", + "sourceRef": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basesteam", + "version": "2.2.10.117011", + "file": "baseSteam-2.2.10.117011.jar", + "path": "build/native117-artifacts/8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B/baseSteam-2.2.10.117011.jar", + "sha256": "8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B", + "sourceRef": "c8e3febcd11c6854d8073c9c053c7c162cdd6687", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "baseelectric", + "version": "2.2.10.117011", + "file": "baseElectric-2.2.10.117011.jar", + "path": "build/native117-artifacts/9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F/baseElectric-2.2.10.117011.jar", + "sha256": "9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F", + "sourceRef": "cf3d4b0e2d1591b2ae8e5d638002ca016e46eac4", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basemetals", + "version": "3.1.1.117011", + "file": "BaseMetals-3.1.1.117011.jar", + "path": "build/native117-artifacts/F3053F23D2415476FC52426A24E3B0B828198A678FBD5DD789923DB2063E0FEB/BaseMetals-3.1.1.117011.jar", + "sha256": "F3053F23D2415476FC52426A24E3B0B828198A678FBD5DD789923DB2063E0FEB", + "sourceRef": "0deeeeecdbbc1fbcdecf910c59477de8efe164d7", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "orespawn", + "version": "4.1.0.117011", + "file": "OreSpawn-4.1.0.117011.jar", + "path": "build/native117-artifacts/1BDEDCBB179CB5A3B169E5C8A6B301E3888AE15B94108B0AEC627D705CFAD833/OreSpawn-4.1.0.117011.jar", + "sha256": "1BDEDCBB179CB5A3B169E5C8A6B301E3888AE15B94108B0AEC627D705CFAD833", + "sourceRef": "b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf", + "workspaceProduced": false, + "pinStatus": "PINNED_UNQUALIFIED" + } + ], + "minecraftVersion": "1.17.1", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/packaged-current.json", + "generatorSettings": "", + "inputStatus": "ARTIFACTS_AND_SOURCES_PINNED_QUALIFICATION_PENDING" +} diff --git a/test-rig/profiles/native117/packaged-decoupled.json b/test-rig/profiles/native117/packaged-decoupled.json new file mode 100644 index 00000000..0f09f960 --- /dev/null +++ b/test-rig/profiles/native117/packaged-decoupled.json @@ -0,0 +1,76 @@ +{ + "name": "packaged-decoupled", + "mode": "packaged", + "runtimeBase": "build/works-native117", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "shutdownTimeoutSeconds": 60, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "sourceVersion": "2.4.9.117011", + "forge": { + "version": "1.17.1-37.1.1", + "installerPath": "build/native117-artifacts/31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B/forge-1.17.1-37.1.1-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.17.1-37.1.1/forge-1.17.1-37.1.1-installer.jar", + "sha256": "31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B" + }, + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.1.117011", + "file": "AdvantageWorksTestHarness-1.1.1.117011.jar", + "path": "build/native117-artifacts/F7EBFD86550E7D595F508B6B696CEA7B52E83D011806C330F102C7245D224EAE/AdvantageWorksTestHarness-1.1.1.117011.jar", + "sha256": "F7EBFD86550E7D595F508B6B696CEA7B52E83D011806C330F102C7245D224EAE", + "sourceRef": "f2f6af74ab59aab7562d9af5936a1f17e2adb534", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basepower", + "version": "2.4.9.117011", + "file": "basePower-2.4.9.117011.jar", + "path": "build/native117-artifacts/C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F/basePower-2.4.9.117011.jar", + "sha256": "C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F", + "sourceRef": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basesteam", + "version": "2.2.10.117011", + "file": "baseSteam-2.2.10.117011.jar", + "path": "build/native117-artifacts/8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B/baseSteam-2.2.10.117011.jar", + "sha256": "8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B", + "sourceRef": "c8e3febcd11c6854d8073c9c053c7c162cdd6687", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "baseelectric", + "version": "2.2.10.117011", + "file": "baseElectric-2.2.10.117011.jar", + "path": "build/native117-artifacts/9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F/baseElectric-2.2.10.117011.jar", + "sha256": "9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F", + "sourceRef": "cf3d4b0e2d1591b2ae8e5d638002ca016e46eac4", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "orespawn", + "version": "4.1.0.117011", + "file": "OreSpawn-4.1.0.117011.jar", + "path": "build/native117-artifacts/1BDEDCBB179CB5A3B169E5C8A6B301E3888AE15B94108B0AEC627D705CFAD833/OreSpawn-4.1.0.117011.jar", + "sha256": "1BDEDCBB179CB5A3B169E5C8A6B301E3888AE15B94108B0AEC627D705CFAD833", + "sourceRef": "b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf", + "workspaceProduced": false, + "pinStatus": "PINNED_UNQUALIFIED" + } + ], + "minecraftVersion": "1.17.1", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/packaged-decoupled.json", + "generatorSettings": "", + "inputStatus": "ARTIFACTS_AND_SOURCES_PINNED_QUALIFICATION_PENDING" +} diff --git a/test-rig/profiles/native117/packaged-mineralogy.json b/test-rig/profiles/native117/packaged-mineralogy.json new file mode 100644 index 00000000..35d6b8f9 --- /dev/null +++ b/test-rig/profiles/native117/packaged-mineralogy.json @@ -0,0 +1,90 @@ +{ + "name": "packaged-mineralogy", + "mode": "packaged", + "runtimeBase": "build/works-native117", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "shutdownTimeoutSeconds": 60, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "sourceVersion": "2.4.9.117011", + "forge": { + "version": "1.17.1-37.1.1", + "installerPath": "build/native117-artifacts/31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B/forge-1.17.1-37.1.1-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.17.1-37.1.1/forge-1.17.1-37.1.1-installer.jar", + "sha256": "31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B" + }, + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.1.117011", + "file": "AdvantageWorksTestHarness-1.1.1.117011.jar", + "path": "build/native117-artifacts/F7EBFD86550E7D595F508B6B696CEA7B52E83D011806C330F102C7245D224EAE/AdvantageWorksTestHarness-1.1.1.117011.jar", + "sha256": "F7EBFD86550E7D595F508B6B696CEA7B52E83D011806C330F102C7245D224EAE", + "sourceRef": "f2f6af74ab59aab7562d9af5936a1f17e2adb534", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basepower", + "version": "2.4.9.117011", + "file": "basePower-2.4.9.117011.jar", + "path": "build/native117-artifacts/C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F/basePower-2.4.9.117011.jar", + "sha256": "C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F", + "sourceRef": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basesteam", + "version": "2.2.10.117011", + "file": "baseSteam-2.2.10.117011.jar", + "path": "build/native117-artifacts/8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B/baseSteam-2.2.10.117011.jar", + "sha256": "8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B", + "sourceRef": "c8e3febcd11c6854d8073c9c053c7c162cdd6687", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "baseelectric", + "version": "2.2.10.117011", + "file": "baseElectric-2.2.10.117011.jar", + "path": "build/native117-artifacts/9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F/baseElectric-2.2.10.117011.jar", + "sha256": "9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F", + "sourceRef": "cf3d4b0e2d1591b2ae8e5d638002ca016e46eac4", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "orespawn", + "version": "4.1.0.117011", + "file": "OreSpawn-4.1.0.117011.jar", + "path": "build/native117-artifacts/1BDEDCBB179CB5A3B169E5C8A6B301E3888AE15B94108B0AEC627D705CFAD833/OreSpawn-4.1.0.117011.jar", + "sha256": "1BDEDCBB179CB5A3B169E5C8A6B301E3888AE15B94108B0AEC627D705CFAD833", + "sourceRef": "b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf", + "workspaceProduced": false, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "mineralogy", + "version": "6.1.4.117011", + "file": "Mineralogy-6.1.4.117011.jar", + "path": "build/native117-artifacts/E59B48D73B43C8A9471FD86878883A20EB8C6DE1554BB96ABB18BDBAE80BD2A8/Mineralogy-6.1.4.117011.jar", + "sha256": "E59B48D73B43C8A9471FD86878883A20EB8C6DE1554BB96ABB18BDBAE80BD2A8", + "sourceRef": "1d7d53450da710701189802f180c1d560ded3c2a", + "workspaceProduced": false, + "pinStatus": "PINNED_UNQUALIFIED" + } + ], + "lifecycleStations": [ + "W-03", + "S-01" + ], + "minecraftVersion": "1.17.1", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/packaged-mineralogy.json", + "generatorSettings": "", + "inputStatus": "ARTIFACTS_AND_SOURCES_PINNED_QUALIFICATION_PENDING" +} diff --git a/test-rig/profiles/native117/packaged-optional-power.json b/test-rig/profiles/native117/packaged-optional-power.json new file mode 100644 index 00000000..a3324571 --- /dev/null +++ b/test-rig/profiles/native117/packaged-optional-power.json @@ -0,0 +1,76 @@ +{ + "name": "packaged-optional-power", + "mode": "packaged", + "runtimeBase": "build/works-native117", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "shutdownTimeoutSeconds": 60, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "sourceVersion": "2.4.9.117011", + "forge": { + "version": "1.17.1-37.1.1", + "installerPath": "build/native117-artifacts/31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B/forge-1.17.1-37.1.1-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.17.1-37.1.1/forge-1.17.1-37.1.1-installer.jar", + "sha256": "31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B" + }, + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.1.117011", + "file": "AdvantageWorksTestHarness-1.1.1.117011.jar", + "path": "build/native117-artifacts/F7EBFD86550E7D595F508B6B696CEA7B52E83D011806C330F102C7245D224EAE/AdvantageWorksTestHarness-1.1.1.117011.jar", + "sha256": "F7EBFD86550E7D595F508B6B696CEA7B52E83D011806C330F102C7245D224EAE", + "sourceRef": "f2f6af74ab59aab7562d9af5936a1f17e2adb534", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basepower", + "version": "2.4.9.117011", + "file": "basePower-2.4.9.117011.jar", + "path": "build/native117-artifacts/C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F/basePower-2.4.9.117011.jar", + "sha256": "C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F", + "sourceRef": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basesteam", + "version": "2.2.10.117011", + "file": "baseSteam-2.2.10.117011.jar", + "path": "build/native117-artifacts/8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B/baseSteam-2.2.10.117011.jar", + "sha256": "8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B", + "sourceRef": "c8e3febcd11c6854d8073c9c053c7c162cdd6687", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "baseelectric", + "version": "2.2.10.117011", + "file": "baseElectric-2.2.10.117011.jar", + "path": "build/native117-artifacts/9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F/baseElectric-2.2.10.117011.jar", + "sha256": "9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F", + "sourceRef": "cf3d4b0e2d1591b2ae8e5d638002ca016e46eac4", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "orespawn", + "version": "4.1.0.117011", + "file": "OreSpawn-4.1.0.117011.jar", + "path": "build/native117-artifacts/1BDEDCBB179CB5A3B169E5C8A6B301E3888AE15B94108B0AEC627D705CFAD833/OreSpawn-4.1.0.117011.jar", + "sha256": "1BDEDCBB179CB5A3B169E5C8A6B301E3888AE15B94108B0AEC627D705CFAD833", + "sourceRef": "b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf", + "workspaceProduced": false, + "pinStatus": "PINNED_UNQUALIFIED" + } + ], + "minecraftVersion": "1.17.1", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/packaged-optional-power.json", + "generatorSettings": "", + "inputStatus": "ARTIFACTS_AND_SOURCES_PINNED_QUALIFICATION_PENDING" +} diff --git a/test-rig/profiles/native117/worldgen-advantage.json b/test-rig/profiles/native117/worldgen-advantage.json new file mode 100644 index 00000000..e7aca3ef --- /dev/null +++ b/test-rig/profiles/native117/worldgen-advantage.json @@ -0,0 +1,168 @@ +{ + "mode": "packaged", + "runtimeBase": "build/works-native117", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "sourceVersion": "2.4.9.117011", + "forge": { + "version": "1.17.1-37.1.1", + "installerPath": "build/native117-artifacts/31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B/forge-1.17.1-37.1.1-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.17.1-37.1.1/forge-1.17.1-37.1.1-installer.jar", + "sha256": "31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B" + }, + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.1.117011", + "file": "AdvantageWorksTestHarness-1.1.1.117011.jar", + "path": "build/native117-artifacts/F7EBFD86550E7D595F508B6B696CEA7B52E83D011806C330F102C7245D224EAE/AdvantageWorksTestHarness-1.1.1.117011.jar", + "sha256": "F7EBFD86550E7D595F508B6B696CEA7B52E83D011806C330F102C7245D224EAE", + "sourceRef": "f2f6af74ab59aab7562d9af5936a1f17e2adb534", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basepower", + "version": "2.4.9.117011", + "file": "basePower-2.4.9.117011.jar", + "path": "build/native117-artifacts/C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F/basePower-2.4.9.117011.jar", + "sha256": "C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F", + "sourceRef": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basesteam", + "version": "2.2.10.117011", + "file": "baseSteam-2.2.10.117011.jar", + "path": "build/native117-artifacts/8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B/baseSteam-2.2.10.117011.jar", + "sha256": "8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B", + "sourceRef": "c8e3febcd11c6854d8073c9c053c7c162cdd6687", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "baseelectric", + "version": "2.2.10.117011", + "file": "baseElectric-2.2.10.117011.jar", + "path": "build/native117-artifacts/9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F/baseElectric-2.2.10.117011.jar", + "sha256": "9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F", + "sourceRef": "cf3d4b0e2d1591b2ae8e5d638002ca016e46eac4", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "orespawn", + "version": "4.1.0.117011", + "file": "OreSpawn-4.1.0.117011.jar", + "path": "build/native117-artifacts/1BDEDCBB179CB5A3B169E5C8A6B301E3888AE15B94108B0AEC627D705CFAD833/OreSpawn-4.1.0.117011.jar", + "sha256": "1BDEDCBB179CB5A3B169E5C8A6B301E3888AE15B94108B0AEC627D705CFAD833", + "sourceRef": "b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf", + "workspaceProduced": false, + "pinStatus": "PINNED_UNQUALIFIED" + } + ], + "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.17.1", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/worldgen-advantage.json", + "inputStatus": "ARTIFACTS_AND_SOURCES_PINNED_QUALIFICATION_PENDING" +} diff --git a/test-rig/profiles/native117/worldgen-legacy-import.json b/test-rig/profiles/native117/worldgen-legacy-import.json new file mode 100644 index 00000000..c140aeeb --- /dev/null +++ b/test-rig/profiles/native117/worldgen-legacy-import.json @@ -0,0 +1,235 @@ +{ + "mode": "packaged", + "runtimeBase": "build/works-native117", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "sourceVersion": "2.4.9.117011", + "forge": { + "version": "1.17.1-37.1.1", + "installerPath": "build/native117-artifacts/31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B/forge-1.17.1-37.1.1-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.17.1-37.1.1/forge-1.17.1-37.1.1-installer.jar", + "sha256": "31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B" + }, + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.1.117011", + "file": "AdvantageWorksTestHarness-1.1.1.117011.jar", + "path": "build/native117-artifacts/F7EBFD86550E7D595F508B6B696CEA7B52E83D011806C330F102C7245D224EAE/AdvantageWorksTestHarness-1.1.1.117011.jar", + "sha256": "F7EBFD86550E7D595F508B6B696CEA7B52E83D011806C330F102C7245D224EAE", + "sourceRef": "f2f6af74ab59aab7562d9af5936a1f17e2adb534", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basepower", + "version": "2.4.9.117011", + "file": "basePower-2.4.9.117011.jar", + "path": "build/native117-artifacts/C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F/basePower-2.4.9.117011.jar", + "sha256": "C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F", + "sourceRef": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basesteam", + "version": "2.2.10.117011", + "file": "baseSteam-2.2.10.117011.jar", + "path": "build/native117-artifacts/8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B/baseSteam-2.2.10.117011.jar", + "sha256": "8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B", + "sourceRef": "c8e3febcd11c6854d8073c9c053c7c162cdd6687", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "baseelectric", + "version": "2.2.10.117011", + "file": "baseElectric-2.2.10.117011.jar", + "path": "build/native117-artifacts/9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F/baseElectric-2.2.10.117011.jar", + "sha256": "9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F", + "sourceRef": "cf3d4b0e2d1591b2ae8e5d638002ca016e46eac4", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "orespawn", + "version": "4.1.0.117011", + "file": "OreSpawn-4.1.0.117011.jar", + "path": "build/native117-artifacts/1BDEDCBB179CB5A3B169E5C8A6B301E3888AE15B94108B0AEC627D705CFAD833/OreSpawn-4.1.0.117011.jar", + "sha256": "1BDEDCBB179CB5A3B169E5C8A6B301E3888AE15B94108B0AEC627D705CFAD833", + "sourceRef": "b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf", + "workspaceProduced": false, + "pinStatus": "PINNED_UNQUALIFIED" + } + ], + "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.17.1", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/worldgen-legacy-import.json", + "inputStatus": "ARTIFACTS_AND_SOURCES_PINNED_QUALIFICATION_PENDING" +} diff --git a/test-rig/profiles/native117/worldgen-mineralogy.json b/test-rig/profiles/native117/worldgen-mineralogy.json new file mode 100644 index 00000000..0aaad91a --- /dev/null +++ b/test-rig/profiles/native117/worldgen-mineralogy.json @@ -0,0 +1,180 @@ +{ + "mode": "packaged", + "runtimeBase": "build/works-native117", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "sourceVersion": "2.4.9.117011", + "forge": { + "version": "1.17.1-37.1.1", + "installerPath": "build/native117-artifacts/31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B/forge-1.17.1-37.1.1-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.17.1-37.1.1/forge-1.17.1-37.1.1-installer.jar", + "sha256": "31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B" + }, + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.1.117011", + "file": "AdvantageWorksTestHarness-1.1.1.117011.jar", + "path": "build/native117-artifacts/F7EBFD86550E7D595F508B6B696CEA7B52E83D011806C330F102C7245D224EAE/AdvantageWorksTestHarness-1.1.1.117011.jar", + "sha256": "F7EBFD86550E7D595F508B6B696CEA7B52E83D011806C330F102C7245D224EAE", + "sourceRef": "f2f6af74ab59aab7562d9af5936a1f17e2adb534", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basepower", + "version": "2.4.9.117011", + "file": "basePower-2.4.9.117011.jar", + "path": "build/native117-artifacts/C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F/basePower-2.4.9.117011.jar", + "sha256": "C55782CA1B0F3B8F7B358BE3150229F5AD92F8C8CE15E3989AAAD1B4F182886F", + "sourceRef": "308d42a89483d56d0fad4805cdb65dac483d9e31", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "basesteam", + "version": "2.2.10.117011", + "file": "baseSteam-2.2.10.117011.jar", + "path": "build/native117-artifacts/8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B/baseSteam-2.2.10.117011.jar", + "sha256": "8A9207A89C0971501BB649FC47F0FF3CF6132FCE3EB2AE651CD8437406BA191B", + "sourceRef": "c8e3febcd11c6854d8073c9c053c7c162cdd6687", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "baseelectric", + "version": "2.2.10.117011", + "file": "baseElectric-2.2.10.117011.jar", + "path": "build/native117-artifacts/9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F/baseElectric-2.2.10.117011.jar", + "sha256": "9788E9DA4DD7698D0D53FC66246ED0637FC6EB89A855B7CFCC4AFFC6EFF7559F", + "sourceRef": "cf3d4b0e2d1591b2ae8e5d638002ca016e46eac4", + "workspaceProduced": true, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "orespawn", + "version": "4.1.0.117011", + "file": "OreSpawn-4.1.0.117011.jar", + "path": "build/native117-artifacts/1BDEDCBB179CB5A3B169E5C8A6B301E3888AE15B94108B0AEC627D705CFAD833/OreSpawn-4.1.0.117011.jar", + "sha256": "1BDEDCBB179CB5A3B169E5C8A6B301E3888AE15B94108B0AEC627D705CFAD833", + "sourceRef": "b56e2b62a1a6ea4d1dd96f8646a87801a817dbdf", + "workspaceProduced": false, + "pinStatus": "PINNED_UNQUALIFIED" + }, + { + "id": "mineralogy", + "version": "6.1.4.117011", + "file": "Mineralogy-6.1.4.117011.jar", + "path": "build/native117-artifacts/E59B48D73B43C8A9471FD86878883A20EB8C6DE1554BB96ABB18BDBAE80BD2A8/Mineralogy-6.1.4.117011.jar", + "sha256": "E59B48D73B43C8A9471FD86878883A20EB8C6DE1554BB96ABB18BDBAE80BD2A8", + "sourceRef": "1d7d53450da710701189802f180c1d560ded3c2a", + "workspaceProduced": false, + "pinStatus": "PINNED_UNQUALIFIED" + } + ], + "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.17.1", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/worldgen-mineralogy.json", + "inputStatus": "ARTIFACTS_AND_SOURCES_PINNED_QUALIFICATION_PENDING" +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ActorProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ActorProbe.java index 10557dcf..598288dd 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ActorProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ActorProbe.java @@ -12,14 +12,14 @@ import java.nio.file.Files; import java.util.Base64; import java.util.UUID; -import net.minecraft.command.Commands; -import net.minecraft.entity.Entity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.commands.Commands; +import net.minecraft.world.entity.Entity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.CompoundTag; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.management.PlayerInteractionManager; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.TickEvent; import net.minecraftforge.event.RegisterCommandsEvent; @@ -28,25 +28,28 @@ final class ActorProbe { private static final Gson JSON = new GsonBuilder().setPrettyPrinting().create(); private static final File MARKER = new File("advantage-migration.allow"); - private final java.util.Set liveWorlds = java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>()); - private final java.util.Map heldActors = new java.util.IdentityHashMap<>(); + private final java.util.Set liveWorlds = java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>()); + private final java.util.Map heldActors = new java.util.IdentityHashMap<>(); static void register() { if (!MARKER.isFile()) return; ActorProbe probe = new ActorProbe(); MinecraftForge.EVENT_BUS.addListener(probe::tick); + MinecraftForge.EVENT_BUS.addListener(probe::entityJoining); MinecraftForge.EVENT_BUS.addListener(probe::starting); } - private boolean frozen(net.minecraft.world.World world) { + private boolean frozen(net.minecraft.world.level.Level world) { return new File("actor-fixture.json").isFile() && !new File("actor-operate.allow").isFile() && !liveWorlds.contains(world); } + static boolean witnessed(Entity entity) { return NativeEntityData.containsString(entity, "AdvWorksActor"); } + private void tick(TickEvent.WorldTickEvent event) { - if (!(event.world instanceof ServerWorld)) return; + if (!(event.world instanceof ServerLevel)) return; if (event.phase == TickEvent.Phase.START && frozen(event.world)) { - NativeEntityHold held = new NativeEntityHold(); - Native116Worlds.entities((ServerWorld) event.world).filter(entity -> entity.getPersistentData().contains("AdvWorksActor", 8)) + NativeEntityHold held = heldActors.computeIfAbsent(event.world, key -> new NativeEntityHold()); + Native117Worlds.entities((ServerLevel) event.world).filter(ActorProbe::witnessed) .forEach(held::hold); heldActors.put(event.world, held); } else if (event.phase == TickEvent.Phase.END) { @@ -55,6 +58,13 @@ private void tick(TickEvent.WorldTickEvent event) { } } + private void entityJoining(net.minecraftforge.event.entity.EntityJoinWorldEvent event) { + if (event.getWorld() instanceof ServerLevel && frozen(event.getWorld()) + && witnessed(event.getEntity())) { + heldActors.computeIfAbsent(event.getWorld(), key -> new NativeEntityHold()).hold(event.getEntity()); + } + } + private void starting(RegisterCommandsEvent event) { for (String operation : new String[] {"snapshot", "read", "operate"}) { event.getDispatcher().register(Commands.literal("advactors").requires(source -> source.hasPermission(4)) @@ -68,7 +78,7 @@ private void starting(RegisterCommandsEvent event) { } } - private void snapshot(MinecraftServer server, ServerWorld world, String operation, String label) throws Exception { + private void snapshot(MinecraftServer server, ServerLevel world, String operation, String label) throws Exception { if (!MARKER.isFile() || !world.getServer().getWorldData().getLevelName().equals("AdvantageWorksMigration") || !label.matches("[a-zA-Z0-9_-]+")) throw new IllegalStateException("Unmarked world or unsafe label"); JsonObject definition = JSON.fromJson(new String(Files.readAllBytes(new File("actor-fixture.json").toPath()), @@ -86,7 +96,7 @@ private void snapshot(MinecraftServer server, ServerWorld world, String operatio result.addProperty("frozen", frozen(world)); result.addProperty("gameTime", world.getGameTime()); try (java.io.FileInputStream input = new java.io.FileInputStream(new File("AdvantageWorksMigration/level.dat"))) { - CompoundNBT data = CompressedStreamTools.readCompressed(input).getCompound("Data"); + CompoundTag data = NbtIo.readCompressed(input).getCompound("Data"); if (data.contains("Player", 10)) result.add("embedded", encode(data.getCompound("Player"))); } boolean read = operation.equals("read"); @@ -94,19 +104,19 @@ private void snapshot(MinecraftServer server, ServerWorld world, String operatio if (read) { GameProfile profile = new GameProfile(UUID.fromString(definition.get("playerUuid").getAsString()), definition.get("playerName").getAsString()); - ServerPlayerEntity player = new ServerPlayerEntity(server, world, profile, new PlayerInteractionManager(world)); + ServerPlayer player = new ServerPlayer(server, world, profile); if (server.getPlayerList().load(player) == null) throw new IllegalStateException("No player file loaded"); - result.add("player", encode(player.saveWithoutId(new CompoundNBT()))); - Native116PlayerIo.save(server.getPlayerList(), player); + result.add("player", encode(player.saveWithoutId(new CompoundTag()))); + Native117PlayerIo.save(server.getPlayerList(), player); } JsonArray entities = new JsonArray(); - for (Entity entity : Native116Worlds.entities(world).collect(java.util.stream.Collectors.toList())) { - if (!entity.getPersistentData().contains("AdvWorksActor", 8)) continue; + for (Entity entity : Native117Worlds.entities(world).collect(java.util.stream.Collectors.toList())) { + if (!witnessed(entity)) continue; if (operation.equals("operate")) entity.canUpdate(true); - CompoundNBT nbt = new CompoundNBT(); + CompoundTag nbt = new CompoundTag(); if (!entity.saveAsPassenger(nbt)) throw new IllegalStateException("Could not serialize actor"); JsonObject entry = encode(nbt); - entry.addProperty("witness", entity.getPersistentData().getString("AdvWorksActor")); + entry.addProperty("witness", nbt.getCompound("ForgeData").getString("AdvWorksActor")); entry.addProperty("ticksExisted", entity.tickCount); entities.add(entry); } @@ -115,9 +125,9 @@ private void snapshot(MinecraftServer server, ServerWorld world, String operatio System.out.println("ADVACTORS COMPLETE " + label); } - private static JsonObject encode(CompoundNBT nbt) throws Exception { + private static JsonObject encode(CompoundTag nbt) throws Exception { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - CompressedStreamTools.writeCompressed(nbt, bytes); + NbtIo.writeCompressed(nbt, bytes); JsonObject result = new JsonObject(); result.addProperty("nbtBase64", Base64.getEncoder().encodeToString(bytes.toByteArray())); return result; diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksCommand.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksCommand.java index 0edd11c6..e0ccb101 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksCommand.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksCommand.java @@ -4,9 +4,9 @@ import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; -import net.minecraft.command.CommandSource; -import net.minecraft.command.Commands; -import net.minecraft.util.text.StringTextComponent; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.TextComponent; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -16,7 +16,7 @@ final class AdvantageWorksCommand { private AdvantageWorksCommand() {} - static void register(CommandDispatcher dispatcher) { + static void register(CommandDispatcher dispatcher) { dispatcher.register(Commands.literal("advworks") .requires(source -> source.hasPermission(2)) .then(targetCommand("build")) @@ -37,7 +37,7 @@ static void register(CommandDispatcher dispatcher) { IntegerArgumentType.getInteger(context, "radius")))))); } - private static com.mojang.brigadier.builder.LiteralArgumentBuilder targetCommand( + private static com.mojang.brigadier.builder.LiteralArgumentBuilder targetCommand( String operation) { return Commands.literal(operation) .executes(context -> execute(context.getSource(), operation, "all", null)) @@ -46,7 +46,7 @@ private static com.mojang.brigadier.builder.LiteralArgumentBuilder implements AutoCloseable { + interface Backend { + void retain(int x, int z); + CompletableFuture request(int x, int z); + void release(int x, int z); + } + + private final Backend backend; + private final ExecutorService executor; + private final Thread owner = Thread.currentThread(); + private final Map> active = new LinkedHashMap<>(); + private volatile boolean closed; + private int peak, requested, released; + + AsyncWorldgenChunks(Backend backend, ExecutorService executor) { + this.backend = Objects.requireNonNull(backend); + this.executor = Objects.requireNonNull(executor); + } + + CompletableFuture request(int x, int z) { + requireOwner(); + if (closed) throw new IllegalStateException("Worldgen chunk dispatcher is closed"); + long key = pack(x, z); + if (active.containsKey(key) || active.size() >= WorldgenSamplingJob.MAX_IN_FLIGHT) { + throw new IllegalStateException("Duplicate or excess native worldgen chunk request"); + } + backend.retain(x, z); + try { + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + if (Thread.currentThread() == owner) { + throw new IllegalStateException("Native FULL future must be requested off the server thread"); + } + if (closed) throw new IllegalStateException("Worldgen chunk dispatcher stopped before dispatch"); + return Objects.requireNonNull(backend.request(x, z), "Native chunk future was null"); + }, executor).thenCompose(value -> value).thenApply(value -> + Objects.requireNonNull(value, "Native FULL chunk was lost")); + active.put(key, future); + requested++; + peak = Math.max(peak, active.size()); + return future; + } catch (RuntimeException failure) { + backend.release(x, z); + throw failure; + } + } + + void release(int x, int z) { + requireOwner(); + CompletableFuture future = active.remove(pack(x, z)); + if (future == null) throw new IllegalStateException("Native worldgen ticket was not held"); + try { future.cancel(false); } + finally { backend.release(x, z); released++; } + } + + @Override public void close() { + requireOwner(); + if (closed) return; + closed = true; + RuntimeException failure = null; + try { + for (long key : new ArrayList<>(active.keySet())) { + try { release((int) (key >> 32), (int) key); } + catch (RuntimeException error) { + if (failure == null) failure = error; + else failure.addSuppressed(error); + } + } + } finally { executor.shutdownNow(); } + if (failure != null) throw failure; + } + + Map diagnostics() { + requireOwner(); + Map result = new LinkedHashMap<>(); + result.put("protocol", "native_FULL_off_thread_future_poll_v1"); + result.put("maximumInFlight", WorldgenSamplingJob.MAX_IN_FLIGHT); + result.put("peakInFlight", peak); + result.put("requests", requested); + result.put("releasedTickets", released); + result.put("activeTickets", active.size()); + result.put("dispatcherClosed", closed); + result.put("executorShutdown", executor.isShutdown()); + return result; + } + + private void requireOwner() { + if (Thread.currentThread() != owner) throw new IllegalStateException("Worldgen ticket access off owner thread"); + } + + private static long pack(int x, int z) { return ((long) x << 32) ^ (z & 0xffffffffL); } +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/BatteryOperationalCases.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/BatteryOperationalCases.java index 006addf0..07be7f06 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/BatteryOperationalCases.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/BatteryOperationalCases.java @@ -3,7 +3,7 @@ import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PoweredEntity; import java.util.List; -import net.minecraft.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; final class BatteryOperationalCases { static void register(List cases) { @@ -15,15 +15,15 @@ static void register(List cases) { cases.add(new MachineScenario("electric/battery-chemistry-" + chemistry, "Each battery chemistry supplies a real LED from finite item NBT, retaining its rated capacity and exact energy debit", 160, c -> { - c.block(7, 4, 8, "electricadvantage:electric_battery_array"); - c.put(7, 4, 8, 0, "electricadvantage:" + chemistry + "_battery", 1); + c.block(7, 4, 8, "baseelectric:electric_battery_array"); + c.put(7, 4, 8, 0, "baseelectric:" + chemistry + "_battery", 1); c.inventory(7, 4, 8).getItem(0).getOrCreateTag().putFloat("energy", 10); - c.block(8, 4, 8, "electricadvantage:led_bar"); + c.block(8, 4, 8, "baseelectric:led_bar"); }, c -> { PoweredEntity array = (PoweredEntity)c.tile(7, 4, 8); c.require(array.getEnergyCapacity(new ConduitType("electricity")) == capacity, "rated battery capacity retained"); c.require(c.world.getBlockState(c.at(8, 4, 8)).getValue(BlockStateProperties.LIT), "battery powers the live LED"); - net.minecraft.util.math.BlockPos source = c.at(7, 4, 8), sink = c.at(8, 4, 8); + net.minecraft.core.BlockPos source = c.at(7, 4, 8), sink = c.at(8, 4, 8); float debit = ElectricOperationalRates.ledDebit(c.activationTick, ElectricOperationalRates.offset(source.getX(), source.getY(), source.getZ()), ElectricOperationalRates.offset(sink.getX(), sink.getY(), sink.getZ()), 160, 10); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/BayDrillAssertions.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/BayDrillAssertions.java index 0b82ebee..e8cce169 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/BayDrillAssertions.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/BayDrillAssertions.java @@ -11,8 +11,8 @@ final class BayDrillAssertions { private BayDrillAssertions() { } static boolean applies(WorksDefinition.Station station, WorksDefinition.Assertion assertion) { - return ("S-05".equals(station.id) && "steamadvantage:steam_drill".equals(assertion.block)) - || ("E-06".equals(station.id) && "electricadvantage:electric_drill".equals(assertion.block)); + return ("S-05".equals(station.id) && "basesteam:steam_drill".equals(assertion.block)) + || ("E-06".equals(station.id) && "baseelectric:electric_drill".equals(assertion.block)); } static int[] rearTrack(WorksDefinition.Station station, WorksDefinition.Assertion assertion) { @@ -22,8 +22,8 @@ static int[] rearTrack(WorksDefinition.Station station, WorksDefinition.Assertio if (direction < 0 || direction >= REAR.length) throw new IllegalArgumentException("Invalid drill facing"); int[] rear = assertion.pos.clone(); for (int i = 0; i < 3; i++) rear[i] += REAR[direction][i]; - String track = assertion.block.startsWith("steamadvantage:") - ? "steamadvantage:steam_track" : "electricadvantage:electric_track"; + String track = assertion.block.startsWith("basesteam:") + ? "basesteam:steam_track" : "baseelectric:electric_track"; if (station.placements.stream().noneMatch(p -> track.equals(p.block) && Arrays.equals(p.pos, rear))) { throw new IllegalArgumentException("Drill bay has no declared rear track"); } 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 6ae8e58d..cc6d0e84 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 @@ -8,13 +8,13 @@ import java.io.File; import java.util.ArrayList; import java.util.List; -import net.minecraft.block.BlockState; -import net.minecraft.command.Commands; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.state.Property; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.commands.Commands; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.TickEvent; import net.minecraftforge.event.RegisterCommandsEvent; @@ -22,18 +22,19 @@ /** The target adapter has no construction, inventory, energy or network-reset operation. */ final class ConnectedProbe implements ConnectedSession.Port { - private ServerWorld world; + private ServerLevel world; private ConnectedSession session; - private final List held = new ArrayList<>(); + private final List held = new ArrayList<>(); static void register() { if (!new File("advantage-connected.allow").isFile()) return; ConnectedProbe probe = new ConnectedProbe(); MinecraftForge.EVENT_BUS.addListener(probe::tick); + MinecraftForge.EVENT_BUS.addListener(probe::chunkLoaded); MinecraftForge.EVENT_BUS.addListener(probe::starting); } - private ConnectedSession get(ServerWorld current) throws Exception { + private ConnectedSession get(ServerLevel current) throws Exception { if (!current.getServer().getWorldData().getLevelName().equals("AdvantageWorksMigration")) throw new IllegalStateException("Unexpected connected world"); if (world != current) { world = current; @@ -43,13 +44,21 @@ private ConnectedSession get(ServerWorld current) throws Exception { } private void tick(TickEvent.WorldTickEvent event) { - if (event.world.isClientSide || Native116Worlds.legacyId(event.world.dimension()) != 0) return; + if (event.world.isClientSide || Native117Worlds.legacyId(event.world.dimension()) != 0) return; try { - ConnectedSession current = get((ServerWorld) event.world); + ConnectedSession current = get((ServerLevel) event.world); if (event.phase == TickEvent.Phase.START) current.start(); else current.end(); } catch (Exception ex) { throw new IllegalStateException("Connected probe failed", ex); } } + private void chunkLoaded(net.minecraftforge.event.world.ChunkEvent.Load event) { + if (!(event.getWorld() instanceof ServerLevel)) return; + ServerLevel loaded = (ServerLevel) event.getWorld(); + if (loaded.dimension() != net.minecraft.world.level.Level.OVERWORLD) return; + if (session == null) held.addAll(NativeBlockEntityTicks.drain(loaded)); + else if (world == loaded) session.start(); + } + private void starting(RegisterCommandsEvent event) { event.getDispatcher().register(Commands.literal("advconnected").requires(s -> s.hasPermission(4)) .then(Commands.literal("snapshot").then(Commands.argument("label", StringArgumentType.word()).executes(c -> { @@ -61,7 +70,7 @@ private void starting(RegisterCommandsEvent event) { }))))); } - private void execute(ServerWorld current, String operation, String label, int ticks) { + private void execute(ServerLevel current, String operation, String label, int ticks) { try { get(current).execute(operation, label, ticks, false); } catch (Exception ex) { throw new IllegalStateException("Connected probe failed", ex); } } @@ -80,8 +89,8 @@ private void execute(ServerWorld current, String operation, String label, int ti } @Override public void build(JsonObject definition) { throw new UnsupportedOperationException("Target construction is forbidden"); } - @Override public void hold() { held.addAll(world.tickableBlockEntities); world.tickableBlockEntities.clear(); } - @Override public void restore() { for (TileEntity tile : held) if (!tile.isRemoved() && !world.tickableBlockEntities.contains(tile)) world.tickableBlockEntities.add(tile); held.clear(); } + @Override public void hold() { held.addAll(NativeBlockEntityTicks.drain(world)); } + @Override public void restore() { NativeBlockEntityTicks.restore(world, held); } @Override public long time() { return world.getGameTime(); } @Override public JsonArray snapshot(JsonObject definition) throws Exception { @@ -96,15 +105,15 @@ private void execute(ServerWorld current, String operation, String label, int ti JsonObject properties = new JsonObject(); for (Property p : state.getProperties()) properties.addProperty(p.getName(), state.getValue(p).toString().toLowerCase(java.util.Locale.ROOT)); entry.add("properties", properties); - TileEntity tile = world.getBlockEntity(pos); + BlockEntity tile = world.getBlockEntity(pos); if (tile != null) { - CompoundNBT tag = tile.save(new CompoundNBT()); + CompoundTag tag = tile.save(new CompoundTag()); java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream(); - net.minecraft.nbt.CompressedStreamTools.writeCompressed(tag, bytes); + net.minecraft.nbt.NbtIo.writeCompressed(tag, bytes); entry.addProperty("nbt", tag.toString()); entry.addProperty("nbtBase64", java.util.Base64.getEncoder().encodeToString(bytes.toByteArray())); entry.addProperty("objectIdentity", System.identityHashCode(tile)); - entry.addProperty("witness", tile.getTileData().getString("AdvConnectedWitness")); + entry.addProperty("witness", tag.getCompound("ForgeData").getString("AdvConnectedWitness")); } result.add(entry); } 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 513b6c86..9fd0fe64 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 @@ -5,46 +5,54 @@ import com.google.gson.JsonArray; import com.google.gson.JsonNull; import com.google.gson.JsonObject; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; import com.mojang.authlib.GameProfile; import com.mojang.brigadier.arguments.StringArgumentType; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.security.MessageDigest; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.IdentityHashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -import net.minecraft.block.BedBlock; -import net.minecraft.command.Commands; -import net.minecraft.entity.Entity; -import net.minecraft.entity.MobEntity; -import net.minecraft.entity.item.LeashKnotEntity; -import net.minecraft.entity.item.HangingEntity; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.block.Blocks; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.network.PacketDirection; -import net.minecraft.network.play.ServerPlayNetHandler; -import net.minecraft.network.NetworkManager; -import net.minecraft.network.IPacket; -import net.minecraft.network.play.client.CClientStatusPacket; +import net.minecraft.world.level.block.BedBlock; +import net.minecraft.commands.Commands; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.decoration.LeashFenceKnotEntity; +import net.minecraft.world.entity.decoration.HangingEntity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ServerboundClientCommandPacket; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.management.PlayerInteractionManager; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.DamageSource; -import net.minecraft.util.Direction; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.FakePlayer; import net.minecraftforge.common.util.FakePlayerFactory; @@ -57,21 +65,25 @@ final class DimensionEntityProbe { private static final Gson JSON = new GsonBuilder().setPrettyPrinting().serializeNulls().create(); private static final File MARKER = new File("advantage-migration.allow"); private static final File DEFINITION = new File("dimension-fixture.json"); + private static final String REQUEST_PROTOCOL = "native117-respawn-request-v1"; + private static final Set REQUEST_KEYS = new HashSet<>(Arrays.asList( + "protocol", "id", "baseFixtureSha256", "keepInventory", "bedMode")); private static final String WITNESS = "AdvWorksDimensionEntity"; private static final BlockPos SPAWN = new BlockPos(160, 100, 24); - private final Set live = Collections.newSetFromMap(new IdentityHashMap()); - private static final Set PINNED = Collections.newSetFromMap(new IdentityHashMap()); - private final Map held = new IdentityHashMap<>(); + private final Set live = Collections.newSetFromMap(new IdentityHashMap()); + private static final Set PINNED = Collections.newSetFromMap(new IdentityHashMap()); + private final Map held = new IdentityHashMap<>(); static void register() { if (!MARKER.isFile()) return; DimensionEntityProbe probe = new DimensionEntityProbe(); MinecraftForge.EVENT_BUS.addListener(probe::load); MinecraftForge.EVENT_BUS.addListener(probe::tick); + MinecraftForge.EVENT_BUS.addListener(probe::entityJoining); MinecraftForge.EVENT_BUS.addListener(probe::starting); } - private static boolean marked(World world) { + private static boolean marked(Level world) { return MARKER.isFile() && !world.isClientSide && "AdvantageWorksMigration".equals(world.getServer().getWorldData().getLevelName()); } @@ -80,19 +92,19 @@ private static boolean nativeOnLoad(JsonObject definition) { return definition.has("nativeOnLoad") && definition.get("nativeOnLoad").getAsBoolean(); } - private boolean frozen(World world, JsonObject definition) { + private boolean frozen(Level world, JsonObject definition) { return marked(world) && DEFINITION.isFile() && !nativeOnLoad(definition) && !live.contains(world); } - private static boolean witnessed(Entity entity) { return entity.getPersistentData().contains(WITNESS, 8); } + private static boolean witnessed(Entity entity) { return NativeEntityData.containsString(entity, WITNESS); } private void load(WorldEvent.Load event) { - if (!(event.getWorld() instanceof ServerWorld)) return; - ServerWorld world = (ServerWorld) event.getWorld(); + if (!(event.getWorld() instanceof ServerLevel)) return; + ServerLevel world = (ServerLevel) event.getWorld(); if (!marked(world) || !DEFINITION.isFile()) return; try { JsonObject definition = definition(); - if (Native116Worlds.legacyId(world.dimension()) == dimension(definition)) prepare(world); + if (Native117Worlds.legacyId(world.dimension()) == dimension(definition)) prepare(world); } catch (Exception ex) { throw new IllegalStateException("Dimension load preparation failed", ex); } } @@ -102,17 +114,17 @@ private void tick(TickEvent.WorldTickEvent event) { try { JsonObject definition = definition(); int selected = dimension(definition); - if (event.world.dimension().equals(World.OVERWORLD) && nativeOnLoad(definition)) { - ServerWorld selectedWorld = event.world.getServer().getLevel(Native116Worlds.key(selected)); + if (event.world.dimension().equals(Level.OVERWORLD) && nativeOnLoad(definition)) { + ServerLevel selectedWorld = event.world.getServer().getLevel(Native117Worlds.key(selected)); if (selectedWorld == null || !marked(selectedWorld)) throw new IllegalStateException("Dimension unavailable"); prepare(selectedWorld); } - if (Native116Worlds.legacyId(event.world.dimension()) != selected) return; + if (Native117Worlds.legacyId(event.world.dimension()) != selected) return; // Pin/load before either frozen observation or normal native initialization. - prepare((ServerWorld) event.world); + prepare((ServerLevel) event.world); if (!frozen(event.world, definition)) return; - NativeEntityHold hold = new NativeEntityHold(); - for (Entity entity : witnesses((ServerWorld) event.world)) hold.hold(entity); + NativeEntityHold hold = held.computeIfAbsent(event.world, key -> new NativeEntityHold()); + for (Entity entity : witnesses((ServerLevel) event.world)) hold.hold(entity); held.put(event.world, hold); } catch (Exception ex) { throw new IllegalStateException("Dimension tick preparation failed", ex); } } else if (event.phase == TickEvent.Phase.END) { @@ -121,6 +133,16 @@ private void tick(TickEvent.WorldTickEvent event) { } } + private void entityJoining(net.minecraftforge.event.entity.EntityJoinWorldEvent event) { + if (!(event.getWorld() instanceof ServerLevel) || !marked(event.getWorld()) + || !DEFINITION.isFile() || !witnessed(event.getEntity())) return; + try { + if (frozen(event.getWorld(), definition())) { + held.computeIfAbsent(event.getWorld(), key -> new NativeEntityHold()).hold(event.getEntity()); + } + } catch (Exception error) { throw new IllegalStateException("Dimension join hold failed", error); } + } + private void starting(RegisterCommandsEvent event) { for (String operation : new String[] {"read", "snapshot", "operate", "respawn"}) { event.getDispatcher().register(Commands.literal("advdimensions").requires(source -> source.hasPermission(4)) @@ -135,11 +157,12 @@ private void starting(RegisterCommandsEvent event) { private void execute(MinecraftServer server, String operation, String label) throws Exception { if (!MARKER.isFile() || !label.matches("[a-zA-Z0-9_-]+")) throw new IllegalArgumentException("Unmarked runtime or unsafe label"); - JsonObject definition = definition(); - ServerWorld overworld = server.getLevel(World.OVERWORLD); + JsonObject request = operation.equals("respawn") ? respawnRequest(DEFINITION.toPath(), label) : null; + JsonObject definition = request == null ? definition() : request.getAsJsonObject("definition"); + ServerLevel overworld = server.getLevel(Level.OVERWORLD); if (overworld == null || !marked(overworld)) throw new IllegalStateException("Unexpected overworld"); - ServerWorld world = server.getLevel(Native116Worlds.key(dimension(definition))); - if (world == null || !marked(world) || Native116Worlds.legacyId(world.dimension()) != dimension(definition)) { + ServerLevel world = server.getLevel(Native117Worlds.key(dimension(definition))); + if (world == null || !marked(world) || Native117Worlds.legacyId(world.dimension()) != dimension(definition)) { throw new IllegalStateException("Dimension unavailable"); } prepare(world); @@ -148,12 +171,17 @@ private void execute(MinecraftServer server, String operation, String label) thr for (Entity entity : witnesses(world)) entity.canUpdate(true); } JsonObject result = snapshot(server, world, definition, operation); + if (request != null) result.add("respawnRequest", request.getAsJsonObject("evidence")); Files.write(new File("dimensions-" + label + ".json").toPath(), JSON.toJson(result).getBytes(StandardCharsets.UTF_8)); System.out.println("ADVDIMENSIONS COMPLETE " + label); } private static JsonObject definition() throws Exception { JsonObject result = JSON.fromJson(new String(Files.readAllBytes(DEFINITION.toPath()), StandardCharsets.UTF_8), JsonObject.class); + return definition(result); + } + + private static JsonObject definition(JsonObject result) { dimension(result); String name = result.has("playerName") ? result.get("playerName").getAsString() : "AdvWorksDimensionPlayer"; UUID uuid = result.has("playerUuid") ? UUID.fromString(result.get("playerUuid").getAsString()) @@ -162,13 +190,80 @@ private static JsonObject definition() throws Exception { return result; } + // The tick reader's root is immutable. Each command consumes one closed file. + static JsonObject respawnRequest(Path fixture, String label) throws Exception { + java.util.regex.Matcher options = java.util.regex.Pattern.compile( + "respawn(?:-restart)?-(keep|drop)-(forced|bed|missing)").matcher(label); + if (!options.matches()) throw new IllegalArgumentException("Unsafe respawn request label"); + Path root = fixture.toAbsolutePath().normalize(); + Path directory = root.resolveSibling("respawn-requests"); + Path path = directory.resolve(label + ".json"); + if (!Files.isRegularFile(root, LinkOption.NOFOLLOW_LINKS) + || !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS) + || Files.isSymbolicLink(directory) || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) + || !directory.toRealPath().getParent().equals(root.getParent().toRealPath())) { + throw new IllegalArgumentException("Respawn request and root must be regular runtime files"); + } + if (Files.size(path) > 4096) throw new IllegalArgumentException("Respawn request exceeds size limit"); + byte[] baseBytes = Files.readAllBytes(root), requestBytes = Files.readAllBytes(path); + if (requestBytes.length > 4096) throw new IllegalArgumentException("Respawn request exceeds size limit"); + JsonObject value = new JsonObject(); + Set keys = new HashSet<>(); + try (JsonReader reader = new JsonReader(new StringReader(new String(requestBytes, StandardCharsets.UTF_8)))) { + reader.setLenient(false); + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + if (!REQUEST_KEYS.contains(key) || !keys.add(key)) { + throw new IllegalArgumentException("Unknown or duplicate respawn request key"); + } + if (key.equals("keepInventory")) { + if (reader.peek() != JsonToken.BOOLEAN) throw new IllegalArgumentException("keepInventory must be a boolean"); + value.addProperty(key, reader.nextBoolean()); + } else { + if (reader.peek() != JsonToken.STRING) throw new IllegalArgumentException("Respawn request field must be a string"); + value.addProperty(key, reader.nextString()); + } + } + reader.endObject(); + if (reader.peek() != JsonToken.END_DOCUMENT) throw new IllegalArgumentException("Trailing respawn request data"); + } catch (java.io.IOException | IllegalStateException error) { + throw new IllegalArgumentException("Malformed respawn request", error); + } + boolean keep = options.group(1).equals("keep"); + String mode = options.group(2), baseHash = sha256(baseBytes); + if (!keys.equals(REQUEST_KEYS) || !REQUEST_PROTOCOL.equals(value.get("protocol").getAsString()) + || !label.equals(value.get("id").getAsString()) + || !baseHash.equals(value.get("baseFixtureSha256").getAsString()) + || value.get("keepInventory").getAsBoolean() != keep || !mode.equals(value.get("bedMode").getAsString())) { + throw new IllegalArgumentException("Respawn request protocol, identity, base hash or options differ"); + } + JsonObject base = definition(JSON.fromJson(new String(baseBytes, StandardCharsets.UTF_8), JsonObject.class)); + JsonObject merged = base.has("respawn") ? base.getAsJsonObject("respawn") : new JsonObject(); + merged.addProperty("keepInventory", keep); + merged.addProperty("bedMode", mode); + base.add("respawn", merged); + value.addProperty("path", "respawn-requests/" + label + ".json"); + value.addProperty("sha256", sha256(requestBytes)); + JsonObject result = new JsonObject(); result.add("definition", base); result.add("evidence", value); + return result; + } + + private static String sha256(byte[] bytes) throws java.security.NoSuchAlgorithmException { + StringBuilder result = new StringBuilder(); + for (byte value : MessageDigest.getInstance("SHA-256").digest(bytes)) { + result.append(String.format(java.util.Locale.ROOT, "%02X", value & 255)); + } + return result.toString(); + } + private static int dimension(JsonObject definition) { int result = definition.get("dimension").getAsInt(); if (result != -1 && result != 1) throw new IllegalArgumentException("Expected Nether (-1) or End (1)"); return result; } - private static void prepare(ServerWorld world) { + private static void prepare(ServerLevel world) { if (!PINNED.contains(world)) { for (int x = 6; x <= 13; x++) for (int z = -2; z <= 2; z++) { world.setChunkForced(x, z, true); @@ -183,11 +278,11 @@ private static void prepare(ServerWorld world) { for (int x = 6; x <= 13; x++) for (int z = -2; z <= 2; z++) world.getChunk(x, z); } - private static List witnesses(ServerWorld world) { + private static List witnesses(ServerLevel world) { Set seen = Collections.newSetFromMap(new IdentityHashMap()); List result = new ArrayList<>(); - Native116Worlds.entities(world).forEach(entity -> collect(entity, seen, result)); - result.sort((a, b) -> a.getPersistentData().getString(WITNESS).compareTo(b.getPersistentData().getString(WITNESS))); + Native117Worlds.entities(world).forEach(entity -> collect(entity, seen, result)); + result.sort((a, b) -> NativeEntityData.string(a, WITNESS).compareTo(NativeEntityData.string(b, WITNESS))); return result; } @@ -197,20 +292,19 @@ private static void collect(Entity entity, Set seen, List result for (Entity passenger : entity.getPassengers()) collect(passenger, seen, result); } - private static ServerPlayerEntity readPlayer(MinecraftServer server, ServerWorld world, JsonObject definition) { - ServerPlayerEntity player = new ServerPlayerEntity(server, world, - new GameProfile(UUID.fromString(definition.get("playerUuid").getAsString()), definition.get("playerName").getAsString()), - new PlayerInteractionManager(world)); + private static ServerPlayer readPlayer(MinecraftServer server, ServerLevel world, JsonObject definition) { + ServerPlayer player = new ServerPlayer(server, world, + new GameProfile(UUID.fromString(definition.get("playerUuid").getAsString()), definition.get("playerName").getAsString())); if (server.getPlayerList().load(player) == null) throw new IllegalStateException("No player file loaded"); - if (Native116Worlds.legacyId(player.getLevel().dimension()) != dimension(definition) || player.getLevel() != world) { + if (Native117Worlds.legacyId(player.getLevel().dimension()) != dimension(definition) || player.getLevel() != world) { throw new IllegalStateException("Player loaded in wrong dimension"); } return player; } - private JsonObject snapshot(MinecraftServer server, ServerWorld world, JsonObject definition, String operation) throws Exception { + private JsonObject snapshot(MinecraftServer server, ServerLevel world, JsonObject definition, String operation) throws Exception { JsonObject result = new JsonObject(); result.add("definition", definition); - result.addProperty("operation", operation); result.addProperty("dimension", Native116Worlds.legacyId(world.dimension())); + result.addProperty("operation", operation); result.addProperty("dimension", Native117Worlds.legacyId(world.dimension())); result.addProperty("worldDimensionKey", world.dimension().location().toString()); result.addProperty("nativeEntityContextVersion", 1); result.addProperty("gameTime", world.getGameTime()); result.addProperty("frozen", frozen(world, definition)); @@ -218,18 +312,18 @@ private JsonObject snapshot(MinecraftServer server, ServerWorld world, JsonObjec boolean read = operation.equals("read") || operation.equals("respawn"); result.addProperty("playerAdapter", read); result.add("playerWorldDimension", JsonNull.INSTANCE); if (read) { - ServerPlayerEntity player = readPlayer(server, world, definition); - result.addProperty("playerWorldDimension", Native116Worlds.legacyId(player.getLevel().dimension())); - result.add("player", encode(player.saveWithoutId(new CompoundNBT()))); + ServerPlayer player = readPlayer(server, world, definition); + result.addProperty("playerWorldDimension", Native117Worlds.legacyId(player.getLevel().dimension())); + result.add("player", encode(player.saveWithoutId(new CompoundTag()))); if (operation.equals("respawn")) result.add("respawn", respawn(server, world, definition, player)); - else Native116PlayerIo.save(server.getPlayerList(), player); + else Native117PlayerIo.save(server.getPlayerList(), player); } JsonArray entities = new JsonArray(); for (Entity entity : witnesses(world)) { - JsonObject context = Native116EntityContext.observe(entity, world); + JsonObject context = Native117EntityContext.observe(entity, world); // This native writer includes type ID and works for both roots and riding passengers. - CompoundNBT nbt = entityNbt(entity); - JsonObject entry = encode(nbt); entry.addProperty("witness", entity.getPersistentData().getString(WITNESS)); + CompoundTag nbt = entityNbt(entity); + JsonObject entry = encode(nbt); entry.addProperty("witness", nbt.getCompound("ForgeData").getString(WITNESS)); entry.add("worldDimensionKey", context.get("worldDimensionKey")); entry.add("sameWorld", context.get("sameWorld")); entry.add("entityWorlds", context.get("entityWorlds")); @@ -239,7 +333,7 @@ private JsonObject snapshot(MinecraftServer server, ServerWorld world, JsonObjec entry.add("vehicleUuid", entity.getVehicle() == null ? JsonNull.INSTANCE : JSON.toJsonTree(entity.getVehicle().getUUID().toString())); JsonArray passengers = new JsonArray(); for (Entity passenger : entity.getPassengers()) passengers.add(passenger.getUUID().toString()); entry.add("passengerUuids", passengers); - Entity leash = entity instanceof MobEntity ? ((MobEntity) entity).getLeashHolder() : null; + Entity leash = entity instanceof Mob ? ((Mob) entity).getLeashHolder() : null; entry.add("leashHolderUuid", leash == null ? JsonNull.INSTANCE : JSON.toJsonTree(leash.getUUID().toString())); entry.add("leashHolderType", leash == null ? JsonNull.INSTANCE : JSON.toJsonTree(leash.getType().getRegistryName().toString())); BlockPos leashPos = leash instanceof HangingEntity ? ((HangingEntity) leash).getPos() : null; @@ -250,12 +344,12 @@ private JsonObject snapshot(MinecraftServer server, ServerWorld 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()); - TileEntity tile = world.getBlockEntity(pos); if (tile != null) generator.add("tile", encode(tile.save(new CompoundNBT()))); + BlockEntity tile = world.getBlockEntity(pos); if (tile != null) generator.add("tile", encode(tile.save(new CompoundTag()))); result.add("generator", generator); return result; } - private static void platform(ServerWorld world) { + private static void platform(ServerLevel world) { for (int x = 156; x <= 166; x++) for (int z = 20; z <= 29; z++) { world.setBlock(new BlockPos(x, 99, z), Blocks.BEDROCK.defaultBlockState(), 2); world.setBlock(new BlockPos(x, 106, z), Blocks.BEDROCK.defaultBlockState(), 2); @@ -264,53 +358,53 @@ private static void platform(ServerWorld world) { } } - private static JsonObject respawn(MinecraftServer server, ServerWorld world, JsonObject definition, ServerPlayerEntity player) throws Exception { + private static JsonObject respawn(MinecraftServer server, ServerLevel world, JsonObject definition, ServerPlayer player) throws Exception { JsonObject options = definition.has("respawn") ? definition.getAsJsonObject("respawn") : new JsonObject(); boolean keep = !options.has("keepInventory") || options.get("keepInventory").getAsBoolean(); String bedMode = options.has("bedMode") ? options.get("bedMode").getAsString() : "forced"; if (!bedMode.matches("forced|bed|missing")) throw new IllegalArgumentException("Invalid bed mode"); - ServerWorld destination = server.getLevel(World.OVERWORLD); + ServerLevel destination = server.getLevel(Level.OVERWORLD); platform(destination); if (bedMode.equals("bed")) { destination.setBlock(SPAWN, Blocks.RED_BED.defaultBlockState().setValue(BedBlock.FACING, Direction.EAST) - .setValue(BedBlock.PART, net.minecraft.state.properties.BedPart.FOOT), 2); + .setValue(BedBlock.PART, net.minecraft.world.level.block.state.properties.BedPart.FOOT), 2); destination.setBlock(SPAWN.east(), Blocks.RED_BED.defaultBlockState().setValue(BedBlock.FACING, Direction.EAST) - .setValue(BedBlock.PART, net.minecraft.state.properties.BedPart.HEAD), 2); + .setValue(BedBlock.PART, net.minecraft.world.level.block.state.properties.BedPart.HEAD), 2); } - player.setRespawnPosition(World.OVERWORLD, SPAWN, 0.0F, bedMode.equals("forced"), false); + player.setRespawnPosition(Level.OVERWORLD, SPAWN, 0.0F, bedMode.equals("forced"), false); JsonObject result = new JsonObject(); result.addProperty("scope", "server-only-dummy-network"); result.addProperty("handler", "ServerPlayNetHandler.PERFORM_RESPAWN"); result.addProperty("keepInventory", keep); result.addProperty("bedMode", bedMode); - CompoundNBT before = player.saveWithoutId(new CompoundNBT()); - result.addProperty("fromDimension", Native116Worlds.legacyId(player.getLevel().dimension())); result.add("before", encode(before)); + CompoundTag before = player.saveWithoutId(new CompoundTag()); + result.addProperty("fromDimension", Native117Worlds.legacyId(player.getLevel().dimension())); result.add("before", encode(before)); JsonObject diagnostics = new JsonObject(); result.add("spawnDiagnostics", diagnostics); diagnostics.add("before", spawnDiagnostics(player, before)); - Set existing = Collections.newSetFromMap(new IdentityHashMap()); Native116Worlds.entities(world).forEach(existing::add); - boolean oldSource = world.getGameRules().getBoolean(net.minecraft.world.GameRules.RULE_KEEPINVENTORY), - oldTarget = destination.getGameRules().getBoolean(net.minecraft.world.GameRules.RULE_KEEPINVENTORY); - ServerPlayerEntity replacement = null; + Set existing = Collections.newSetFromMap(new IdentityHashMap()); Native117Worlds.entities(world).forEach(existing::add); + boolean oldSource = world.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY), + oldTarget = destination.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY); + ServerPlayer replacement = null; try { - world.getGameRules().getRule(net.minecraft.world.GameRules.RULE_KEEPINVENTORY).set(keep, server); - destination.getGameRules().getRule(net.minecraft.world.GameRules.RULE_KEEPINVENTORY).set(keep, server); + world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(keep, server); + destination.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(keep, server); OfflineConnection connection = new OfflineConnection(server, player); player.setHealth(0); player.die(DamageSource.GENERIC); - CompoundNBT afterDeath = player.saveWithoutId(new CompoundNBT()); + CompoundTag afterDeath = player.saveWithoutId(new CompoundTag()); result.add("afterDeath", encode(afterDeath)); diagnostics.add("afterDeath", spawnDiagnostics(player, afterDeath)); if (player.wonGame) throw new IllegalStateException("Expected ordinary death respawn, not queued End exit"); - connection.handleClientCommand(new CClientStatusPacket(CClientStatusPacket.State.PERFORM_RESPAWN)); + connection.handleClientCommand(new ServerboundClientCommandPacket(ServerboundClientCommandPacket.Action.PERFORM_RESPAWN)); replacement = server.getPlayerList().getPlayer(player.getUUID()); if (replacement == null || replacement == player) throw new IllegalStateException("Native respawn handler did not replace player"); new OfflineConnection(server, replacement); - CompoundNBT after = replacement.saveWithoutId(new CompoundNBT()); + CompoundTag after = replacement.saveWithoutId(new CompoundTag()); result.add("after", encode(after)); diagnostics.add("after", spawnDiagnostics(replacement, after)); - result.addProperty("toDimension", Native116Worlds.legacyId(replacement.getLevel().dimension())); + result.addProperty("toDimension", Native117Worlds.legacyId(replacement.getLevel().dimension())); BlockPos bed = replacement.getRespawnPosition(); result.add("bedLocation", bed == null ? JsonNull.INSTANCE : JSON.toJsonTree(new int[] {bed.getX(), bed.getY(), bed.getZ()})); JsonArray drops = new JsonArray(); - for (Entity entity : Native116Worlds.entities(world).collect(java.util.stream.Collectors.toList())) if (entity instanceof ItemEntity && !existing.contains(entity)) { - CompoundNBT nbt = new CompoundNBT(); if (!entity.saveAsPassenger(nbt)) throw new IllegalStateException("Could not serialize death drop"); + for (Entity entity : Native117Worlds.entities(world).collect(java.util.stream.Collectors.toList())) if (entity instanceof ItemEntity && !existing.contains(entity)) { + CompoundTag nbt = new CompoundTag(); if (!entity.saveAsPassenger(nbt)) throw new IllegalStateException("Could not serialize death drop"); drops.add(encode(nbt)); } result.add("drops", drops); @@ -319,16 +413,16 @@ private static JsonObject respawn(MinecraftServer server, ServerWorld world, Jso try { if (replacement != null) server.getPlayerList().remove(replacement); } finally { - world.getGameRules().getRule(net.minecraft.world.GameRules.RULE_KEEPINVENTORY).set(oldSource, server); - destination.getGameRules().getRule(net.minecraft.world.GameRules.RULE_KEEPINVENTORY).set(oldTarget, server); + world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(oldSource, server); + destination.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(oldTarget, server); } } } - private static JsonObject spawnDiagnostics(ServerPlayerEntity player, CompoundNBT nbt) { + private static JsonObject spawnDiagnostics(ServerPlayer player, CompoundTag nbt) { JsonObject result = new JsonObject(); result.addProperty("queryDimension", 0); - result.addProperty("playerWorldDimension", Native116Worlds.legacyId(player.getLevel().dimension())); - result.addProperty("spawnDimension", Native116Worlds.legacyId(player.getRespawnDimension())); + result.addProperty("playerWorldDimension", Native117Worlds.legacyId(player.getLevel().dimension())); + result.addProperty("spawnDimension", Native117Worlds.legacyId(player.getRespawnDimension())); BlockPos bed = player.getRespawnPosition(); result.add("bedLocation", bed == null ? JsonNull.INSTANCE : JSON.toJsonTree(new int[] {bed.getX(), bed.getY(), bed.getZ()})); result.addProperty("spawnForced", player.isRespawnForced()); @@ -343,27 +437,27 @@ private static JsonObject spawnDiagnostics(ServerPlayerEntity player, CompoundNB return result; } - private static final class OfflineConnection extends ServerPlayNetHandler { - OfflineConnection(MinecraftServer server, ServerPlayerEntity player) { super(server, new NetworkManager(PacketDirection.SERVERBOUND), player); } - @Override public void send(IPacket packet) { /* Deliberately no socket or client login. */ } - @Override public void send(IPacket packet, GenericFutureListener> listener) { + private static final class OfflineConnection extends ServerGamePacketListenerImpl { + OfflineConnection(MinecraftServer server, ServerPlayer player) { super(server, new Connection(PacketFlow.SERVERBOUND), player); } + @Override public void send(Packet packet) { /* Deliberately no socket or client login. */ } + @Override public void send(Packet packet, GenericFutureListener> listener) { // Death-message packets use this overload; no native network work is queued. } } - private static CompoundNBT entityNbt(Entity entity) { - if (entity instanceof LeashKnotEntity) { - CompoundNBT nbt = entity.saveWithoutId(new CompoundNBT()); + private static CompoundTag entityNbt(Entity entity) { + if (entity instanceof LeashFenceKnotEntity) { + CompoundTag nbt = entity.saveWithoutId(new CompoundTag()); nbt.putString("id", entity.getType().getRegistryName().toString()); return nbt; } - CompoundNBT nbt = new CompoundNBT(); + CompoundTag nbt = new CompoundTag(); if (!entity.saveAsPassenger(nbt)) throw new IllegalStateException("Could not serialize dimension witness"); return nbt; } - private static JsonObject encode(CompoundNBT nbt) throws Exception { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); CompressedStreamTools.writeCompressed(nbt, bytes); + private static JsonObject encode(CompoundTag nbt) throws Exception { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); NbtIo.writeCompressed(nbt, bytes); JsonObject result = new JsonObject(); result.addProperty("nbtBase64", Base64.getEncoder().encodeToString(bytes.toByteArray())); return result; } } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ElectricOperationalCases.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ElectricOperationalCases.java index 442ecfd5..67c466b8 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ElectricOperationalCases.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ElectricOperationalCases.java @@ -3,21 +3,21 @@ import java.util.List; import java.util.UUID; import com.mojang.authlib.GameProfile; -import net.minecraft.entity.EntityType; -import net.minecraft.entity.Entity; -import net.minecraft.entity.ai.attributes.Attributes; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.entity.monster.HuskEntity; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.inventory.IInventory; -import net.minecraft.item.ItemStack; -import net.minecraft.util.Direction; -import net.minecraft.state.properties.BlockStateProperties; -import net.minecraft.scoreboard.ScorePlayerTeam; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.math.AxisAlignedBB; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.GameRules; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.monster.Husk; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.phys.AABB; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.GameRules; import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fluids.capability.CapabilityFluidHandler; import net.minecraftforge.fluids.capability.IFluidHandler; @@ -26,7 +26,7 @@ /** Independent operating contracts. Only setup seeds inputs; all work uses world ticks. */ public final class ElectricOperationalCases { - private static final String EA = "electricadvantage:"; + private static final String EA = "baseelectric:"; private static final String ELECTRICITY = "electricity"; private static final String GREENHOUSE = "greenhouse"; private static final int Y = 2; @@ -167,7 +167,7 @@ private static void refinery(List cases) { int x = CELLS[i]; c.block(x, Y, Z, EA + "plastic_refinery"); if (i != 1) c.electricSupply(x - 1, Y, Z, 4000); - if (i != 2) reserve(c, x, Y, Z, "getTank", "poweradvantage:refined_oil", i == 4 ? 99 : 200); + if (i != 2) reserve(c, x, Y, Z, "getTank", "basepower:refined_oil", i == 4 ? 99 : 200); } rejectFluid(c, CELLS[2], "minecraft:water", 200); c.put(CELLS[3], Y, Z, 0, EA + "petrolplastic_ingot", 64); @@ -191,15 +191,15 @@ private static void still(List cases) { int x = CELLS[i]; c.block(x, Y, Z, EA + "electric_still"); if (i != 1) c.electricSupply(x - 1, Y, Z, 1000); - if (i != 2) reserve(c, x, Y, Z, "getInputTank", "poweradvantage:crude_oil", i == 4 ? 1 : 40); + if (i != 2) reserve(c, x, Y, Z, "getInputTank", "basepower:crude_oil", i == 4 ? 1 : 40); } rejectFluid(c, CELLS[2], "minecraft:water", 40); - reserve(c, CELLS[3], Y, Z, "getOutputTank", "poweradvantage:refined_oil", 1000); + reserve(c, CELLS[3], Y, Z, "getOutputTank", "basepower:refined_oil", 1000); equal(c, "setup.fullOutput", tankAmount(c, CELLS[3], 1), 1000); }, c -> { equal(c, "positive.input", tankAmount(c, CELLS[0], 0), 0); equal(c, "positive.output", tankAmount(c, CELLS[0], 1), 20); - fluidIs(c, CELLS[0], 1, "poweradvantage:refined_oil"); + fluidIs(c, CELLS[0], 1, "basepower:refined_oil"); close(c, "positive.energyDebit", 1000 - pairEnergy(c, CELLS[0]), 320, 0.01f); for (int i = 1; i < CELLS.length; i++) { equal(c, "negative" + i + ".input", tankAmount(c, CELLS[i], 0), i == 2 ? 0 : i == 4 ? 1 : 40); @@ -212,7 +212,7 @@ private static void still(List cases) { + "mutation. The 48 finite mB then operate on natural ticks, making 24 mB refined oil for 384 electricity.", 80, c -> { c.block(8, Y, Z, EA + "electric_still"); c.electricSupply(7, Y, Z, 1000); - net.minecraft.fluid.Fluid oil = ForgeRegistries.FLUIDS.getValue(new ResourceLocation("poweradvantage:crude_oil")); + net.minecraft.world.level.material.Fluid oil = ForgeRegistries.FLUIDS.getValue(new ResourceLocation("basepower:crude_oil")); c.require(oil != null, "Crude oil exists for faced automation"); int stored = 0; for (Direction face : Direction.values()) { @@ -226,12 +226,12 @@ private static void still(List cases) { }, c -> { equal(c, "faced.input", tankAmount(c, 8, 0), 0); equal(c, "faced.output", tankAmount(c, 8, 1), 24); - fluidIs(c, 8, 1, "poweradvantage:refined_oil"); + fluidIs(c, 8, 1, "basepower:refined_oil"); close(c, "faced.energyDebit", 1000 - pairEnergy(c, 8), 384, 0.01f); for (Direction face : Direction.values()) { FluidStack simulated = fluids(c, 8, face).drain(24, IFluidHandler.FluidAction.SIMULATE); equal(c, "face." + face + ".drainable", simulated.getAmount(), 24); - c.require(new ResourceLocation("poweradvantage:refined_oil").equals(simulated.getFluid().getRegistryName()), "Face offers the real refined product " + face); + c.require(new ResourceLocation("basepower:refined_oil").equals(simulated.getFluid().getRegistryName()), "Face offers the real refined product " + face); equal(c, "face." + face + ".drainSimulationUnchanged", tankAmount(c, 8, 1), 24); } })); @@ -323,7 +323,7 @@ private static void growth(List cases) { equal(c, "scarce.output", output(c, 9, 3, 6, "minecraft:sugar_cane"), 0); equal(c, "full.seed", slotCount(c, 23, 0, "minecraft:sugar_cane"), 1); equal(c, "full.output", output(c, 23, 3, 6, "minecraft:sugar_cane"), 384); - AxisAlignedBB bay = new AxisAlignedBB(c.at(0, 0, 0), c.at(31, 31, 31)); + AABB bay = new AABB(c.at(0, 0, 0), c.at(31, 31, 31)); equal(c, "overflowItems", c.world.getEntitiesOfClass(ItemEntity.class, bay).size(), 0); float soil = ((Number)c.invoke(8, Y, Z, "getSoil", new Class[0])).floatValue(); close(c, "scarce.convertedGrowth", 1000 - soil, 799, 0.1f); @@ -394,7 +394,7 @@ private static void hydro(List cases) { c.block(27, Y, 22, EA + "hydroelectric_generator"); }, c -> { c.require(!c.world.getFluidState(c.at(8, 1, Z)).isEmpty(), "Water reached the turbine"); - net.minecraft.util.math.vector.Vector3d flow = c.world.getFluidState(c.at(8, 1, Z)).getFlow(c.world, c.at(8, 1, Z)); + net.minecraft.world.phys.Vec3 flow = c.world.getFluidState(c.at(8, 1, Z)).getFlow(c.world, c.at(8, 1, Z)); c.require(flow.x * flow.x + flow.z * flow.z > 0, "Turbine sees actual horizontal flow"); float generated = c.energy(8, Y, Z, ELECTRICITY) + c.energy(8, Y + 1, Z, ELECTRICITY); @@ -407,7 +407,7 @@ private static void hydro(List cases) { close(c, "dry.generated", c.energy(27, Y, 22, ELECTRICITY), 0, 0.001f); int turbines = 0; for (Entity actor : c.world.getEntitiesOfClass(Entity.class, - new AxisAlignedBB(c.at(0, 0, 0), c.at(31, 31, 31)))) { + new AABB(c.at(0, 0, 0), c.at(31, 31, 31)))) { if (!new ResourceLocation(EA + "hydroturbine").equals(actor.getType().getRegistryName())) continue; turbines++; c.record("hydro.motion/" + actor.getUUID(), actor.getDeltaMovement().toString()); @@ -577,7 +577,7 @@ private static void batteries(List cases) { private static void turret(List cases) { for (int variant = 0; variant < 4; variant++) { final int mode = variant; - final HuskEntity[] target = new HuskEntity[1]; + final Husk[] target = new Husk[1]; cases.add(new MachineScenario("electric/turret-" + new String[] {"hit", "no-power", "scarce", "occluded"}[mode], "Natural turret acquisition must damage a real stationary hostile entity and debit a 1000-unit " + "shot plus tracking energy. No electricity, sub-shot reserve and a solid wall cannot damage it.", 80, c -> { @@ -586,7 +586,7 @@ private static void turret(List cases) { if (mode != 1) c.power(8, 1, Z, ELECTRICITY, mode == 2 ? 999 : 1500); if (mode == 3) for (int y = 1; y <= 4; y++) for (int z = Z - 2; z <= Z + 2; z++) c.block(12, y, z, "minecraft:stone"); - HuskEntity husk = EntityType.HUSK.create(c.world); + Husk husk = EntityType.HUSK.create(c.world); c.require(husk != null, "Created a real hostile target"); husk.moveTo(c.at(16, 1, Z).getX() + 0.5, c.at(16, 1, Z).getY(), c.at(16, 1, Z).getZ() + 0.5, 0, 0); husk.setNoAi(true); @@ -596,13 +596,13 @@ private static void turret(List cases) { husk.getAttribute(Attributes.ARMOR).setBaseValue(0); equal(c, "target.fixtureArmor", husk.getArmorValue(), 0); // Fire immunity isolates the seven-point laser impact from its burn side effect. - husk.addEffect(new net.minecraft.potion.EffectInstance(net.minecraft.potion.Effects.FIRE_RESISTANCE, 200, 0)); + husk.addEffect(new net.minecraft.world.effect.MobEffectInstance(net.minecraft.world.effect.MobEffects.FIRE_RESISTANCE, 200, 0)); c.require(c.world.addFreshEntity(husk), "Spawned target into the real server world"); target[0] = husk; c.record("target.initialHealth", husk.getHealth()); c.record("target.entityUuid", husk.getUUID().toString()); }, c -> { - HuskEntity husk = target[0]; + Husk husk = target[0]; try { c.require(husk != null && husk.isAlive(), "Hostile target survives for impact measurement"); c.record("target.health", husk.getHealth()); @@ -618,14 +618,14 @@ private static void turret(List cases) { if (mode == 3) between(c, "occluded.trackingDebit", 1500 - c.energy(8, 1, Z, ELECTRICITY), 70, 80); } } finally { - if (husk != null) husk.remove(); + if (husk != null) husk.discard(); target[0] = null; } })); } final FakePlayer[] owner = new FakePlayer[1]; - final ScorePlayerTeam[] team = new ScorePlayerTeam[1]; - final HuskEntity[] victim = new HuskEntity[1]; + final PlayerTeam[] team = new PlayerTeam[1]; + final Husk[] victim = new Husk[1]; cases.add(new MachineScenario("electric/turret-owner-credit", "A real naturally acquired hostile target is hit and its last attacking mob is the turret's " + "registered owner, proving ownership has a gameplay effect beyond saved owner NBT.", 40, c -> { @@ -640,8 +640,8 @@ private static void turret(List cases) { team[0] = c.world.getScoreboard().addPlayerTeam(name); c.world.getScoreboard().addPlayerToTeam(player.getScoreboardName(), team[0]); c.world.addNewPlayer(player); - c.invoke(8, 1, Z, "setOwner", new Class[] {PlayerEntity.class}, player); - HuskEntity husk = EntityType.HUSK.create(c.world); + c.invoke(8, 1, Z, "setOwner", new Class[] {Player.class}, player); + Husk husk = EntityType.HUSK.create(c.world); c.require(husk != null, "Created ownership target"); victim[0] = husk; husk.moveTo(c.at(16, 1, Z).getX() + 0.5, c.at(16, 1, Z).getY(), c.at(16, 1, Z).getZ() + 0.5, 0, 0); @@ -651,7 +651,7 @@ private static void turret(List cases) { c.require(husk.getAttribute(Attributes.ARMOR) != null, "Ownership target has an armor attribute"); husk.getAttribute(Attributes.ARMOR).setBaseValue(0); equal(c, "ownerCredit.fixtureArmor", husk.getArmorValue(), 0); - husk.addEffect(new net.minecraft.potion.EffectInstance(net.minecraft.potion.Effects.FIRE_RESISTANCE, 200, 0)); + husk.addEffect(new net.minecraft.world.effect.MobEffectInstance(net.minecraft.world.effect.MobEffects.FIRE_RESISTANCE, 200, 0)); c.require(c.world.addFreshEntity(husk), "Ownership target is in the real world"); c.record("owner.profile", name); }, c -> { @@ -663,9 +663,9 @@ private static void turret(List cases) { c.record("ownerCredit.attacker", victim[0].getLastHurtByMob().getUUID().toString()); between(c, "ownerCredit.energyDebit", 1500 - c.energy(8, 1, Z, ELECTRICITY), 1024, 1032); } finally { - if (victim[0] != null) victim[0].remove(); + if (victim[0] != null) victim[0].discard(); if (owner[0] != null) { - owner[0].remove(); + owner[0].discard(); c.world.players().remove(owner[0]); } if (team[0] != null) c.world.getScoreboard().removePlayerTeam(team[0]); @@ -719,7 +719,7 @@ private static int inputCount(MachineContext c, int x, int slots, String id) { } private static int output(MachineContext c, int x, int first, int slots, String id) { - IInventory inventory = c.inventory(x, Y, Z); + Container inventory = c.inventory(x, Y, Z); int count = 0; for (int slot = first; slot < first + slots; slot++) count += stackCount(inventory.getItem(slot), id); return count; @@ -743,7 +743,7 @@ private static IFluidHandler fluids(MachineContext c, int x, Direction face) { private static void reserve(MachineContext c, int x, int y, int z, String method, String id, int amount) { cyano.poweradvantage.api.fluid.FluidTank tank = (cyano.poweradvantage.api.fluid.FluidTank) c.invoke(x, y, z, method, new Class[0]); - net.minecraft.fluid.Fluid fluid = ForgeRegistries.FLUIDS.getValue(new ResourceLocation(id)); + net.minecraft.world.level.material.Fluid fluid = ForgeRegistries.FLUIDS.getValue(new ResourceLocation(id)); c.require(fluid != null && amount >= 0 && amount <= tank.getCapacity(), "Valid finite fluid reserve " + id); tank.setFluid(new cyano.poweradvantage.api.fluid.FluidStack(fluid, amount)); c.tile(x, y, z).setChanged(); @@ -767,7 +767,7 @@ private static void fluidIs(MachineContext c, int x, int tank, String id) { } private static void rejectFluid(MachineContext c, int x, String id, int amount) { - net.minecraft.fluid.Fluid fluid = ForgeRegistries.FLUIDS.getValue(new ResourceLocation(id)); + net.minecraft.world.level.material.Fluid fluid = ForgeRegistries.FLUIDS.getValue(new ResourceLocation(id)); c.require(fluid != null, "Negative-control fluid exists: " + id); equal(c, "invalidFluid.accepted", fluids(c, x).fill(new FluidStack(fluid, amount), IFluidHandler.FluidAction.EXECUTE), 0); } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/HammerQueryGate.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/HammerQueryGate.java new file mode 100644 index 00000000..1c1d7321 --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/HammerQueryGate.java @@ -0,0 +1,29 @@ +package zone.moddev.mc.advantageworks; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** Preconditions for one synchronous native hammer call, not its outcome. */ +final class HammerQueryGate { + private HammerQueryGate() {} + + static boolean ready(boolean accepted, boolean removed, boolean inputMatches, JsonObject before) { + if (!accepted || removed || !inputMatches || before == null + || !flag(before.get("sourceInAabb")) || !flag(before.get("uuidLookupIsSource"))) return false; + JsonElement items = before.get("items"); + return items != null && items.isJsonArray() && items.getAsJsonArray().size() == 1 + && items.getAsJsonArray().get(0).isJsonObject() + && flag(items.getAsJsonArray().get(0).getAsJsonObject().get("isSource")); + } + + static boolean selectorObserved(JsonObject selection) { + JsonElement status = selection == null ? null : selection.get("status"); + if (status == null || !status.isJsonPrimitive() || !status.getAsJsonPrimitive().isString()) return false; + return "MATCH".equals(status.getAsString()) || "NO_MATCH".equals(status.getAsString()); + } + + private static boolean flag(JsonElement value) { + return value != null && value.isJsonPrimitive() && value.getAsJsonPrimitive().isBoolean() + && value.getAsBoolean(); + } +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/JsonNbtConverter.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/JsonNbtConverter.java index 353c1f00..fe88f03d 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/JsonNbtConverter.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/JsonNbtConverter.java @@ -4,14 +4,14 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; -import net.minecraft.nbt.INBT; -import net.minecraft.nbt.ByteNBT; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.DoubleNBT; -import net.minecraft.nbt.IntNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.LongNBT; -import net.minecraft.nbt.StringNBT; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.StringTag; import java.util.Map; @@ -19,34 +19,34 @@ final class JsonNbtConverter { private JsonNbtConverter() { } - static CompoundNBT toCompound(JsonObject object) { - CompoundNBT result = new CompoundNBT(); + static CompoundTag toCompound(JsonObject object) { + CompoundTag result = new CompoundTag(); for (Map.Entry entry : object.entrySet()) { result.put(entry.getKey(), toTag(entry.getValue())); } return result; } - private static INBT toTag(JsonElement element) { + private static Tag toTag(JsonElement element) { if (element == null || element.isJsonNull()) { throw new IllegalArgumentException("NBT values may not be null"); } if (element.isJsonObject()) return toCompound(element.getAsJsonObject()); if (element.isJsonArray()) { - ListNBT result = new ListNBT(); + ListTag result = new ListTag(); JsonArray array = element.getAsJsonArray(); for (JsonElement child : array) result.add(toTag(child)); return result; } JsonPrimitive primitive = element.getAsJsonPrimitive(); - if (primitive.isBoolean()) return ByteNBT.valueOf((byte) (primitive.getAsBoolean() ? 1 : 0)); - if (primitive.isString()) return StringNBT.valueOf(primitive.getAsString()); + if (primitive.isBoolean()) return ByteTag.valueOf((byte) (primitive.getAsBoolean() ? 1 : 0)); + if (primitive.isString()) return StringTag.valueOf(primitive.getAsString()); String number = primitive.getAsString(); if (number.contains(".") || number.contains("e") || number.contains("E")) { - return DoubleNBT.valueOf(primitive.getAsDouble()); + return DoubleTag.valueOf(primitive.getAsDouble()); } long value = primitive.getAsLong(); return value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE - ? IntNBT.valueOf((int) value) : LongNBT.valueOf(value); + ? IntTag.valueOf((int) value) : LongTag.valueOf(value); } } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleNaturalTickObserver.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleNaturalTickObserver.java new file mode 100644 index 00000000..22a4d202 --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleNaturalTickObserver.java @@ -0,0 +1,170 @@ +package zone.moddev.mc.advantageworks; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.TickingBlockEntity; +import net.minecraft.world.level.chunk.LevelChunk; + +/** Observes the inner native dispatch; an outer ticker invocation can be skipped. */ +final class LifecycleNaturalTickObserver implements AutoCloseable { + final BlockEntity tile; + final TickingBlockEntity registration; + final Object bound; + final Field bindingField, delegateField; + final BlockEntityTicker original; + final Capture capture = new Capture(); + private BlockEntityTicker observer; + + @SuppressWarnings("unchecked") + LifecycleNaturalTickObserver(Level world, BlockEntity tile) { + this.tile = tile; + List found = new ArrayList<>(); + for (TickingBlockEntity entry : NativeBlockEntityTicks.registrations(world)) { + if (!entry.isRemoved() && entry.getPos().equals(tile.getBlockPos())) found.add(entry); + } + if (found.size() != 1) throw new IllegalStateException("Expected exactly one native ticker registration"); + registration = found.get(0); + requireNativeInner(registration.getClass()); + bindingField = field(registration.getClass(), TickingBlockEntity.class); + bound = get(bindingField, registration); + requireNativeInner(bound.getClass()); + if (get(field(bound.getClass(), BlockEntity.class), bound) != tile) + throw new IllegalStateException("Native ticker binds another tile instance"); + delegateField = field(bound.getClass(), BlockEntityTicker.class); + original = (BlockEntityTicker) get(delegateField, bound); + if (original == null) throw new IllegalStateException("Native ticker has no delegate"); + } + + private static void requireNativeInner(Class type) { + if (type.getEnclosingClass() != LevelChunk.class) + throw new IllegalStateException("Not a native LevelChunk ticker binding: " + type.getName()); + } + + private static Field field(Class type, Class fieldType) { + Field selected = null; + for (Field field : type.getDeclaredFields()) { + if (field.getType() != fieldType) continue; + if (selected != null) throw new IllegalStateException("Ambiguous native ticker layout"); + selected = field; + } + if (selected == null) throw new IllegalStateException("Unknown native ticker layout"); + selected.setAccessible(true); + return selected; + } + + private static Object get(Field field, Object owner) { + try { return field.get(owner); } + catch (IllegalAccessException error) { throw new IllegalStateException(error); } + } + + private void set(Object value) { + try { delegateField.set(bound, value); } + catch (IllegalAccessException error) { throw new IllegalStateException(error); } + } + + boolean current(Level world) { + return NativeBlockEntityTicks.registrations(world).stream().filter(entry -> entry == registration).count() == 1 + && get(bindingField, registration) == bound && !registration.isRemoved() + && tile.getLevel() == world && !tile.isRemoved(); + } + + void install(Level expectedWorld, long callbackTime, BooleanSupplier inWindow, + Supplier> witness, Supplier> prerequisites) { + if (observer != null || get(delegateField, bound) != original || !current(expectedWorld)) + throw new IllegalStateException("Changed or already observed native binding"); + Thread expectedThread = Thread.currentThread(); + observer = (world, pos, state, actual) -> { + List stack = new ArrayList<>(); + boolean nativeBound = false, nativeLoop = false; + for (StackTraceElement frame : Thread.currentThread().getStackTrace()) { + // Use explicit fields so module/loader prefixes cannot change the receipt format. + stack.add(frame.getClassName() + "." + frame.getMethodName() + + "(" + frame.getFileName() + ":" + frame.getLineNumber() + ")"); + nativeBound |= frame.getClassName().equals(bound.getClass().getName()); + nativeLoop |= nativeLoopFrame(frame); + } + boolean natural = nativeBound && nativeLoop; + Map actualFlags = prerequisites.get(); + boolean valid = natural && world == expectedWorld && actual == tile && pos.equals(tile.getBlockPos()) + && Thread.currentThread() == expectedThread && world.getGameTime() == callbackTime + && inWindow.getAsBoolean() && current(world) && tile.getType().isValid(state) + && actualFlags.values().stream().allMatch(Boolean.TRUE::equals); + boolean first = capture.enter(world.getGameTime(), valid, natural, stack, witness.get()); + if (first) capture.prerequisites = new LinkedHashMap<>(actualFlags); + // Forward only the callback the native BoundTickingBlockEntity just dispatched. + // Commands and phase observers never invoke this delegate themselves. + original.tick(world, pos, state, actual); + capture.exit(first, witness.get()); + }; + set(observer); + } + + static boolean nativeLoopFrame(StackTraceElement frame) { + return frame.getClassName().equals(Level.class.getName()) + && ("tickBlockEntities".equals(frame.getMethodName()) || "m_46463_".equals(frame.getMethodName())); + } + + Map report() { + Map result = capture.report(); + result.put("registrationIdentity", System.identityHashCode(registration)); + result.put("boundIdentity", System.identityHashCode(bound)); + result.put("delegateIdentity", System.identityHashCode(original)); + result.put("delegateClass", original.getClass().getName()); + result.put("boundClass", bound.getClass().getName()); + result.put("tileClass", tile.getClass().getName()); + result.put("tileCodeSource", String.valueOf(tile.getClass().getProtectionDomain().getCodeSource())); + return result; + } + + @Override public void close() { + if (observer == null) return; + if (get(delegateField, bound) != observer) throw new IllegalStateException("Observed native delegate was replaced"); + set(original); + observer = null; + } + + static final class Capture { + int calls, completed; + long worldTime; + boolean valid, nativeStackValidated; + List nativeStack; + Map before, after; + Map prerequisites; + + boolean enter(long time, boolean eligible, boolean natural, List stack, Map state) { + calls++; + if (calls != 1) return false; + worldTime = time; + valid = eligible; + nativeStackValidated = natural; + nativeStack = new ArrayList<>(stack); + before = state; + return true; + } + + void exit(boolean first, Map state) { + completed++; + if (first) after = state; + } + + boolean complete() { return calls == 1 && completed == 1 && valid && nativeStackValidated && after != null; } + + Map report() { + Map result = new LinkedHashMap<>(); + result.put("calls", calls); result.put("completed", completed); + result.put("valid", valid); result.put("nativeStackValidated", nativeStackValidated); + result.put("worldTime", worldTime); result.put("nativeStack", nativeStack); + result.put("before", before); result.put("after", after); + result.put("prerequisiteFlags", prerequisites); + 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 fb012361..7dffec97 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 @@ -18,20 +18,20 @@ import java.util.Map; import java.util.Set; import java.util.function.BooleanSupplier; -import net.minecraft.command.Commands; -import net.minecraft.block.Blocks; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.ChunkPos; -import net.minecraft.world.server.ServerWorld; -import net.minecraft.world.chunk.Chunk; +import net.minecraft.commands.Commands; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.LevelChunk; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.world.ChunkEvent; import net.minecraftforge.event.TickEvent; import net.minecraftforge.fml.ModList; import net.minecraftforge.event.RegisterCommandsEvent; -import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry; +import zone.moddev.mc.basepower.conduitnetwork.ConduitRegistry; /** Observes real tile movement and chunk lifecycles only in marked disposable worlds. */ final class LifecycleProbe { @@ -46,6 +46,21 @@ final class LifecycleProbe { private final List> chunkEvents = new ArrayList<>(); private boolean watching; private boolean tracingLoads; + private RoundTripRequest roundTrip; + + private static final class RoundTripRequest { + final ServerLevel world; + final String operation, label; + final LifecycleRoundTrip window = new LifecycleRoundTrip(System.nanoTime()); + final Map bindings = new LinkedHashMap<>(); + final Map> prerequisites = new LinkedHashMap<>(); + final List> attempts = new ArrayList<>(); + String completionPhase; + + RoundTripRequest(ServerLevel world, String operation, String label) { + this.world = world; this.operation = operation; this.label = label; + } + } static final class Fixture { String id; @@ -90,7 +105,7 @@ static void validate(Fixture fixture, BooleanSupplier mineralogyLoaded) { for (int[] pos : fixture.witnesses) requireInside(fixture, pos); if (fixture.bucket != null) { boolean mineralogy = "mineralogy:crude_oil_bucket".equals(fixture.bucket); - if (!fixture.bucket.matches("poweradvantage:(crude|refined)_oil_bucket") && !mineralogy) + if (!fixture.bucket.matches("basepower:(crude|refined)_oil_bucket") && !mineralogy) throw new IllegalArgumentException("Unsupported oil bucket"); if (mineralogy && !mineralogyLoaded.getAsBoolean()) throw new IllegalArgumentException("Mineralogy oil bucket requires mineralogy"); @@ -118,8 +133,8 @@ static void validate(Fixture fixture, BooleanSupplier mineralogyLoaded) { for (int axis = 0; axis < 3; axis++) distance += Math.abs(fixture.from[axis] - fixture.to[axis]); if ((fixture.route == null && distance != 1) || fixture.initialEnergy <= 0 || !Float.isFinite(fixture.initialEnergy)) throw new IllegalArgumentException("Invalid movement fixture"); - if (!"steamadvantage:steam_drill".equals(fixture.block) - && !"electricadvantage:electric_drill".equals(fixture.block)) + if (!"basesteam:steam_drill".equals(fixture.block) + && !"baseelectric:electric_drill".equals(fixture.block)) throw new IllegalArgumentException("Not an Advantage drill"); if (fixture.route != null) { if (fixture.route.size() < 2 || fixture.route.size() > 8 || fixture.controls == null @@ -180,13 +195,18 @@ private void starting(RegisterCommandsEvent event) { String operation = StringArgumentType.getString(context, "operation"); String label = StringArgumentType.getString(context, "label"); if (!label.matches("[A-Za-z0-9_-]+")) throw new IllegalArgumentException("Invalid label"); - execute(context.getSource().getLevel(), operation, label); - System.out.println("ADVLIFECYCLE COMPLETE " + operation + " " + label); + if (roundTrip != null) throw new IllegalStateException("Lifecycle round trip is busy"); + if ("legacy-read".equals(operation) || "changed-target-read".equals(operation)) { + queueRoundTrip(context.getSource().getLevel(), operation, label); + } else { + execute(context.getSource().getLevel(), operation, label); + System.out.println("ADVLIFECYCLE COMPLETE " + operation + " " + label); + } return 1; })))); } - private void execute(ServerWorld world, String operation, String label) { + private void execute(ServerLevel world, String operation, String label) { if (operation.startsWith("release-") || operation.startsWith("load-")) { boolean acquire = operation.startsWith("load-"); String group = operation.substring(acquire ? 5 : 8); @@ -212,15 +232,15 @@ private void execute(ServerWorld world, String operation, String label) { if (oil.bucket == null) continue; BlockPos source = position(oil.source); if (!world.isEmptyBlock(source)) throw new IllegalStateException("Oil source is not empty: " + oil.id); - net.minecraft.item.Item item = net.minecraftforge.registries.ForgeRegistries.ITEMS.getValue( - new net.minecraft.util.ResourceLocation(oil.bucket)); - if (!(item instanceof net.minecraft.item.BucketItem) - || !((net.minecraft.item.BucketItem) item).emptyBucket(null, world, source, null)) + net.minecraft.world.item.Item item = net.minecraftforge.registries.ForgeRegistries.ITEMS.getValue( + new net.minecraft.resources.ResourceLocation(oil.bucket)); + if (!(item instanceof net.minecraft.world.item.BucketItem) + || !((net.minecraft.world.item.BucketItem) item).emptyContents(null, world, source, null)) throw new IllegalStateException("Bucket placement failed: " + oil.id); String expected = oil.bucket.substring(0, oil.bucket.length() - "_bucket".length()); - net.minecraft.fluid.FluidState state = world.getFluidState(source); + net.minecraft.world.level.material.FluidState state = world.getFluidState(source); if (!state.isSource() || !expected.equals( - net.minecraft.util.registry.Registry.FLUID.getKey(state.getType()).toString())) + net.minecraft.core.Registry.FLUID.getKey(state.getType()).toString())) throw new IllegalStateException("Bucket placed the wrong fluid: " + oil.id); } break; @@ -229,7 +249,7 @@ private void execute(ServerWorld world, String operation, String label) { for (Fixture fixture : fixtures.values()) { if (fixture.from == null) continue; int[] current = fixture.route == null ? fixture.from : fixture.route.get(routeSteps.getOrDefault(fixture.id, 0)); - TileEntity tile = world.getBlockEntity(position(current)); + BlockEntity tile = world.getBlockEntity(position(current)); if (!(tile instanceof PoweredEntity) || !fixture.block.equals(world.getBlockState(tile.getBlockPos()).getBlock().getRegistryName().toString())) throw new IllegalStateException("Missing drill " + fixture.id); float reserve = "low-prime".equals(operation) ? ("steam".equals(fixture.energyType) ? 9 : 249) : fixture.initialEnergy; @@ -257,7 +277,7 @@ private void execute(ServerWorld world, String operation, String label) { Fixture fixture = fixtures.get("C-FLUID"); if (fixture == null) throw new IllegalStateException("Missing fluid fixture"); int accepted = FluidNetworkApi.offerFluid(world, position(fixture.witnesses.get(0)), - new FluidStack(net.minecraft.fluid.Fluids.WATER, 1000), 1000); + new FluidStack(net.minecraft.world.level.material.Fluids.WATER, 1000), 1000); int expected = "offer-blocked".equals(operation) ? 0 : 1000; if (accepted != expected) throw new IllegalStateException("Fluid network accepted " + accepted + " instead of " + expected); break; @@ -275,33 +295,201 @@ private void execute(ServerWorld world, String operation, String label) { case "snapshot": break; case "legacy-read": case "changed-target-read": - for (Fixture drill : fixtures.values()) { - if (drill.to == null) continue; - TileEntity tile = world.getBlockEntity(position(drill.to)); - if (!(tile instanceof PoweredEntity) - || !drill.block.equals(world.getBlockState(position(drill.to)).getBlock().getRegistryName().toString())) - throw new IllegalStateException("Missing restored drill " + drill.id); - CompoundNBT saved = tile.save(new CompoundNBT()); - if (!saved.contains("targetX", 99) || saved.getShort("progress") <= 0) - throw new IllegalStateException("Drill must be mid-block " + drill.id); - if ("legacy-read".equals(operation)) { - saved.remove("TargetState"); - saved.remove("TargetStates"); - } else { - int[] target = {saved.getInt("targetX"), saved.getInt("targetY"), saved.getInt("targetZ")}; - requireInside(drill, target); - world.setBlock(position(target), Blocks.BEDROCK.defaultBlockState(), 3); - } - // Round-trip real saved state; never inject quantities, energy or progress. - tile.load(tile.getBlockState(), saved); - tile.setChanged(); - } - break; + throw new IllegalStateException("Saved reads require a queued native START/END window"); default: throw new IllegalArgumentException("Unknown lifecycle operation " + operation); } snapshot(world, label); } + private static void requireNewResult(String label) { + File directory = new File(System.getProperty("advantageWorks.resultsDir", "advantage-works-results")); + if (new File(directory, "lifecycle-" + label + ".json").exists() + || new File(directory, "lifecycle-" + label + ".json.tmp").exists()) + throw new IllegalStateException("Refusing to overwrite lifecycle evidence: " + label); + } + + private void queueRoundTrip(ServerLevel world, String operation, String label) { + for (String suffix : Arrays.asList("", "-initial", "-end")) requireNewResult(label + suffix); + roundTrip = new RoundTripRequest(world, operation, label); + System.out.println("ADVLIFECYCLE QUEUED " + operation + " " + label); + } + + private static BlockEntity existingTile(ServerLevel world, BlockPos pos) { + LevelChunk chunk = world.getChunkSource().getChunkNow(pos.getX() >> 4, pos.getZ() >> 4); + return chunk == null ? null : chunk.getBlockEntity(pos, LevelChunk.EntityCreationType.CHECK); + } + + private boolean roundTripPrerequisites(RoundTripRequest request) { + ServerLevel world = request.world; + boolean allChunks = true; + for (long key : chunks()) { + 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); + } + request.bindings.clear(); request.prerequisites.clear(); + boolean ready = true; + for (Fixture fixture : fixtures.values()) { + if (fixture.to == null) continue; + 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()); + 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("targetChunkLoaded", world.getChunkSource().getChunkNow(target.getX() >> 4, target.getZ() >> 4) != null); + flags.put("targetTicking", world.isPositionTickingWithEntitiesLoaded(target)); + flags.put("worldAttached", tile != null && tile.getLevel() == world); + flags.put("notRemoved", tile != null && !tile.isRemoved()); + flags.put("blockTypeValid", tile instanceof PoweredEntity && chunk != null + && tile.getType().isValid(chunk.getBlockState(pos)) + && fixture.block.equals(chunk.getBlockState(pos).getBlock().getRegistryName().toString())); + String bindingFailure = null; + if (tile != null) { + try { request.bindings.put(fixture.id, new LifecycleNaturalTickObserver(world, tile)); } + catch (IllegalStateException error) { bindingFailure = error.getMessage(); } + } + flags.put("nativeBindingCurrent", request.bindings.containsKey(fixture.id) + && request.bindings.get(fixture.id).current(world)); + Map row = new LinkedHashMap<>(); + row.put("flags", flags); row.put("tileIdentity", tile == null ? null : System.identityHashCode(tile)); + row.put("savedNbt", saved.toString()); row.put("target", Arrays.asList(target.getX(), target.getY(), target.getZ())); + row.put("bindingFailure", bindingFailure); + request.prerequisites.put(fixture.id, row); + ready &= flags.values().stream().allMatch(Boolean.TRUE::equals); + } + if (request.prerequisites.isEmpty()) throw new IllegalStateException("No saved-read fixtures"); + Map attempt = new LinkedHashMap<>(); + attempt.put("worldTime", world.getGameTime()); attempt.put("phase", "START"); + attempt.put("prerequisites", new LinkedHashMap<>(request.prerequisites)); + request.attempts.add(attempt); + return ready; + } + + 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()); + 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")}); + if ("changed-target-read".equals(request.operation) && !tag.contains("TargetState", 10) && !tag.contains("TargetStates", 9)) + throw new IllegalStateException("Changed-target prerequisite lacks restored saved state " + fixture.id); + saved.put(fixture.id, tag); + } + for (Fixture fixture : fixtures.values()) { + if (fixture.to == null) continue; + request.bindings.get(fixture.id).install(request.world, request.window.startWorldTime + 1, + () -> roundTrip == request && request.window.started && !request.window.finished, + () -> witness(request.world, fixture.to), () -> callbackPrerequisites(request, fixture)); + } + request.window.mutated(); + for (Map.Entry entry : saved.entrySet()) { + CompoundTag tag = entry.getValue(); + if ("legacy-read".equals(request.operation)) { + tag.remove("TargetState"); tag.remove("TargetStates"); + } else { + request.world.setBlock(new BlockPos(tag.getInt("targetX"), tag.getInt("targetY"), tag.getInt("targetZ")), + Blocks.BEDROCK.defaultBlockState(), 3); + } + BlockEntity tile = request.bindings.get(entry.getKey()).tile; + tile.load(tag); tile.setChanged(); + } + requireNewResult(request.label + "-initial"); + Map initial = observe(request.world); + initial.put("observedPhase", "START"); + new WorksResultWriter().write("lifecycle", request.label + "-initial", initial); + } + + @SuppressWarnings("unchecked") + private Map callbackPrerequisites(RoundTripRequest request, Fixture fixture) { + ServerLevel world = request.world; + BlockPos pos = position(fixture.to); + LifecycleNaturalTickObserver binding = request.bindings.get(fixture.id); + List saved = (List) request.prerequisites.get(fixture.id).get("target"); + BlockPos target = new BlockPos(saved.get(0), saved.get(1), saved.get(2)); + LevelChunk chunk = world.getChunkSource().getChunkNow(pos.getX() >> 4, pos.getZ() >> 4); + Map flags = new LinkedHashMap<>(); + flags.put("currentTile", existingTile(world, pos) == binding.tile); + flags.put("worldAttached", binding.tile.getLevel() == world); + 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("targetChunkLoaded", world.getChunkSource().getChunkNow(target.getX() >> 4, target.getZ() >> 4) != null); + flags.put("targetHasChunkAt", world.hasChunkAt(target)); + return flags; + } + + @SuppressWarnings("unchecked") + private void finishRoundTrip(RoundTripRequest request) { + Map result = observe(request.world); + Map proof = new LinkedHashMap<>(); + proof.put("protocol", LifecycleRoundTrip.PROTOCOL); proof.put("operation", request.operation); + proof.put("label", request.label); proof.put("dimension", request.world.dimension().location().toString()); + proof.put("status", request.window.failure == null ? "PASS" : "FAIL"); + proof.put("failure", request.window.failure); proof.put("startPhase", "START"); proof.put("endPhase", request.completionPhase); + proof.put("startWorldTime", request.window.startWorldTime); proof.put("endWorldTime", request.world.getGameTime()); + proof.put("mutationCount", request.window.mutations); proof.put("attempts", request.attempts); + proof.put("prerequisites", request.prerequisites); + Map callbacks = new LinkedHashMap<>(); + for (Map.Entry entry : request.bindings.entrySet()) { + callbacks.put(entry.getKey(), entry.getValue().report()); + } + proof.put("callbacks", callbacks); + result.put("observedPhase", request.completionPhase); + requireNewResult(request.label + "-end"); + new WorksResultWriter().write("lifecycle", request.label + "-end", result); + Map>> cases = (Map>>) result.get("cases"); + for (Map.Entry entry : request.bindings.entrySet()) { + Map first = entry.getValue().capture.after; + if (first == null) continue; + List> rows = cases.get(entry.getKey()); + int[] to = fixtures.get(entry.getKey()).to; + for (int i = 0; i < rows.size(); i++) { + if (Arrays.equals((int[]) rows.get(i).get("pos"), to)) rows.set(i, first); + } + } + result.put("roundTrip", proof); + for (LifecycleNaturalTickObserver binding : request.bindings.values()) binding.close(); + requireNewResult(request.label); + new WorksResultWriter().write("lifecycle", request.label, result); + roundTrip = null; + System.out.println("ADVLIFECYCLE COMPLETE " + request.operation + " " + request.label); + } + + private void roundTripTick(TickEvent.WorldTickEvent event) { + RoundTripRequest request = roundTrip; + if (request == null || event.world != request.world) return; + boolean finish = false; + try { + if (event.phase == TickEvent.Phase.START && !request.window.started) { + if (request.window.start(request.world.getGameTime(), System.nanoTime(), roundTripPrerequisites(request))) + applyRoundTrip(request); + finish = request.window.finished; + } else if (event.phase == TickEvent.Phase.END && request.window.started) { + boolean complete = request.bindings.values().stream().allMatch(binding -> binding.capture.complete() && binding.current(request.world)); + request.window.end(request.world.getGameTime(), complete); + finish = true; + } + } catch (RuntimeException error) { + request.window.fail(error.toString()); + finish = true; + } + if (finish) { + request.completionPhase = event.phase.name(); + finishRoundTrip(request); + } + } + private Set chunks() { Set result = new LinkedHashSet<>(); for (Fixture fixture : fixtures.values()) @@ -311,14 +499,14 @@ private Set chunks() { } private void unload(ChunkEvent.Unload event) { - if (event.getWorld() instanceof ServerWorld) { + if (event.getWorld() instanceof ServerLevel) { increment(unloads, event.getChunk().getPos().toLong()); recordEvent(event, "unload"); } } private void load(ChunkEvent.Load event) { - if (event.getWorld() instanceof ServerWorld) { + if (event.getWorld() instanceof ServerLevel) { increment(loads, event.getChunk().getPos().toLong()); recordEvent(event, "load"); } @@ -329,7 +517,7 @@ private void recordEvent(ChunkEvent event, String operation) { Map record = new LinkedHashMap<>(); record.put("operation", operation); record.put("pos", Arrays.asList(event.getChunk().getPos().x, event.getChunk().getPos().z)); - record.put("worldTime", ((ServerWorld) event.getWorld()).getGameTime()); + record.put("worldTime", ((ServerLevel) event.getWorld()).getGameTime()); if (tracingLoads && "load".equals(operation)) { List stack = new ArrayList<>(); for (StackTraceElement frame : Thread.currentThread().getStackTrace()) { @@ -345,19 +533,19 @@ private void increment(Map counters, long key) { if (chunks().contains(key)) counters.put(key, counters.getOrDefault(key, 0) + 1); } - private Map witness(ServerWorld world, int[] coordinates) { + private Map witness(ServerLevel world, int[] coordinates) { Map result = new LinkedHashMap<>(); result.put("pos", coordinates); BlockPos pos = position(coordinates); // No world.getChunk/getTileEntity call is permitted before this non-loading lookup. - Chunk chunk = world.getChunkSource().getChunkNow(pos.getX() >> 4, pos.getZ() >> 4); + LevelChunk chunk = world.getChunkSource().getChunkNow(pos.getX() >> 4, pos.getZ() >> 4); result.put("loaded", chunk != null); if (chunk == null) return result; result.put("block", chunk.getBlockState(pos).getBlock().getRegistryName().toString()); result.put("state", chunk.getBlockState(pos).toString()); - TileEntity tile = chunk.getBlockEntity(pos); + BlockEntity tile = chunk.getBlockEntity(pos, LevelChunk.EntityCreationType.CHECK); if (tile != null) { - result.put("nbt", tile.save(new CompoundNBT()).toString()); + result.put("nbt", tile.save(new CompoundTag()).toString()); result.put("tileIdentity", System.identityHashCode(tile)); if (tile instanceof PoweredEntity) { PoweredEntity powered = (PoweredEntity) tile; @@ -370,8 +558,9 @@ private Map witness(ServerWorld world, int[] coordinates) { } private void tick(TickEvent.WorldTickEvent event) { - if (!watching || !(event.world instanceof ServerWorld)) return; - ServerWorld world = (ServerWorld) event.world; + roundTripTick(event); + if (!watching || !(event.world instanceof ServerLevel)) return; + ServerLevel world = (ServerLevel) event.world; for (Fixture fixture : fixtures.values()) { if (fixture.from == null || completed.contains(fixture.id)) continue; int step = routeSteps.getOrDefault(fixture.id, 0); @@ -402,7 +591,11 @@ private void tick(TickEvent.WorldTickEvent event) { } } - private void snapshot(ServerWorld world, String label) { + private void snapshot(ServerLevel world, String label) { + new WorksResultWriter().write("lifecycle", label, observe(world)); + } + + private Map observe(ServerLevel world) { Map result = new LinkedHashMap<>(); Map cases = new LinkedHashMap<>(); for (Fixture fixture : fixtures.values()) { @@ -427,7 +620,7 @@ private void snapshot(ServerWorld world, String label) { result.put("movements", movements); result.put("chunkEvents", chunkEvents); result.put("routeSteps", routeSteps); - new WorksResultWriter().write("lifecycle", label, result); + return result; } private static BlockPos position(int[] coordinates) { diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleRoundTrip.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleRoundTrip.java new file mode 100644 index 00000000..bf1558a2 --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleRoundTrip.java @@ -0,0 +1,45 @@ +package zone.moddev.mc.advantageworks; + +/** One mutation window, bounded by native phases rather than machine outcomes. */ +final class LifecycleRoundTrip { + static final String PROTOCOL = "native117-lifecycle-first-callback-v1"; + static final long MAX_NANOS = 20_000_000_000L; + static final int MAX_STARTS = 160; + final long requestedNanos; + int attempts; + int mutations; + long startWorldTime; + boolean started, finished; + String failure; + + LifecycleRoundTrip(long now) { requestedNanos = now; } + + boolean start(long worldTime, long now, boolean ready) { + if (started || finished) throw new IllegalStateException("Round trip already started or finished"); + attempts++; + if (now - requestedNanos >= MAX_NANOS || attempts > MAX_STARTS) { + fail("Simultaneous native START prerequisites timed out"); + return false; + } + if (!ready) return false; + started = true; + startWorldTime = worldTime; + return true; + } + + void mutated() { + if (!started || finished || mutations != 0) throw new IllegalStateException("Duplicate or unstarted mutation"); + mutations++; + } + + void end(long worldTime, boolean firstCallbacksComplete) { + if (!started || finished) throw new IllegalStateException("Unmatched END"); + if (worldTime != startWorldTime + 1 || mutations != 1 || !firstCallbacksComplete) { + fail("Matching first natural tile callbacks were not completed"); + } else { + finished = true; + } + } + + void fail(String reason) { failure = reason; finished = true; } +} 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 8a2efe6d..77392c18 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 @@ -12,31 +12,33 @@ import java.util.List; import java.util.Map; import java.util.Set; -import net.minecraft.block.Block; -import net.minecraft.block.BlockState; -import net.minecraft.fluid.Fluid; -import net.minecraft.inventory.IInventory; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.state.Property; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.Container; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; import net.minecraftforge.registries.ForgeRegistries; final class MachineContext { - public final ServerWorld world; + public final ServerLevel world; final BlockPos origin; final Set positions = new LinkedHashSet<>(); final List> actions = new ArrayList<>(), checks = new ArrayList<>(); final Map measurements = new LinkedHashMap<>(); boolean settingUp = true; + private boolean foundationLaid; + private boolean foundationRequested; long activationTick; int verified; - MachineContext(ServerWorld world, int index) { + MachineContext(ServerLevel world, int index) { this.world = world; origin = new BlockPos(50000 + index * 64, 80, 50000); } @@ -58,16 +60,34 @@ private void mutation(String operation, BlockPos pos, Object value) { actions.add(row); } - void floor() { + void requestFoundation() { if (!settingUp) throw new IllegalStateException("Foundation construction after setup"); + if (foundationRequested) return; for (int x = origin.getX() >> 4; x <= (origin.getX() + 31) >> 4; x++) { for (int z = origin.getZ() >> 4; z <= (origin.getZ() + 31) >> 4; z++) { world.setChunkForced(x, z, true); - world.getChunk(x, z); } } + foundationRequested = true; + } + + void floor() { + if (!settingUp || !foundationRequested) throw new IllegalStateException("Foundation tickets were not prepared"); + if (foundationLaid) return; + requireTickingFoundation(); for (int x = 0; x < 32; x++) for (int z = 0; z < 32; z++) { - world.setBlock(at(x, -1, z), net.minecraft.block.Blocks.BEDROCK.defaultBlockState(), 2); + world.setBlock(at(x, -1, z), net.minecraft.world.level.block.Blocks.BEDROCK.defaultBlockState(), 2); + } + foundationLaid = true; + } + + 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), + "Native block and entity sections are ticking before fixture construction: " + x + "," + z); + } } } @@ -93,25 +113,25 @@ void facing(int x, int y, int z, String facing) { throw new IllegalArgumentException("No facing on " + state); } - TileEntity tile(int x, int y, int z) { + BlockEntity tile(int x, int y, int z) { BlockPos pos = at(x, y, z); positions.add(pos.immutable()); - TileEntity tile = world.getBlockEntity(pos); + BlockEntity tile = world.getBlockEntity(pos); if (tile == null) throw new IllegalArgumentException("Missing fixture tile at " + pos); return tile; } - IInventory inventory(int x, int y, int z) { - TileEntity value = tile(x, y, z); - if (!(value instanceof IInventory)) throw new IllegalArgumentException("Not an inventory: " + value); - return (IInventory) value; + Container inventory(int x, int y, int z) { + BlockEntity value = tile(x, y, z); + if (!(value instanceof Container)) throw new IllegalArgumentException("Not an inventory: " + value); + return (Container) value; } void put(int x, int y, int z, int slot, String item, int count) { ResourceLocation id = new ResourceLocation(item); if (!ForgeRegistries.ITEMS.containsKey(id) || count < 0 || count > 64) throw new IllegalArgumentException("Invalid stock " + item); mutation("inventory", at(x, y, z), slot + ":" + item + ":" + count); - IInventory inventory = inventory(x, y, z); + Container inventory = inventory(x, y, z); if (slot < 0 || slot >= inventory.getContainerSize()) throw new IllegalArgumentException("Invalid fixture slot"); inventory.setItem(slot, count == 0 ? ItemStack.EMPTY : new ItemStack(ForgeRegistries.ITEMS.getValue(id), count)); inventory.setChanged(); @@ -120,7 +140,7 @@ void put(int x, int y, int z, int slot, String item, int count) { int count(int x, int y, int z, String item) { Item expected = ForgeRegistries.ITEMS.getValue(new ResourceLocation(item)); if (!ForgeRegistries.ITEMS.containsKey(new ResourceLocation(item))) throw new IllegalArgumentException("Missing item " + item); - IInventory inventory = inventory(x, y, z); + Container inventory = inventory(x, y, z); int total = 0; for (int slot = 0; slot < inventory.getContainerSize(); slot++) { ItemStack stack = inventory.getItem(slot); @@ -147,14 +167,14 @@ float energy(int x, int y, int z, String type) { } void steamSupply(int x, int y, int z, float reserve) { - block(x, y, z, "steamadvantage:steam_tank"); + block(x, y, z, "basesteam:steam_tank"); power(x, y, z, "steam", reserve); } void electricSupply(int x, int y, int z, float reserve) { if (!Float.isFinite(reserve) || reserve < 0 || reserve > 8000) throw new IllegalArgumentException("Invalid battery reserve"); - block(x, y, z, "electricadvantage:electric_battery_array"); - put(x, y, z, 0, "electricadvantage:lithium_battery", 1); + block(x, y, z, "baseelectric:electric_battery_array"); + put(x, y, z, 0, "baseelectric:lithium_battery", 1); inventory(x, y, z).getItem(0).getOrCreateTag().putFloat("energy", reserve); inventory(x, y, z).setChanged(); } @@ -175,19 +195,19 @@ int fluidAmount(int x, int y, int z) { return amount; } - CompoundNBT nbt(int x, int y, int z) { return tile(x, y, z).save(new CompoundNBT()); } + CompoundTag nbt(int x, int y, int z) { return tile(x, y, z).save(new CompoundTag()); } - void seed(int x, int y, int z, CompoundNBT patch) { + void seed(int x, int y, int z, CompoundTag patch) { mutation("nbt", at(x, y, z), patch.toString()); - TileEntity tile = tile(x, y, z); - CompoundNBT nbt = tile.save(new CompoundNBT()); + BlockEntity tile = tile(x, y, z); + CompoundTag nbt = tile.save(new CompoundTag()); nbt.merge(patch.copy()); - tile.load(world.getBlockState(at(x, y, z)), nbt); + tile.load(nbt); tile.setChanged(); } Object invoke(int x, int y, int z, String method, Class[] types, Object... arguments) { - TileEntity tile = tile(x, y, z); + BlockEntity tile = tile(x, y, z); try { return tile.getClass().getMethod(method, types).invoke(tile, arguments); } catch (InvocationTargetException error) { throw new IllegalStateException("Fixture method failed: " + method, error.getCause()); } catch (ReflectiveOperationException error) { throw new IllegalStateException("Missing fixture method: " + method, error); } @@ -213,8 +233,8 @@ JsonArray snapshot() { BlockState state = world.getBlockState(pos); row.addProperty("block", state.getBlock().getRegistryName().toString()); row.addProperty("state", state.toString()); - TileEntity tile = world.getBlockEntity(pos); - if (tile != null) row.addProperty("nbt", tile.save(new CompoundNBT()).toString()); + BlockEntity tile = world.getBlockEntity(pos); + if (tile != null) row.addProperty("nbt", tile.save(new CompoundTag()).toString()); rows.add(row); } return rows; @@ -222,10 +242,10 @@ JsonArray snapshot() { JsonArray entities() { JsonArray result = new JsonArray(); - net.minecraft.util.math.AxisAlignedBB bounds = new net.minecraft.util.math.AxisAlignedBB(at(0, -1, 0), at(31, 31, 31)).inflate(.5); - for (net.minecraft.entity.Entity entity : world.getEntitiesOfClass(net.minecraft.entity.Entity.class, bounds)) { - if (entity instanceof net.minecraft.entity.player.PlayerEntity) continue; - CompoundNBT nbt = new CompoundNBT(); + net.minecraft.world.phys.AABB bounds = new net.minecraft.world.phys.AABB(at(0, -1, 0), at(31, 31, 31)).inflate(.5); + for (net.minecraft.world.entity.Entity entity : world.getEntitiesOfClass(net.minecraft.world.entity.Entity.class, bounds)) { + if (entity instanceof net.minecraft.world.entity.player.Player) continue; + CompoundTag nbt = new CompoundTag(); if (!entity.save(nbt)) throw new IllegalStateException("Test actor could not be saved: " + entity); JsonObject row = new JsonObject(); row.addProperty("uuid", entity.getUUID().toString()); row.addProperty("nbt", nbt.toString()); 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 3c1e1623..036be1d3 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 @@ -15,12 +15,12 @@ import java.util.List; import java.util.Map; import java.util.Set; -import net.minecraft.command.Commands; -import net.minecraft.entity.Entity; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.GameRules; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.commands.Commands; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.GameRules; +import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.FakePlayer; import net.minecraftforge.common.util.FakePlayerFactory; @@ -33,11 +33,12 @@ final class MachineOperationalProbe { private static final File FIXTURE = new File("machine-operation-fixture.json"); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private final List runs = new ArrayList<>(); - private final List held = new ArrayList<>(); + private final List held = new ArrayList<>(); private final NativeEntityHold heldEntities = new NativeEntityHold(); - private ServerWorld world; + private ServerLevel world; private boolean paused = true; private boolean preparing; + private int foundationIndex; private int prepareIndex; private String label; private long campaignStart; @@ -51,6 +52,25 @@ static void register() { MachineOperationalProbe probe = new MachineOperationalProbe(); MinecraftForge.EVENT_BUS.addListener(probe::commands); MinecraftForge.EVENT_BUS.addListener(probe::tick); + MinecraftForge.EVENT_BUS.addListener(probe::chunkLoaded); + MinecraftForge.EVENT_BUS.addListener(probe::entityJoining); + } + + private void chunkLoaded(net.minecraftforge.event.world.ChunkEvent.Load event) { + if (paused && event.getWorld() instanceof ServerLevel) { + ServerLevel loaded = (ServerLevel) event.getWorld(); + if (loaded.dimension() == net.minecraft.world.level.Level.OVERWORLD) { + held.addAll(NativeBlockEntityTicks.drain(loaded)); + } + } + } + + private void entityJoining(net.minecraftforge.event.entity.EntityJoinWorldEvent event) { + if (paused && event.getWorld() instanceof ServerLevel + && event.getWorld().dimension() == net.minecraft.world.level.Level.OVERWORLD + && !(event.getEntity() instanceof net.minecraft.world.entity.player.Player)) { + heldEntities.hold(event.getEntity()); + } } private void commands(RegisterCommandsEvent event) { @@ -86,7 +106,7 @@ private static List catalog() { return scenarios; } - private void authorize(ServerWorld current, String requestedLabel) { + private void authorize(ServerLevel current, String requestedLabel) { if (!MARKER.isFile() || !current.getServer().getWorldData().getLevelName().equals("AdvantageWorksMigration") || !requestedLabel.matches("[A-Za-z0-9_-]+")) throw new IllegalStateException("Unmarked or unsafe operating world"); world = current; @@ -95,7 +115,7 @@ private void authorize(ServerWorld current, String requestedLabel) { if (!world.players().contains(sentinel)) world.players().add(sentinel); } - private void start(ServerWorld current, String group, String requestedLabel) { + private void start(ServerLevel current, String group, String requestedLabel) { authorize(current, requestedLabel); if (FIXTURE.exists() || !runs.isEmpty() || !paused) throw new IllegalStateException("Construction is single-use; never rebuild a restarted fixture"); List catalog = catalog(); @@ -125,6 +145,7 @@ private void setup(Run run) { run.started = true; try { run.context.floor(); + run.context.requireTickingFoundation(); run.scenario.setup.accept(run.context); run.before = run.context.snapshot(); run.startTick = world.getGameTime(); @@ -134,16 +155,14 @@ private void setup(Run run) { } private void tick(TickEvent.WorldTickEvent event) { - if (event.world.isClientSide || !(event.world instanceof ServerWorld) - || event.world.dimension() != net.minecraft.world.World.OVERWORLD) return; - ServerWorld current = (ServerWorld) event.world; + if (event.world.isClientSide || !(event.world instanceof ServerLevel) + || event.world.dimension() != net.minecraft.world.level.Level.OVERWORLD) return; + ServerLevel current = (ServerLevel) event.world; if (event.phase == TickEvent.Phase.START && paused) { - held.addAll(current.tickableBlockEntities); - current.tickableBlockEntities.clear(); - for (Entity entity : current.getAllEntities()) if (!(entity instanceof net.minecraft.entity.player.PlayerEntity)) heldEntities.hold(entity); + held.addAll(NativeBlockEntityTicks.drain(current)); + for (Entity entity : current.getAllEntities()) if (!(entity instanceof net.minecraft.world.entity.player.Player)) heldEntities.hold(entity); } else if (event.phase == TickEvent.Phase.END) { - for (TileEntity tile : held) if (!tile.isRemoved() && !current.tickableBlockEntities.contains(tile)) current.tickableBlockEntities.add(tile); - held.clear(); heldEntities.restore(); + NativeBlockEntityTicks.restore(current, held); heldEntities.restore(); if (current != world) return; if (inspectionCases != null) { if (world.getGameTime() >= nextPreparation) inspectNext(); @@ -151,6 +170,12 @@ private void tick(TickEvent.WorldTickEvent event) { } if (preparing) { if (world.getGameTime() < nextPreparation) return; + if (foundationIndex < runs.size()) { + // Obtain native ticking tickets before creating actors or machines. + runs.get(foundationIndex++).context.requestFoundation(); + nextPreparation = world.getGameTime() + 20; + return; + } while (prepareIndex < runs.size()) { Run run = runs.get(prepareIndex++); if (run.scenario.exclusive) continue; @@ -230,7 +255,7 @@ private void finish() { System.out.println("ADVMACHINES COMPLETE " + label); } - private void inspect(ServerWorld current, String requestedLabel) { + private void inspect(ServerLevel current, String requestedLabel) { authorize(current, requestedLabel); if (!paused || !runs.isEmpty()) throw new IllegalStateException("Restart inspection must run before any construction or operation"); try { @@ -255,8 +280,8 @@ private void inspectNext() { actual.addProperty("x", pos.getX()); actual.addProperty("y", pos.getY()); actual.addProperty("z", pos.getZ()); actual.addProperty("block", world.getBlockState(pos).getBlock().getRegistryName().toString()); actual.addProperty("state", world.getBlockState(pos).toString()); - TileEntity tile = world.getBlockEntity(pos); - if (tile != null) actual.addProperty("nbt", tile.save(new net.minecraft.nbt.CompoundNBT()).toString()); + BlockEntity tile = world.getBlockEntity(pos); + if (tile != null) actual.addProperty("nbt", tile.save(new net.minecraft.nbt.CompoundTag()).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 817c1b56..db462fd2 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 @@ -11,13 +11,13 @@ import java.nio.file.Files; import java.util.Locale; import java.util.Map; -import net.minecraft.state.Property; -import net.minecraft.block.BlockState; -import net.minecraft.command.Commands; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.commands.Commands; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.TickEvent; import net.minecraftforge.event.world.WorldEvent; @@ -33,17 +33,21 @@ static void register() { MigrationProbe probe = new MigrationProbe(); MinecraftForge.EVENT_BUS.addListener(probe::load); MinecraftForge.EVENT_BUS.addListener(probe::tick); + MinecraftForge.EVENT_BUS.addListener(probe::chunkLoaded); MinecraftForge.EVENT_BUS.addListener(probe::starting); } private void load(WorldEvent.Load event) { - if (event.getWorld() instanceof World) freeze((World) event.getWorld()); + if (event.getWorld() instanceof Level) freeze((Level) event.getWorld()); } private void tick(TickEvent.WorldTickEvent event) { if (event.phase == TickEvent.Phase.START) freeze(event.world); } - private void freeze(World world) { - if (!world.isClientSide && !new File("migration-operate.allow").isFile()) world.tickableBlockEntities.clear(); + private void chunkLoaded(net.minecraftforge.event.world.ChunkEvent.Load event) { + if (event.getWorld() instanceof Level) freeze((Level) event.getWorld()); + } + private void freeze(Level world) { + if (!world.isClientSide && !new File("migration-operate.allow").isFile()) NativeBlockEntityTicks.drain(world); } private void starting(RegisterCommandsEvent event) { @@ -59,7 +63,7 @@ private void starting(RegisterCommandsEvent event) { })))); } - private void snapshot(World world, String label) throws Exception { + private void snapshot(Level world, String label) throws Exception { if (!MARKER.isFile() || !world.getServer().getWorldData().getLevelName().equals("AdvantageWorksMigration")) { throw new IllegalStateException("Refusing an unmarked or unexpected world"); } @@ -77,13 +81,13 @@ private void snapshot(World world, String label) throws Exception { properties.addProperty(property.getName(), state.getValue(property).toString().toLowerCase(Locale.ROOT)); } entry.add("properties", properties); - TileEntity tile = world.getBlockEntity(pos); + BlockEntity tile = world.getBlockEntity(pos); if (tile != null) { tile.setChanged(); - CompoundNBT nbt = tile.save(new CompoundNBT()); + CompoundTag nbt = tile.save(new CompoundTag()); entry.addProperty("nbt", nbt.toString()); java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream(); - net.minecraft.nbt.CompressedStreamTools.writeCompressed(nbt, bytes); + net.minecraft.nbt.NbtIo.writeCompressed(nbt, bytes); entry.addProperty("nbtBase64", java.util.Base64.getEncoder().encodeToString(bytes.toByteArray())); } result.add(entry); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native116Worlds.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native116Worlds.java deleted file mode 100644 index 58b9d3e7..00000000 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native116Worlds.java +++ /dev/null @@ -1,32 +0,0 @@ -package zone.moddev.mc.advantageworks; - -import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import net.minecraft.entity.Entity; -import net.minecraft.util.RegistryKey; -import net.minecraft.world.World; -import net.minecraft.world.server.ServerWorld; - -final class Native116Worlds { - private Native116Worlds() {} - - static RegistryKey key(int legacyId) { - switch (legacyId) { - case -1: return World.NETHER; - case 0: return World.OVERWORLD; - case 1: return World.END; - default: throw new IllegalArgumentException("Unsupported fixture dimension: " + legacyId); - } - } - - static int legacyId(RegistryKey key) { - if (key.equals(World.NETHER)) return -1; - if (key.equals(World.OVERWORLD)) return 0; - if (key.equals(World.END)) return 1; - throw new IllegalArgumentException("Unsupported native fixture dimension: " + key.location()); - } - - static Stream entities(ServerWorld world) { - return StreamSupport.stream(world.getAllEntities().spliterator(), false); - } -} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native116EntityContext.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117EntityContext.java similarity index 82% rename from test-rig/src/main/java/zone/moddev/mc/advantageworks/Native116EntityContext.java rename to test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117EntityContext.java index 75ff1994..1dc6ef7d 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native116EntityContext.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117EntityContext.java @@ -4,13 +4,13 @@ import java.util.Collections; import java.util.IdentityHashMap; import java.util.Set; -import net.minecraft.entity.Entity; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.entity.Entity; +import net.minecraft.server.level.ServerLevel; -final class Native116EntityContext { - private Native116EntityContext() {} +final class Native117EntityContext { + private Native117EntityContext() {} - static JsonObject observe(Entity entity, ServerWorld world) { + static JsonObject observe(Entity entity, ServerLevel world) { if (entity == null || world == null) throw new IllegalStateException("Missing native entity/world context"); JsonObject worlds = new JsonObject(); Set seen = Collections.newSetFromMap(new IdentityHashMap()); @@ -22,7 +22,7 @@ static JsonObject observe(Entity entity, ServerWorld world) { return result; } - private static void collect(Entity entity, ServerWorld world, JsonObject worlds, Set seen) { + private static void collect(Entity entity, ServerLevel world, JsonObject worlds, Set seen) { if (entity.level != world) throw new IllegalStateException("Dimension witness belongs to another world instance"); if (!seen.add(entity)) throw new IllegalStateException("Repeated entity in dimension observation"); String uuid = entity.getUUID().toString(); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native116PlayerIo.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117PlayerIo.java similarity index 72% rename from test-rig/src/main/java/zone/moddev/mc/advantageworks/Native116PlayerIo.java rename to test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117PlayerIo.java index 2b8f09f8..c6af69d7 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native116PlayerIo.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117PlayerIo.java @@ -3,22 +3,22 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Field; import java.lang.reflect.Method; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.server.management.PlayerList; -import net.minecraft.world.storage.PlayerData; -import net.minecraftforge.fml.common.ObfuscationReflectionHelper; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.level.storage.PlayerDataStorage; +import net.minecraftforge.fml.util.ObfuscationReflectionHelper; -final class Native116PlayerIo { +final class Native117PlayerIo { private static final Method SAVE = findSave(); private static final Field PLAYER_DATA = findPlayerData(); - private Native116PlayerIo() {} + private Native117PlayerIo() {} - static void save(PlayerList players, ServerPlayerEntity player) { + static void save(PlayerList players, ServerPlayer player) { try { // Forge skips PlayerList.save for these offline probe players. if (player.connection == null) { - PlayerData data = (PlayerData) PLAYER_DATA.get(players); + PlayerDataStorage data = (PlayerDataStorage) PLAYER_DATA.get(players); if (data == null) throw new IllegalStateException("Native PlayerData is missing"); data.save(player); } else { @@ -31,11 +31,11 @@ static void save(PlayerList players, ServerPlayerEntity player) { private static Method findSave() { try { - Method method = PlayerList.class.getDeclaredMethod("save", ServerPlayerEntity.class); + Method method = PlayerList.class.getDeclaredMethod("save", ServerPlayer.class); method.setAccessible(true); return method; } catch (NoSuchMethodException officialNameAbsent) { - return ObfuscationReflectionHelper.findMethod(PlayerList.class, "func_72391_b", ServerPlayerEntity.class); + return ObfuscationReflectionHelper.findMethod(PlayerList.class, "m_6765_", ServerPlayer.class); } } @@ -45,7 +45,7 @@ private static Field findPlayerData() { field.setAccessible(true); return field; } catch (NoSuchFieldException officialNameAbsent) { - return ObfuscationReflectionHelper.findField(PlayerList.class, "field_72412_k"); + return ObfuscationReflectionHelper.findField(PlayerList.class, "f_11204_"); } } } 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 new file mode 100644 index 00000000..59de9bb6 --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117Readiness.java @@ -0,0 +1,206 @@ +package zone.moddev.mc.advantageworks; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import net.minecraft.commands.Commands; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.TickEvent; + +/** Observes native chunk readiness; never loads chunks, constructs actors or changes world state. */ +final class Native117Readiness { + static final String PROTOCOL = "native117-chunk-readiness-v1"; + static final int MAX_CHUNKS = 256; + static final int MAX_CHECKS = 1200; + static final long MAX_WAIT_NANOS = 120_000_000_000L; + private static final File MARKER = new File("advantage-works.allow"); + private ServerLevel world; + private String label; + private File output; + private Observation observation; + + static void register() { + if (!MARKER.isFile()) return; + Native117Readiness probe = new Native117Readiness(); + MinecraftForge.EVENT_BUS.addListener(probe::commands); + MinecraftForge.EVENT_BUS.addListener(probe::tick); + } + + private void commands(RegisterCommandsEvent event) { + event.getDispatcher().register(Commands.literal("advreadiness").requires(source -> source.hasPermission(4)) + .then(Commands.literal("chunks") + .then(Commands.argument("minX", IntegerArgumentType.integer(-1875000, 1874999)) + .then(Commands.argument("minZ", IntegerArgumentType.integer(-1875000, 1874999)) + .then(Commands.argument("maxX", IntegerArgumentType.integer(-1875000, 1874999)) + .then(Commands.argument("maxZ", IntegerArgumentType.integer(-1875000, 1874999)) + .then(Commands.argument("label", StringArgumentType.word()).executes(context -> { + WorksController.getInstance().requireAuthorizedWorld(context.getSource().getServer()); + request(context.getSource().getLevel(), new Bounds( + IntegerArgumentType.getInteger(context, "minX"), + IntegerArgumentType.getInteger(context, "minZ"), + IntegerArgumentType.getInteger(context, "maxX"), + IntegerArgumentType.getInteger(context, "maxZ")), + StringArgumentType.getString(context, "label")); + return 1; + })))))))); + } + + private void request(ServerLevel level, Bounds bounds, String requestedLabel) { + if (!MARKER.isFile() || observation != null) throw new IllegalStateException("Unmarked or busy readiness probe"); + if (!requestedLabel.matches("[A-Za-z0-9_-]{1,64}")) throw new IllegalArgumentException("Unsafe readiness label"); + File destination = new File("advantage-works-results", "native-readiness-" + requestedLabel + ".json"); + if (destination.exists()) throw new IllegalStateException("Refusing to overwrite readiness evidence"); + world = level; + label = requestedLabel; + output = destination; + observation = new Observation(bounds, level.getGameTime(), System.nanoTime()); + System.out.println("ADVREADINESS QUEUED " + label); + } + + private void tick(TickEvent.WorldTickEvent event) { + if (observation == null || event.phase != TickEvent.Phase.END || event.world != world) return; + List states = new ArrayList<>(); + 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)))); + } + } + 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("dimension", world.dimension().location().toString()); + try { + Files.createDirectories(output.toPath().getParent()); + Files.writeString(output.toPath(), new GsonBuilder().setPrettyPrinting().create().toJson(report), + StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); + } catch (java.io.IOException failure) { throw new IllegalStateException("Cannot retain readiness evidence", failure); } + String completedLabel = label; + boolean passed = observation.passed; + observation = null; + world = null; + output = null; + label = null; + System.out.println("ADVREADINESS RESULT " + completedLabel + " status=" + (passed ? "PASS" : "FAIL")); + System.out.println("ADVREADINESS " + (passed ? "COMPLETE " : "FAILED ") + completedLabel); + } + + static final class Bounds { + final int minX, minZ, maxX, maxZ; + + Bounds(int minX, int minZ, int maxX, int maxZ) { + long width = (long) maxX - minX + 1, depth = (long) maxZ - minZ + 1; + if (minX < -1875000 || minZ < -1875000 || maxX > 1874999 || maxZ > 1874999 + || width <= 0 || depth <= 0 || width > 64 || depth > 64 || width * depth > MAX_CHUNKS) { + throw new IllegalArgumentException("Readiness requires an ordered rectangle of at most 256 chunks and 64 per axis"); + } + this.minX = minX; this.minZ = minZ; this.maxX = maxX; this.maxZ = maxZ; + } + + int size() { return (maxX - minX + 1) * (maxZ - minZ + 1); } + } + + static final class State { + final int x, z; + final boolean blockAndEntitiesLoaded, entityTicking; + + State(int x, int z, boolean blockAndEntitiesLoaded, boolean entityTicking) { + this.x = x; this.z = z; + this.blockAndEntitiesLoaded = blockAndEntitiesLoaded; + this.entityTicking = entityTicking; + } + + boolean ready() { return blockAndEntitiesLoaded && entityTicking; } + boolean same(State other) { + return x == other.x && z == other.z && blockAndEntitiesLoaded == other.blockAndEntitiesLoaded + && entityTicking == other.entityTicking; + } + JsonObject json() { + JsonObject row = new JsonObject(); + row.addProperty("x", x); row.addProperty("z", z); + row.addProperty("blockAndEntitiesLoaded", blockAndEntitiesLoaded); + row.addProperty("entityTicking", entityTicking); + return row; + } + } + + static final class Observation { + final Bounds bounds; + final long requestedWorldTime, startedNanos; + final JsonArray transitions = new JsonArray(); + List latest = new ArrayList<>(); + int checks; + long completedWorldTime, elapsedNanos; + boolean finished, passed; + + Observation(Bounds bounds, long requestedWorldTime, long startedNanos) { + this.bounds = bounds; this.requestedWorldTime = requestedWorldTime; this.startedNanos = startedNanos; + } + + boolean observe(long worldTime, long nowNanos, List states) { + if (finished) throw new IllegalStateException("Readiness observation is terminal"); + if (states.size() != bounds.size()) throw new IllegalArgumentException("Incomplete readiness sample"); + int index = 0; + for (int x = bounds.minX; x <= bounds.maxX; x++) for (int z = bounds.minZ; z <= bounds.maxZ; z++) { + State state = states.get(index++); + if (state.x != x || state.z != z) throw new IllegalArgumentException("Readiness sample has duplicate or misplaced chunks"); + } + checks++; + JsonArray changes = new JsonArray(); + for (int i = 0; i < states.size(); i++) { + if (latest.isEmpty() || !states.get(i).same(latest.get(i))) changes.add(states.get(i).json()); + } + if (changes.size() > 0) { + JsonObject transition = new JsonObject(); + transition.addProperty("check", checks); + transition.addProperty("worldTime", worldTime); + transition.add("changes", changes); + transitions.add(transition); + } + latest = new ArrayList<>(states); + completedWorldTime = worldTime; + elapsedNanos = nowNanos - startedNanos; + if (elapsedNanos < 0) throw new IllegalArgumentException("Readiness clock moved backwards"); + passed = checks <= MAX_CHECKS && elapsedNanos <= MAX_WAIT_NANOS && latest.stream().allMatch(State::ready); + finished = passed || checks >= MAX_CHECKS || elapsedNanos >= MAX_WAIT_NANOS; + return finished; + } + + JsonObject report() { + if (!finished) throw new IllegalStateException("Unfinished readiness cannot produce a receipt"); + JsonObject report = new JsonObject(); + report.addProperty("protocol", PROTOCOL); + report.addProperty("status", passed ? "PASS" : "FAIL"); + if (!passed) report.addProperty("failure", "Native chunk readiness timed out"); + report.addProperty("requestedWorldTime", requestedWorldTime); + report.addProperty("completedWorldTime", completedWorldTime); + report.addProperty("checks", checks); + report.addProperty("elapsedMillis", elapsedNanos / 1_000_000L); + report.addProperty("maximumChecks", MAX_CHECKS); + report.addProperty("maximumWaitMillis", MAX_WAIT_NANOS / 1_000_000L); + JsonObject rectangle = new JsonObject(); + rectangle.addProperty("minX", bounds.minX); rectangle.addProperty("minZ", bounds.minZ); + rectangle.addProperty("maxX", bounds.maxX); rectangle.addProperty("maxZ", bounds.maxZ); + report.add("bounds", rectangle); + JsonArray chunks = new JsonArray(); + latest.forEach(state -> chunks.add(state.json())); + report.add("chunks", chunks); + report.add("transitions", transitions); + return report; + } + } +} 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 new file mode 100644 index 00000000..03cd3d86 --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshot.java @@ -0,0 +1,175 @@ +package zone.moddev.mc.advantageworks; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Base64; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.WeakHashMap; +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +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 org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Loaded non-player NBT at the normal stop boundary; never freezes or ticks anything. */ +final class Native117StoppedEntitiesSnapshot { + static final String FILE = "native117-stopped-entities.json"; + static final String WORLD = "AdvantageWorksMigration"; + private static final Gson JSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Logger LOG = LogManager.getLogger(); + private static final Map BOOTS = Collections.synchronizedMap(new WeakHashMap<>()); + + private Native117StoppedEntitiesSnapshot() {} + + static void register() { + Native117StoppedEntitiesSnapshotV2.register(); + } + + static boolean marked(Path runtime, String name) { + return WORLD.equals(name) && (Files.isRegularFile(runtime.resolve("advantage-migration.allow"), LinkOption.NOFOLLOW_LINKS) + || Files.isRegularFile(runtime.resolve("advantage-machine-operation.allow"), LinkOption.NOFOLLOW_LINKS)); + } + + private static JsonObject header(MinecraftServer server, String boot, String phase) { + JsonObject result = new JsonObject(); + result.addProperty("format", 1); + result.addProperty("minecraftVersion", SharedConstants.getCurrentVersion().getName()); + result.addProperty("dataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); + result.addProperty("worldName", server.getWorldData().getLevelName()); + result.addProperty("worldDirectory", server.getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize().toString()); + result.addProperty("bootId", boot); + result.addProperty("phase", phase); + return result; + } + + private static void starting(FMLServerStartingEvent 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"); + pending.addProperty("status", "PENDING_STOP"); + try { + write(runtime, pending); + } catch (IOException error) { + LOG.error("Could not invalidate the previous Advantage Works stop snapshot", error); + } + } + + private static void stopping(FMLServerStoppingEvent 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"); + try { + if (boot == null) throw new IllegalStateException("No marked startup was observed for this server"); + JsonObject dimensions = new JsonObject(); + Set seen = new HashSet<>(); + int count = 0; + for (int id : new int[] {0, -1, 1}) { + ResourceKey key = Native117Worlds.key(id); + ServerLevel world = server.getLevel(key); + if (world == null) throw new IllegalStateException("Missing vanilla dimension: " + key.location()); + JsonArray entities = entities(world, world.getAllEntities(), seen); + JsonObject dimension = new JsonObject(); + dimension.addProperty("dimension", id); + dimension.addProperty("worldDimensionKey", key.location().toString()); + dimension.addProperty("gameTime", world.getGameTime()); + dimension.addProperty("entityCount", entities.size()); + dimension.add("entities", entities); + dimensions.add(key.location().toString(), dimension); + count += entities.size(); + } + result.add("dimensions", dimensions); + result.addProperty("entityCount", count); + result.addProperty("enumeration", "ServerLevel.getAllEntities"); + result.addProperty("status", "CAPTURED"); + } catch (Exception error) { + result.addProperty("status", "FAILED"); + result.addProperty("error", error.toString()); + LOG.error("Advantage Works stopping entity capture failed", error); + } + try { + write(runtime, result); + } catch (IOException error) { + // A failed observer must not suppress Minecraft's final save. Strict log gates reject this run. + LOG.error("Could not retain the Advantage Works stopping entity snapshot", error); + } + } + + static JsonArray entities(ServerLevel world, Iterable available, Set seen) throws IOException { + Native117Worlds.legacyId(world.dimension()); + JsonArray result = new JsonArray(); + for (Entity entity : available) { + if (entity instanceof Player) continue; + if (entity.level != world) throw new IllegalStateException("Enumerated entity belongs to another world"); + if (!seen.add(entity.getUUID())) throw new IllegalStateException("Duplicate enumerated entity UUID: " + entity.getUUID()); + CompoundTag nbt = serialized(entity); + if (!nbt.hasUUID("UUID") || !nbt.getUUID("UUID").equals(entity.getUUID())) { + throw new IllegalStateException("Native serialization changed entity UUID"); + } + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + NbtIo.writeCompressed(nbt, bytes); + JsonObject entry = new JsonObject(); + entry.addProperty("uuid", entity.getUUID().toString()); + entry.addProperty("id", nbt.getString("id")); + entry.addProperty("worldDimensionKey", world.dimension().location().toString()); + entry.addProperty("vehicleUuid", entity.getVehicle() == null ? null : entity.getVehicle().getUUID().toString()); + entry.addProperty("nbtBase64", Base64.getEncoder().encodeToString(bytes.toByteArray())); + result.add(entry); + } + return result; + } + + static CompoundTag serialized(Entity entity) { + if (entity instanceof Player) throw new IllegalArgumentException("Players are not entity-region witnesses"); + if (entity.isPassenger()) { + // The native root-only writer rejects riding entities. This is its unprojected + // saveWithoutId payload plus the same native registry ID, also used by DimensionEntityProbe. + CompoundTag nbt = entity.saveWithoutId(new CompoundTag()); + nbt.putString("id", EntityType.getKey(entity.getType()).toString()); + return nbt; + } + CompoundTag nbt = new CompoundTag(); + if (!entity.saveAsPassenger(nbt)) throw new IllegalStateException("Native entity writer refused " + entity.getUUID()); + return nbt; + } + + static void write(Path runtime, JsonObject document) throws IOException { + Path temporary = Files.createTempFile(runtime, "native117-stopped-entities-", ".tmp"); + try { + Files.write(temporary, (JSON.toJson(document) + "\n").getBytes(StandardCharsets.UTF_8)); + Files.move(temporary, runtime.resolve(FILE), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } finally { + Files.deleteIfExists(temporary); + } + } +} 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 new file mode 100644 index 00000000..a6941ea0 --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2.java @@ -0,0 +1,189 @@ +package zone.moddev.mc.advantageworks; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.util.Base64; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.WeakHashMap; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.decoration.LeashFenceKnotEntity; +import net.minecraft.world.entity.player.Player; +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 org.apache.logging.log4j.LogManager; + +/** Versioned stop-boundary observation. Native no-save helpers are never saved-state witnesses. */ +final class Native117StoppedEntitiesSnapshotV2 { + static final String CONTRACT = "native117-helper-contract.json"; + static final String POLICY = "native117-leash-knot-nosave-v1"; + private static final Map BOOTS = Collections.synchronizedMap(new WeakHashMap<>()); + + private Native117StoppedEntitiesSnapshotV2() {} + + static void register() { + MinecraftForge.EVENT_BUS.addListener(EventPriority.LOWEST, Native117StoppedEntitiesSnapshotV2::starting); + MinecraftForge.EVENT_BUS.addListener(EventPriority.LOWEST, Native117StoppedEntitiesSnapshotV2::stopping); + } + + static byte[] contract() throws IOException { + try (InputStream input = Native117StoppedEntitiesSnapshotV2.class.getResourceAsStream( + "/assets/advantageworkstest/" + CONTRACT)) { + if (input == null) throw new IOException("Missing native helper control evidence"); + return input.readAllBytes(); + } + } + + static String sha(byte[] bytes) throws Exception { + StringBuilder out = new StringBuilder(); + for (byte value : MessageDigest.getInstance("SHA-256").digest(bytes)) out.append(String.format("%02X", value & 255)); + return out.toString(); + } + + private static JsonObject header(MinecraftServer server, String boot, String phase) throws Exception { + JsonObject result = new JsonObject(); + result.addProperty("format", 2); + result.addProperty("minecraftVersion", SharedConstants.getCurrentVersion().getName()); + result.addProperty("dataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); + result.addProperty("worldName", server.getWorldData().getLevelName()); + result.addProperty("worldDirectory", server.getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize().toString()); + result.addProperty("bootId", boot); + result.addProperty("phase", phase); + result.addProperty("helperPolicy", POLICY); + result.addProperty("helperContractSha256", sha(contract())); + return result; + } + + private static void starting(FMLServerStartingEvent event) { + MinecraftServer server = event.getServer(); + Path runtime = Path.of("").toAbsolutePath().normalize(); + if (!Native117StoppedEntitiesSnapshot.marked(runtime, server.getWorldData().getLevelName())) return; + String boot = UUID.randomUUID().toString(); + BOOTS.put(server, boot); + try { + Files.write(runtime.resolve(CONTRACT), contract()); + JsonObject pending = header(server, boot, "FMLServerStartingEvent"); + pending.addProperty("status", "PENDING_STOP"); + Native117StoppedEntitiesSnapshot.write(runtime, pending); + } catch (Exception error) { + LogManager.getLogger().error("Could not initialize versioned stopping observation", error); + } + } + + private static void stopping(FMLServerStoppingEvent 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"); + if (boot == null) throw new IllegalStateException("No marked startup for this server"); + JsonObject dimensions = new JsonObject(); + Set seen = new HashSet<>(); + int persistent = 0, helpers = 0; + for (int id : new int[] {0, -1, 1}) { + ServerLevel world = server.getLevel(Native117Worlds.key(id)); + if (world == null) throw new IllegalStateException("Missing vanilla dimension: " + id); + JsonObject dimension = partition(world, world.getAllEntities(), seen); + dimension.addProperty("gameTime", world.getGameTime()); + dimensions.add(world.dimension().location().toString(), dimension); + persistent += dimension.get("entityCount").getAsInt(); + helpers += dimension.get("nonpersistentHelperCount").getAsInt(); + } + result.add("dimensions", dimensions); + result.addProperty("entityCount", persistent); + result.addProperty("nonpersistentHelperCount", helpers); + result.addProperty("observedEntityCount", persistent + helpers); + result.addProperty("enumeration", "ServerLevel.getAllEntities"); + result.addProperty("status", "CAPTURED"); + } catch (Exception error) { + result.addProperty("status", "FAILED"); + result.addProperty("error", error.toString()); + LogManager.getLogger().error("Versioned stopping entity capture failed", error); + } + try { + Native117StoppedEntitiesSnapshot.write(runtime, result); + } catch (IOException error) { + LogManager.getLogger().error("Could not retain versioned stopping entity capture", error); + } + } + + static String diagnostic(Entity entity) { + return "uuid=" + entity.getUUID() + " class=" + entity.getClass().getName() + + " id=" + EntityType.getKey(entity.getType()) + " canSerialize=" + entity.getType().canSerialize() + + " removed=" + entity.isRemoved() + " passenger=" + entity.isPassenger(); + } + + static JsonObject partition(ServerLevel world, Iterable available, Set seen) throws Exception { + JsonArray persistent = new JsonArray(), helpers = new JsonArray(); + for (Entity entity : available) { + if (entity instanceof Player) continue; + if (entity.level != world) throw new IllegalStateException("Entity belongs to another world: " + diagnostic(entity)); + if (entity.getClass() == LeashFenceKnotEntity.class && entity.getType() == EntityType.LEASH_KNOT + && !entity.getType().canSerialize() && !entity.isRemoved() && !entity.isPassenger() + && entity.getPassengers().isEmpty()) { + if (!seen.add(entity.getUUID())) throw new IllegalStateException("Duplicate enumerated UUID: " + diagnostic(entity)); + CompoundTag nbt = entity.saveWithoutId(new CompoundTag()); + nbt.putString("id", EntityType.getKey(entity.getType()).toString()); + if (!nbt.hasUUID("UUID") || !entity.getUUID().equals(nbt.getUUID("UUID"))) + throw new IllegalStateException("Helper writer changed UUID: " + diagnostic(entity)); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + NbtIo.writeCompressed(nbt, bytes); + JsonObject helper = new JsonObject(); + helper.addProperty("uuid", entity.getUUID().toString()); + helper.addProperty("id", "minecraft:leash_knot"); + helper.addProperty("nativeClass", entity.getClass().getName()); + helper.addProperty("worldDimensionKey", world.dimension().location().toString()); + helper.addProperty("canSerialize", false); + helper.addProperty("removed", false); + helper.addProperty("passenger", false); + helper.addProperty("passengerCount", 0); + helper.addProperty("policy", POLICY); + BlockPos pos = ((LeashFenceKnotEntity) entity).getPos(); + JsonArray attachment = new JsonArray(); + attachment.add(pos.getX()); attachment.add(pos.getY()); attachment.add(pos.getZ()); + helper.add("attachmentPosition", attachment); + helper.addProperty("nbtBase64", Base64.getEncoder().encodeToString(bytes.toByteArray())); + helpers.add(helper); + } else { + if (!entity.getType().canSerialize() || entity.isRemoved()) + throw new IllegalStateException("Unconfirmed nonpersistent entity: " + diagnostic(entity)); + try { + persistent.add(Native117StoppedEntitiesSnapshot.entities(world, List.of(entity), seen).get(0)); + } catch (Exception error) { + throw new IllegalStateException("Persistent writer failed: " + diagnostic(entity), error); + } + } + } + JsonObject result = new JsonObject(); + result.addProperty("dimension", Native117Worlds.legacyId(world.dimension())); + result.addProperty("worldDimensionKey", world.dimension().location().toString()); + result.addProperty("entityCount", persistent.size()); + result.addProperty("nonpersistentHelperCount", helpers.size()); + result.addProperty("observedEntityCount", persistent.size() + helpers.size()); + result.add("entities", persistent); + result.add("nonpersistentHelpers", helpers); + return result; + } +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native116TestFlat.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117TestFlat.java similarity index 59% rename from test-rig/src/main/java/zone/moddev/mc/advantageworks/Native116TestFlat.java rename to test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117TestFlat.java index bad6a74b..381674fc 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native116TestFlat.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117TestFlat.java @@ -2,30 +2,30 @@ import java.util.Collections; import java.util.Optional; -import net.minecraft.block.Blocks; -import net.minecraft.util.ResourceLocation; -import net.minecraft.world.gen.FlatChunkGenerator; -import net.minecraft.world.gen.FlatGenerationSettings; -import net.minecraft.world.gen.FlatLayerInfo; -import net.minecraft.world.gen.settings.DimensionStructuresSettings; +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 Native116TestFlat { +final class Native117TestFlat { static final String ID = "advantageworkstest:test_flat"; - private Native116TestFlat() {} + 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"); - FlatGenerationSettings flat = new FlatGenerationSettings( - new DimensionStructuresSettings(Optional.empty(), Collections.emptyMap()), biomes); + 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 FlatChunkGenerator(flat); + return new FlatLevelSource(flat); }).setRegistryName(new ResourceLocation(ID)); } 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 new file mode 100644 index 00000000..59d9d414 --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117WorldgenAccess.java @@ -0,0 +1,82 @@ +package zone.moddev.mc.advantageworks; + +import java.util.Comparator; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraftforge.registries.ForgeRegistries; + +final class Native117WorldgenAccess implements WorldgenSamplingJob.WorldAccess { + private static final TicketType TICKET = TicketType.create( + "advantageworks_worldgen_sample", Comparator.comparingLong(ChunkPos::toLong)); + private final ServerLevel world; + private final BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + private final AsyncWorldgenChunks chunks; + + Native117WorldgenAccess(ServerLevel world) { + this.world = world; + ServerChunkCache source = world.getChunkSource(); + chunks = new AsyncWorldgenChunks<>(new AsyncWorldgenChunks.Backend() { + @Override public void retain(int x, int z) { + ChunkPos position = new ChunkPos(x, z); + source.addRegionTicket(TICKET, position, 0, position); + } + + @Override public CompletableFuture request(int x, int z) { + // Forge 37's 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())); + if (!(access instanceof LevelChunk) || access.getStatus() != ChunkStatus.FULL + || !access.isLightCorrect() || !access.getPos().equals(new ChunkPos(x, z))) { + throw new IllegalStateException("Native chunk was not FULL, lit, and at the requested position"); + } + return (LevelChunk) access; + }); + } + + @Override public void release(int x, int z) { + ChunkPos position = new ChunkPos(x, z); + source.removeRegionTicket(TICKET, position, 0, position); + } + }, Executors.newSingleThreadExecutor(task -> { + Thread thread = new Thread(task, "advantageworks-worldgen-dispatch"); + thread.setDaemon(true); + return thread; + })); + } + + @Override public boolean matchesBiome(String expected, int x, int y, int z) { + return WorksController.matchesExpectedBiome(world, expected, world.getBiome(cursor.set(x, y, z))); + } + + @Override public CompletableFuture requestChunk(int x, int z, boolean counting) { + return chunks.request(x, z).thenApply(chunk -> new WorldgenSamplingJob.SampleChunk() { + 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)); + return WorksController.matchesExpectedBiome(world, expected, center); + } + + @Override public String blockName(int blockX, int y, int blockZ) { + ResourceLocation id = ForgeRegistries.BLOCKS.getKey( + chunk.getBlockState(cursor.set(blockX, y, blockZ)).getBlock()); + return id == null ? null : id.toString(); + } + }); + } + + @Override public void releaseChunk(int x, int z) { chunks.release(x, z); } + @Override public void close() { chunks.close(); } + @Override public Map diagnostics() { return chunks.diagnostics(); } +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117Worlds.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117Worlds.java new file mode 100644 index 00000000..69a04091 --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117Worlds.java @@ -0,0 +1,32 @@ +package zone.moddev.mc.advantageworks; + +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import net.minecraft.world.entity.Entity; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.minecraft.server.level.ServerLevel; + +final class Native117Worlds { + private Native117Worlds() {} + + static ResourceKey key(int legacyId) { + switch (legacyId) { + case -1: return Level.NETHER; + case 0: return Level.OVERWORLD; + case 1: return Level.END; + default: throw new IllegalArgumentException("Unsupported fixture dimension: " + legacyId); + } + } + + static int legacyId(ResourceKey key) { + if (key.equals(Level.NETHER)) return -1; + if (key.equals(Level.OVERWORLD)) return 0; + if (key.equals(Level.END)) return 1; + throw new IllegalArgumentException("Unsupported native fixture dimension: " + key.location()); + } + + static Stream entities(ServerLevel world) { + return StreamSupport.stream(world.getAllEntities().spliterator(), false); + } +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeBlockEntityTicks.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeBlockEntityTicks.java new file mode 100644 index 00000000..d7887a08 --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeBlockEntityTicks.java @@ -0,0 +1,73 @@ +package zone.moddev.mc.advantageworks; + +import java.util.ArrayList; +import java.util.List; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.TickingBlockEntity; +import net.minecraftforge.fml.util.ObfuscationReflectionHelper; + +/** Holds real native ticker registrations without replacing tiles or writing state. */ +final class NativeBlockEntityTicks { + private NativeBlockEntityTicks() {} + + private static List active(Level world) { + return ObfuscationReflectionHelper.getPrivateValue(Level.class, world, "f_151512_"); + } + + private static List pending(Level world) { + return ObfuscationReflectionHelper.getPrivateValue(Level.class, world, "f_151503_"); + } + + static List registrations(Level world) { + List result = new ArrayList<>(active(world)); + result.addAll(pending(world)); + return java.util.Collections.unmodifiableList(result); + } + + static List drain(Level world) { + List held = new ArrayList<>(active(world)); + for (TickingBlockEntity entry : pending(world)) if (!held.contains(entry)) held.add(entry); + active(world).clear(); + pending(world).clear(); + return held; + } + + static void restore(Level world, List held) { + for (TickingBlockEntity entry : held) { + if (!entry.isRemoved() && !active(world).contains(entry) && !pending(world).contains(entry)) { + world.addBlockEntityTicker(entry); + } + } + held.clear(); + } + + static boolean contains(Level world, BlockEntity actual) { + if (actual == null || actual.isRemoved() || world.getBlockEntity(actual.getBlockPos()) != actual) return false; + return find(active(world), actual) != null || find(pending(world), actual) != null; + } + + static void remove(Level world, BlockEntity actual) { + if (world.getBlockEntity(actual.getBlockPos()) != actual) { + throw new AssertionError("Measured tile is not the native world's current instance"); + } + TickingBlockEntity first = find(active(world), actual), second = find(pending(world), actual); + if ((first == null ? 0 : 1) + (second == null ? 0 : 1) != 1) { + throw new AssertionError("Expected one native measured ticker registration"); + } + if (first != null) SteamReloadControlCases.removeMeasuredTile(active(world), first); + if (second != null) SteamReloadControlCases.removeMeasuredTile(pending(world), second); + if (contains(world, actual)) throw new AssertionError("Measured tile remained tickable"); + } + + private static TickingBlockEntity find(List entries, BlockEntity actual) { + TickingBlockEntity found = null; + for (TickingBlockEntity entry : entries) { + if (!entry.isRemoved() && entry.getPos().equals(actual.getBlockPos())) { + if (found != null) throw new AssertionError("Duplicate native ticker at " + actual.getBlockPos()); + found = entry; + } + } + return found; + } +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeEntityData.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeEntityData.java new file mode 100644 index 00000000..f022be1e --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeEntityData.java @@ -0,0 +1,56 @@ +package zone.moddev.mc.advantageworks; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Objects; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.Entity; + +/** Read-only access to the pinned Forge field without invoking its allocating getter. */ +final class NativeEntityData { + private static final Field PERSISTENT_DATA = persistentField(); + + private NativeEntityData() { } + + static boolean containsString(Entity entity, String key) { + CompoundTag data = peek(entity); + return data != null && data.contains(key, 8); + } + + static String string(Entity entity, String key) { + CompoundTag data = peek(entity); + return data == null ? "" : data.getString(key); + } + + private static CompoundTag peek(Entity entity) { + Objects.requireNonNull(entity, "Missing observed entity"); + try { + return (CompoundTag) PERSISTENT_DATA.get(entity); + } catch (IllegalAccessException exception) { + throw new IllegalStateException("Cannot read pinned native117 Entity.persistentData", exception); + } + } + + private static Field persistentField() { + try { + return requireNativeField(Entity.class.getDeclaredField("persistentData")); + } catch (NoSuchFieldException exception) { + throw new IllegalStateException("Missing pinned native117 Entity.persistentData", exception); + } + } + + static Field requireNativeField(Field field) { + if (field == null || field.getDeclaringClass() != Entity.class + || !field.getName().equals("persistentData") || field.getType() != CompoundTag.class + || !Modifier.isPrivate(field.getModifiers()) || Modifier.isStatic(field.getModifiers()) + || Modifier.isFinal(field.getModifiers())) { + throw new IllegalStateException("Unexpected pinned native117 persistent-data field: " + field); + } + try { + field.setAccessible(true); + } catch (RuntimeException exception) { + throw new IllegalStateException("Cannot access pinned native117 Entity.persistentData", exception); + } + return field; + } +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeEntityHold.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeEntityHold.java index e900a51c..4bd42869 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeEntityHold.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeEntityHold.java @@ -2,14 +2,20 @@ import java.util.IdentityHashMap; import java.util.Map; -import net.minecraft.entity.Entity; +import net.minecraft.world.entity.Entity; +import net.minecraft.server.level.ServerLevel; /** Holds native entity ticks without removing actors from the world's tracking indexes. */ final class NativeEntityHold { private final Map held = new IdentityHashMap<>(); void hold(Entity entity) { - if (!held.containsKey(entity)) held.put(entity, new State(entity)); + if (!(entity.level instanceof ServerLevel)) throw new AssertionError("Entity hold requires a native server world"); + if (!held.containsKey(entity)) { + State state = new State(entity); + held.put(entity, state); + state.gate.hold(entity, this); + } entity.canUpdate(false); } @@ -17,20 +23,19 @@ void restore() { for (Map.Entry entry : held.entrySet()) { Entity entity = entry.getKey(); State state = entry.getValue(); - // ServerWorld increments this even when canUpdate() is false. - entity.tickCount = state.ticks; - entity.canUpdate(state.updating); + if (entity.tickCount != state.ticks) throw new AssertionError("Entity ticked while held: " + entity.getUUID()); + state.gate.release(entity, this); } held.clear(); } private static final class State { - final boolean updating; final int ticks; + final NativeEntityTickGate gate; State(Entity entity) { - updating = entity.canUpdate(); ticks = entity.tickCount; + gate = NativeEntityTickGate.of((ServerLevel) entity.level); } } } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeEntityTickGate.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeEntityTickGate.java new file mode 100644 index 00000000..89ee987a --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeEntityTickGate.java @@ -0,0 +1,71 @@ +package zone.moddev.mc.advantageworks; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.lang.reflect.Field; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.EntityTickList; + +/** Test-only selective ticking hold; the native entity lookup and sections stay intact. */ +final class NativeEntityTickGate extends EntityTickList { + private final EntityTickList nativeTicks; + private final Map holds = new IdentityHashMap<>(); + + private NativeEntityTickGate(EntityTickList nativeTicks) { this.nativeTicks = nativeTicks; } + + static NativeEntityTickGate of(ServerLevel world) { + try { + Field field = tickerField(); + EntityTickList actual = (EntityTickList) field.get(world); + if (actual == null) throw new AssertionError("Missing actual native entity tick list"); + if (actual instanceof NativeEntityTickGate) return (NativeEntityTickGate) actual; + NativeEntityTickGate gate = new NativeEntityTickGate(actual); + field.set(world, gate); + return gate; + } catch (ReflectiveOperationException error) { throw new AssertionError("Native 1.17 ticker field is inaccessible", error); } + } + + private static Field tickerField() throws NoSuchFieldException { + // Exact official/SRG names for the pinned engine; plain JUnit has no ModLauncher remapper. + for (String name : new String[] {"entityTickList", "f_143243_"}) { + try { + Field field = ServerLevel.class.getDeclaredField(name); + if (field.getType() != EntityTickList.class) throw new AssertionError("Unexpected native ticker field type"); + field.setAccessible(true); + return field; + } catch (NoSuchFieldException absent) { } + } + throw new NoSuchFieldException("Native 1.17 entityTickList/f_143243_"); + } + + void hold(Entity entity, Object owner) { + holds.computeIfAbsent(entity, ignored -> new Hold(entity.canUpdate())).owners.add(owner); + entity.canUpdate(false); + } + + void release(Entity entity, Object owner) { + Hold hold = holds.get(entity); + if (hold == null || !hold.owners.remove(owner)) throw new AssertionError("Unobserved native entity hold"); + if (hold.owners.isEmpty()) { + holds.remove(entity); + entity.canUpdate(hold.updating); + } + } + + @Override public void add(Entity entity) { nativeTicks.add(entity); } + @Override public void remove(Entity entity) { nativeTicks.remove(entity); } + @Override public boolean contains(Entity entity) { return !holds.containsKey(entity) && nativeTicks.contains(entity); } + @Override public void forEach(Consumer action) { + nativeTicks.forEach(entity -> { if (!holds.containsKey(entity)) action.accept(entity); }); + } + + private static final class Hold { + final Set owners = Collections.newSetFromMap(new IdentityHashMap<>()); + final boolean updating; + Hold(boolean updating) { this.updating = updating; } + } +} 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 524b5754..10f978da 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 @@ -10,32 +10,31 @@ import java.nio.file.Files; import java.util.List; import java.util.UUID; -import net.minecraft.block.Block; -import net.minecraft.block.BlockState; -import net.minecraft.command.Commands; -import net.minecraft.enchantment.Enchantments; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; -import net.minecraft.network.IPacket; -import net.minecraft.network.NetworkManager; -import net.minecraft.network.PacketDirection; -import net.minecraft.network.play.ServerPlayNetHandler; -import net.minecraft.server.management.PlayerInteractionManager; -import net.minecraft.state.BooleanProperty; -import net.minecraft.util.Hand; -import net.minecraft.util.Direction; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.math.AxisAlignedBB; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.shapes.IBooleanFunction; -import net.minecraft.util.math.shapes.VoxelShapes; -import net.minecraft.world.GameType; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.commands.Commands; +import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.InteractionHand; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.phys.AABB; +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.level.GameType; +import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.common.ToolType; import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.registries.ForgeRegistries; @@ -63,13 +62,13 @@ private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); } - private static void witness(ServerWorld world, ServerPlayerEntity player, JsonArray rows, String id, + private static void witness(ServerLevel world, ServerPlayer player, JsonArray rows, String id, String blockId, int level, Item toolItem, boolean silk, boolean allowed, String dropId, int minimum, int maximum, int trials) { witness(world, player, rows, id, blockId, level, toolItem, silk, allowed, dropId, minimum, maximum, trials, -1); } - private static void witness(ServerWorld world, ServerPlayerEntity player, JsonArray rows, String id, + private static void witness(ServerLevel world, ServerPlayer player, JsonArray rows, String id, String blockId, int level, Item toolItem, boolean silk, boolean allowed, String dropId, int minimum, int maximum, int trials, int connectionMask) { JsonObject row = new JsonObject(); @@ -81,52 +80,57 @@ private static void witness(ServerWorld world, ServerPlayerEntity player, JsonAr ResourceLocation key = new ResourceLocation(blockId); require(ForgeRegistries.BLOCKS.containsKey(key), "Missing real block: " + blockId); Block block = ForgeRegistries.BLOCKS.getValue(key); - player.gameMode.setGameModeForPlayer(GameType.SURVIVAL); + player.gameMode.changeGameModeForPlayer(GameType.SURVIVAL); for (int trial = 0; trial < trials; trial++) { JsonObject evidence = new JsonObject(); evidence.addProperty("trial", trial); evidence.addProperty("worldTime", world.getGameTime()); - List indexed = world.getEntitiesOfClass(ItemEntity.class, new AxisAlignedBB(POS).inflate(2)); - evidence.addProperty("removedBeforeCleanup", indexed.stream().filter(entity -> entity.removed).count()); - evidence.addProperty("liveBeforeCleanup", indexed.stream().filter(entity -> !entity.removed).count()); + List indexed = world.getEntitiesOfClass(ItemEntity.class, new AABB(POS).inflate(2)); + evidence.addProperty("removedBeforeCleanup", indexed.stream().filter(entity -> entity.isRemoved()).count()); + evidence.addProperty("liveBeforeCleanup", indexed.stream().filter(entity -> !entity.isRemoved()).count()); dropEvidence.add(evidence); - harvestDrops(world, new AxisAlignedBB(POS).inflate(2)).forEach(ItemEntity::remove); - int remaining = harvestDrops(world, new AxisAlignedBB(POS).inflate(2)).size(); + harvestDrops(world, new AABB(POS).inflate(2)).forEach(ItemEntity::discard); + int remaining = harvestDrops(world, new AABB(POS).inflate(2)).size(); evidence.addProperty("liveAfterCleanup", remaining); require(remaining == 0, "Harvest fixture retained a live item before placement"); - world.setBlock(POS, net.minecraft.block.Blocks.AIR.defaultBlockState(), 2); + world.setBlock(POS, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState(), 2); BlockState requested = block.defaultBlockState(); if (connectionMask >= 0) { int bit = 0; for (Direction direction : Direction.values()) { - net.minecraft.state.Property property = block.getStateDefinition().getProperty(direction.getName()); + net.minecraft.world.level.block.state.properties.Property property = block.getStateDefinition().getProperty(direction.getName()); require(property instanceof BooleanProperty, "Missing inherited conduit connection state"); boolean connected = (connectionMask & (1 << bit++)) != 0; requested = requested.setValue((BooleanProperty) property, connected); world.setBlock(POS.relative(direction), connected ? block.defaultBlockState() - : net.minecraft.block.Blocks.AIR.defaultBlockState(), 2); + : net.minecraft.world.level.block.Blocks.AIR.defaultBlockState(), 2); } } require(world.setBlock(POS, requested, 2), "Cannot place harvest witness"); BlockState state = world.getBlockState(POS); require(state.equals(requested), "Native connection state changed before mining"); - boolean expectedSolid = blockId.endsWith("_ore") || blockId.equals("electricadvantage:electric_conduit"); + boolean expectedSolid = blockId.endsWith("_ore") || blockId.equals("baseelectric:electric_conduit"); require(state.canOcclude() == expectedSolid, "Legacy solid/non-solid property changed"); row.addProperty("actualSolid", state.canOcclude()); - boolean fullCollision = !VoxelShapes.joinIsNotEmpty(state.getCollisionShape(world, POS), - VoxelShapes.block(), IBooleanFunction.NOT_SAME); - require(fullCollision != blockId.equals("electricadvantage:electric_conduit"), + boolean fullCollision = !Shapes.joinIsNotEmpty(state.getCollisionShape(world, POS), + Shapes.block(), BooleanOp.NOT_SAME); + require(fullCollision != blockId.equals("baseelectric:electric_conduit"), "Legacy wire/frame/scaffold collision geometry changed"); row.addProperty("fullCollision", fullCollision); - if (level >= 0) require(state.getHarvestTool() == ToolType.PICKAXE - && state.getHarvestLevel() == level, "Historical harvest level/tool changed"); + if (level >= 0) { + require(state.is(net.minecraft.tags.BlockTags.MINEABLE_WITH_PICKAXE), "Historical pickaxe tag missing"); + require(state.is(net.minecraft.tags.BlockTags.NEEDS_STONE_TOOL) == (level == 1) + && !state.is(net.minecraft.tags.BlockTags.NEEDS_IRON_TOOL) + && !state.is(net.minecraft.tags.BlockTags.NEEDS_DIAMOND_TOOL), "Historical tool tier changed"); + require(state.requiresCorrectToolForDrops(), "Ore no longer requires the proper tool"); + } ItemStack tool = toolItem == null ? ItemStack.EMPTY : new ItemStack(toolItem); if (silk) tool.enchant(Enchantments.SILK_TOUCH, 1); - player.setItemInHand(Hand.MAIN_HAND, tool); + player.setItemInHand(InteractionHand.MAIN_HAND, tool); require(state.canHarvestBlock(world, POS, player) == allowed, "Tool failure/success gate changed"); require(player.gameMode.destroyBlock(POS) && world.isEmptyBlock(POS), "Native PlayerInteractionManager did not remove the block"); - List drops = harvestDrops(world, new AxisAlignedBB(POS).inflate(2)); + List drops = harvestDrops(world, new AABB(POS).inflate(2)); JsonArray actualDrops = new JsonArray(); for (ItemEntity entity : drops) { JsonObject actual = new JsonObject(); @@ -134,7 +138,7 @@ private static void witness(ServerWorld world, ServerPlayerEntity player, JsonAr actual.addProperty("uuid", entity.getUUID().toString()); actual.addProperty("item", entity.getItem().getItem().getRegistryName().toString()); actual.addProperty("count", entity.getItem().getCount()); - actual.addProperty("removed", entity.removed); + actual.addProperty("removed", entity.isRemoved()); actualDrops.add(actual); } evidence.add("actualDrops", actualDrops); @@ -162,23 +166,22 @@ private static void witness(ServerWorld world, ServerPlayerEntity player, JsonAr rows.add(row); } - private static void run(ServerWorld world, String label) throws Exception { + private static void run(ServerLevel world, String label) throws Exception { require(MARKER.isFile() && label.matches("[A-Za-z0-9_-]+"), "Unmarked harvest runtime or unsafe label"); File output = new File("advantage-works-results", "native-harvest-" + label + ".json"); require(!output.exists(), "Refusing to overwrite harvest evidence"); - ServerPlayerEntity player = new ServerPlayerEntity(world.getServer(), world, - new GameProfile(UUID.fromString("c791aefe-a372-4536-9251-27032c01f115"), "AdvWorksHarvest"), - new PlayerInteractionManager(world)); - player.connection = new ServerPlayNetHandler(world.getServer(), new NetworkManager(PacketDirection.SERVERBOUND), player) { - @Override public void send(IPacket packet) {} + ServerPlayer player = new ServerPlayer(world.getServer(), world, + new GameProfile(UUID.fromString("c791aefe-a372-4536-9251-27032c01f115"), "AdvWorksHarvest")); + player.connection = new ServerGamePacketListenerImpl(world.getServer(), new Connection(PacketFlow.SERVERBOUND), player) { + @Override public void send(Packet packet) {} }; player.setPos(POS.getX() + 0.5, POS.getY() + 1, POS.getZ() + 0.5); JsonArray rows = new JsonArray(); - String sulfur = "electricadvantage:sulfur_ore", lithium = "electricadvantage:li_ore"; + String sulfur = "baseelectric:sulfur_ore", lithium = "baseelectric:li_ore"; witness(world, player, rows, "sulfur/hand-fails", sulfur, 0, null, false, false, "", 0, 0, 1); witness(world, player, rows, "sulfur/axe-fails", sulfur, 0, Items.IRON_AXE, false, false, "", 0, 0, 1); - witness(world, player, rows, "sulfur/wood", sulfur, 0, Items.WOODEN_PICKAXE, false, true, "electricadvantage:sulfur_powder", 1, 4, 8); - witness(world, player, rows, "sulfur/stone", sulfur, 0, Items.STONE_PICKAXE, false, true, "electricadvantage:sulfur_powder", 1, 4, 8); + witness(world, player, rows, "sulfur/wood", sulfur, 0, Items.WOODEN_PICKAXE, false, true, "baseelectric:sulfur_powder", 1, 4, 8); + witness(world, player, rows, "sulfur/stone", sulfur, 0, Items.STONE_PICKAXE, false, true, "baseelectric:sulfur_powder", 1, 4, 8); witness(world, player, rows, "sulfur/silk", sulfur, 0, Items.WOODEN_PICKAXE, true, true, sulfur, 1, 1, 1); witness(world, player, rows, "lithium/hand-fails", lithium, 1, null, false, false, "", 0, 0, 1); witness(world, player, rows, "lithium/wood-fails", lithium, 1, Items.WOODEN_PICKAXE, false, false, "", 0, 0, 1); @@ -187,11 +190,11 @@ private static void run(ServerWorld world, String label) throws Exception { witness(world, player, rows, "lithium/stone", lithium, 1, Items.STONE_PICKAXE, false, true, lithium, 1, 1, 1); witness(world, player, rows, "lithium/diamond", lithium, 1, Items.DIAMOND_PICKAXE, false, true, lithium, 1, 1, 1); witness(world, player, rows, "lithium/silk", lithium, 1, Items.STONE_PICKAXE, true, true, lithium, 1, 1, 1); - witness(world, player, rows, "frame/hand", "poweradvantage:steel_frame", -1, null, false, true, "poweradvantage:steel_frame", 1, 1, 1); - witness(world, player, rows, "frame/silk", "poweradvantage:steel_frame", -1, Items.DIAMOND_PICKAXE, true, true, "poweradvantage:steel_frame", 1, 1, 1); + witness(world, player, rows, "frame/hand", "basepower:steel_frame", -1, null, false, true, "basepower:steel_frame", 1, 1, 1); + witness(world, player, rows, "frame/silk", "basepower:steel_frame", -1, Items.DIAMOND_PICKAXE, true, true, "basepower:steel_frame", 1, 1, 1); for (int mask = 0; mask < 64; mask++) { for (String family : new String[] {"wire", "scaffold"}) { - String identity = "electricadvantage:" + (family.equals("wire") ? "electric_conduit" : "electric_track"); + String identity = "baseelectric:" + (family.equals("wire") ? "electric_conduit" : "electric_track"); String prefix = family + "/" + mask; witness(world, player, rows, prefix + "/hand", identity, -1, null, false, true, identity, 1, 1, 1, mask); witness(world, player, rows, prefix + "/silk", identity, -1, Items.DIAMOND_PICKAXE, true, true, identity, 1, 1, 1, mask); @@ -200,8 +203,8 @@ private static void run(ServerWorld world, String label) throws Exception { JsonObject solar = new JsonObject(); solar.addProperty("id", "solar/native-solidity"); try { - Block block = ForgeRegistries.BLOCKS.getValue(new ResourceLocation("electricadvantage:photovoltaic_generator")); - require(block != null && block != net.minecraft.block.Blocks.AIR, "Missing native photovoltaic block"); + Block block = ForgeRegistries.BLOCKS.getValue(new ResourceLocation("baseelectric:photovoltaic_generator")); + require(block != null && block != net.minecraft.world.level.block.Blocks.AIR, "Missing native photovoltaic block"); world.setBlock(POS, block.defaultBlockState(), 2); require(world.getBlockState(POS).canOcclude(), "Legacy photovoltaic solid=true was lost"); solar.addProperty("actualSolid", true); @@ -210,14 +213,14 @@ private static void run(ServerWorld world, String label) throws Exception { solar.addProperty("status", "FAIL"); solar.addProperty("failure", error.toString()); } rows.add(solar); - world.setBlock(POS, net.minecraft.block.Blocks.AIR.defaultBlockState(), 2); - for (Direction direction : Direction.values()) world.setBlock(POS.relative(direction), net.minecraft.block.Blocks.AIR.defaultBlockState(), 2); - harvestDrops(world, new AxisAlignedBB(POS).inflate(2)).forEach(ItemEntity::remove); + world.setBlock(POS, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState(), 2); + for (Direction direction : Direction.values()) world.setBlock(POS.relative(direction), net.minecraft.world.level.block.Blocks.AIR.defaultBlockState(), 2); + harvestDrops(world, new AABB(POS).inflate(2)).forEach(ItemEntity::discard); JsonObject report = new JsonObject(); report.addProperty("label", label); - report.addProperty("minecraft", "1.16.5"); + report.addProperty("minecraft", "1.17.1"); report.addProperty("worldTime", world.getGameTime()); - report.addProperty("fixtureIsolation", "native116-live-drops-v1"); + report.addProperty("fixtureIsolation", "native117-live-drops-v1"); report.addProperty("status", java.util.stream.StreamSupport.stream(rows.spliterator(), false) .allMatch(row -> "PASS".equals(row.getAsJsonObject().get("status").getAsString())) ? "PASS" : "FAIL"); report.add("checks", rows); @@ -226,8 +229,8 @@ private static void run(ServerWorld world, String label) throws Exception { System.out.println("ADVHARVEST COMPLETE " + label); } - static List harvestDrops(net.minecraft.world.IEntityReader world, AxisAlignedBB bounds) { + static List harvestDrops(net.minecraft.world.level.EntityGetter world, AABB bounds) { // remove() only marks an entity; the chunk index can retain it until the next tick. - return world.getEntitiesOfClass(ItemEntity.class, bounds, entity -> !entity.removed); + return world.getEntitiesOfClass(ItemEntity.class, bounds, entity -> !entity.isRemoved()); } } 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 d050f964..9be1b0bb 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 @@ -10,19 +10,19 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.List; -import net.minecraft.block.Block; -import net.minecraft.block.BlockState; -import net.minecraft.command.Commands; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.inventory.IInventory; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.math.AxisAlignedBB; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.commands.Commands; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.phys.AABB; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.energy.CapabilityEnergy; @@ -34,10 +34,10 @@ import net.minecraftforge.items.CapabilityItemHandler; import net.minecraftforge.items.IItemHandler; import net.minecraftforge.registries.ForgeRegistries; -import zone.moddev.mc.poweradvantage.machines.conveyors.BlockConveyor; -import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityConveyor; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.StorageTankTileEntity; -import zone.moddev.mc.poweradvantage.util.InventoryLocks; +import zone.moddev.mc.basepower.machines.conveyors.BlockConveyor; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityConveyor; +import zone.moddev.mc.basepower.machines.fluidmachines.StorageTankTileEntity; +import zone.moddev.mc.basepower.util.InventoryLocks; /** Capability and native loot witnesses, restricted to a marked disposable world. */ final class NativeInteropProbe { @@ -85,26 +85,26 @@ private static Block block(String id) { return ForgeRegistries.BLOCKS.getValue(key); } - private static void place(ServerWorld world, BlockPos pos, BlockState state) { + private static void place(ServerLevel world, BlockPos pos, BlockState state) { require(pos.getX() >= 1023 && pos.getX() <= 1100 && pos.getY() == 8 && pos.getZ() >= 1023 && pos.getZ() <= 1030, "Outside interop fixture bounds"); - world.setBlock(pos, net.minecraft.block.Blocks.AIR.defaultBlockState(), 3); + world.setBlock(pos, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState(), 3); world.setBlock(pos, state, 3); } - private static IFluidHandler fluid(TileEntity tile) { + private static IFluidHandler fluid(BlockEntity tile) { return tile.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, Direction.UP) .orElseThrow(() -> new AssertionError("Missing native fluid capability")); } - private static void energy(ServerWorld world, JsonArray checks) { + private static void energy(ServerLevel world, JsonArray checks) { String[] ids = {"converter_rf_steam", "converter_rf_electricity", "converter_rf_quantum", "converter_tr_electricity"}; String[] types = {"steam", "electricity", "quantum", "electricity"}; for (int index = 0; index < ids.length; index++) { final String id = ids[index]; final ConduitType type = new ConduitType(types[index]); final BlockPos pos = new BlockPos(1060 + index * 8, 8, 1024); - place(world, pos, block("poweradvantage:" + id).defaultBlockState()); + place(world, pos, block("basepower:" + id).defaultBlockState()); TileEntityRFConverter tile = (TileEntityRFConverter) world.getBlockEntity(pos); check(checks, id + "/capability", () -> { IEnergyStorage storage = tile.getCapability(CapabilityEnergy.ENERGY, Direction.NORTH) @@ -147,45 +147,45 @@ private static void energy(ServerWorld world, JsonArray checks) { check(checks, id + "/invalidation", () -> { LazyOptional old = tile.getCapability(CapabilityEnergy.ENERGY, Direction.NORTH); require(old.isPresent(), "Missing original capability"); - world.setBlock(pos, net.minecraft.block.Blocks.AIR.defaultBlockState(), 3); + world.setBlock(pos, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState(), 3); require(!old.isPresent(), "Removed converter still exposes its stale capability"); - world.setBlock(pos, block("poweradvantage:" + id).defaultBlockState(), 3); + world.setBlock(pos, block("basepower:" + id).defaultBlockState(), 3); LazyOptional replacement = world.getBlockEntity(pos).getCapability(CapabilityEnergy.ENERGY, Direction.NORTH); require(replacement.isPresent() && replacement != old, "Replacement did not expose a fresh capability"); }); } } - private static void tank(ServerWorld world, JsonArray checks) { - Block block = block("poweradvantage:fluid_storage_tank"); + private static void tank(ServerLevel world, JsonArray checks) { + Block block = block("basepower:fluid_storage_tank"); place(world, TANK, block.defaultBlockState()); - TileEntity tile = world.getBlockEntity(TANK); + BlockEntity tile = world.getBlockEntity(TANK); check(checks, "fluid/simulation-conservation-and-identity", () -> { IFluidHandler tank = fluid(tile); - FluidStack offered = new FluidStack(net.minecraft.fluid.Fluids.WATER, 1000); + FluidStack offered = new FluidStack(net.minecraft.world.level.material.Fluids.WATER, 1000); require(tank.fill(offered, IFluidHandler.FluidAction.SIMULATE) == 1000 && tank.getFluidInTank(0).isEmpty(), "Simulated fill changed contents"); require(tank.fill(offered, IFluidHandler.FluidAction.EXECUTE) == 1000 && offered.getAmount() == 1000, "Native fill amount or input immutability failed"); - require(tank.fill(new FluidStack(net.minecraft.fluid.Fluids.LAVA, 1000), IFluidHandler.FluidAction.EXECUTE) == 0, + require(tank.fill(new FluidStack(net.minecraft.world.level.material.Fluids.LAVA, 1000), IFluidHandler.FluidAction.EXECUTE) == 0, "Different fluids mixed"); require(tank.drain(500, IFluidHandler.FluidAction.SIMULATE).getAmount() == 500 && tank.getFluidInTank(0).getAmount() == 1000, "Simulated drain changed contents"); FluidStack drained = tank.drain(500, IFluidHandler.FluidAction.EXECUTE); - require(drained.getAmount() == 500 && drained.getFluid() == net.minecraft.fluid.Fluids.WATER + require(drained.getAmount() == 500 && drained.getFluid() == net.minecraft.world.level.material.Fluids.WATER && tank.getFluidInTank(0).getAmount() == 500, "Drain lost fluid or identity"); }); check(checks, "portable-tank/native-break-drop-and-replace", () -> { LazyOptional old = tile.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, Direction.UP); require(world.destroyBlock(TANK, true), "Native block destruction failed"); require(!old.isPresent(), "Removed tank still exposes a stale capability"); - List drops = world.getEntitiesOfClass(ItemEntity.class, new AxisAlignedBB(TANK).inflate(2)); + List drops = world.getEntitiesOfClass(ItemEntity.class, new AABB(TANK).inflate(2)); require(drops.size() == 1, "Expected exactly one portable tank drop; got " + drops.size()); ItemStack item = drops.get(0).getItem().copy(); require(item.getItem() == block.asItem() && item.getCount() == 1 && item.hasTag() && item.getTag().getString("FluidID").equals("minecraft:water") && item.getTag().getInt("Volume") == 500, "Native loot lost portable fluid NBT: " + item); - drops.get(0).remove(); + drops.get(0).discard(); world.setBlock(TANK, block.defaultBlockState(), 3); block.setPlacedBy(world, TANK, block.defaultBlockState(), net.minecraftforge.common.util.FakePlayerFactory.getMinecraft(world), item); @@ -194,18 +194,18 @@ private static void tank(ServerWorld world, JsonArray checks) { }); } - private static void locks(ServerWorld world, JsonArray checks) { - place(world, BELT.west(), net.minecraft.block.Blocks.CHEST.defaultBlockState()); - place(world, BELT, block("poweradvantage:item_conveyor").defaultBlockState().setValue(BlockConveyor.FACING, Direction.EAST)); - place(world, BELT.east(), net.minecraft.block.Blocks.CHEST.defaultBlockState()); - IInventory input = (IInventory) world.getBlockEntity(BELT.west()); - IInventory output = (IInventory) world.getBlockEntity(BELT.east()); - input.setItem(0, new ItemStack(net.minecraft.item.Items.DIRT, 16)); + private static void locks(ServerLevel world, JsonArray checks) { + place(world, BELT.west(), net.minecraft.world.level.block.Blocks.CHEST.defaultBlockState()); + place(world, BELT, block("basepower:item_conveyor").defaultBlockState().setValue(BlockConveyor.FACING, Direction.EAST)); + place(world, BELT.east(), net.minecraft.world.level.block.Blocks.CHEST.defaultBlockState()); + Container input = (Container) world.getBlockEntity(BELT.west()); + Container output = (Container) world.getBlockEntity(BELT.east()); + input.setItem(0, new ItemStack(net.minecraft.world.item.Items.DIRT, 16)); TileEntityConveyor belt = (TileEntityConveyor) world.getBlockEntity(BELT); - TileEntity chest = world.getBlockEntity(BELT.west()); - CompoundNBT state = chest.save(new CompoundNBT()); + BlockEntity chest = world.getBlockEntity(BELT.west()); + CompoundTag state = chest.save(new CompoundTag()); state.putString("Lock", "AdvantageInteropKey"); - chest.load(chest.getBlockState(), state); + chest.load(state); check(checks, "locked-chest/conveyor-refuses-extraction", () -> { require(InventoryLocks.isLocked(input), "Native nonempty chest lock was not detected"); for (int i = 0; i < 160; i++) belt.tick(); @@ -213,17 +213,18 @@ private static void locks(ServerWorld world, JsonArray checks) { "Conveyor bypassed locked source"); }); state.remove("Lock"); - chest.load(chest.getBlockState(), state); + chest.load(state); check(checks, "unlocked-chest/conveyor-transfers-exactly", () -> { require(!InventoryLocks.isLocked(input), "Cleared lock still blocks extraction"); for (int i = 0; i < 160; i++) belt.tick(); require(input.isEmpty() && belt.isEmpty() && output.getItem(0).getCount() == 16 - && output.getItem(0).getItem() == net.minecraft.item.Items.DIRT, + && output.getItem(0).getItem() == net.minecraft.world.item.Items.DIRT, "Unlocked native chest did not deliver exactly sixteen dirt"); }); } private static final class CallbackTank extends StorageTankTileEntity { + CallbackTank() { super(BlockPos.ZERO, zone.moddev.mc.basepower.init.Blocks.storage_tank.defaultBlockState()); } void reviveNativeCapabilities() { super.reviveCaps(); } } @@ -236,9 +237,9 @@ private static CallbackTank callbackTank() { CallbackTank tile = new CallbackTank(); require(tile.getLevel() == null && !tile.isRemoved(), "Callback fixture is not a fresh standalone tank"); cyano.poweradvantage.api.fluid.FluidStack contents = - new cyano.poweradvantage.api.fluid.FluidStack(net.minecraft.fluid.Fluids.WATER, 500); - contents.tag = new CompoundNBT(); - contents.tag.putString("InteropWitness", "native116-callback"); + new cyano.poweradvantage.api.fluid.FluidStack(net.minecraft.world.level.material.Fluids.WATER, 500); + contents.tag = new CompoundTag(); + contents.tag.putString("InteropWitness", "native117-callback"); tile.getTank().setFluid(contents); return tile; } @@ -261,12 +262,12 @@ private static void capabilityCallbacks(JsonArray checks) { LazyOptional oldFluid = tile.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, Direction.UP); LazyOptional oldItems = tile.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, Direction.DOWN); require(oldFluid.isPresent() && oldItems.isPresent(), "Standalone tank lacks native capability wrappers"); - CompoundNBT contents = tile.getTank().writeToNBT(new CompoundNBT()); + CompoundTag contents = tile.getTank().writeToNBT(new CompoundTag()); tile.onChunkUnloaded(); require(!tile.isRemoved(), "Native unload callback unexpectedly marked the standalone tank removed"); require(!oldFluid.isPresent() && !oldItems.isPresent(), "Unload callback left old optionals valid"); requireUnavailable(tile); - require(contents.equals(tile.getTank().writeToNBT(new CompoundNBT())) + require(contents.equals(tile.getTank().writeToNBT(new CompoundTag())) && tile.getContainerSize() == 0 && tile.isEmpty(), "Unload callback changed tank contents"); }); check(checks, "capability/native-clear-removed-and-revive", () -> { @@ -277,11 +278,11 @@ private static void capabilityCallbacks(JsonArray checks) { IFluidHandler oldFluidWrapper = oldFluid.orElseThrow(() -> new AssertionError("Missing original fluid wrapper")); IItemHandler oldItemWrapper = oldItems.orElseThrow(() -> new AssertionError("Missing original item wrapper")); FluidStack before = oldFluidWrapper.getFluidInTank(0); - require(before.getFluid() == net.minecraft.fluid.Fluids.WATER && before.getAmount() == 500 - && before.hasTag() && before.getTag().getString("InteropWitness").equals("native116-callback") + require(before.getFluid() == net.minecraft.world.level.material.Fluids.WATER && before.getAmount() == 500 + && before.hasTag() && before.getTag().getString("InteropWitness").equals("native117-callback") && oldItemWrapper.getSlots() == 0, "Standalone wrappers do not expose the seeded tank contents"); - CompoundNBT contents = tile.getTank().writeToNBT(new CompoundNBT()); - CompoundNBT exposedFluid = before.writeToNBT(new CompoundNBT()); + CompoundTag contents = tile.getTank().writeToNBT(new CompoundTag()); + CompoundTag exposedFluid = before.writeToNBT(new CompoundTag()); tile.onChunkUnloaded(); require(!tile.isRemoved() && !oldFluid.isPresent() && !oldItems.isPresent(), "Revival fixture was not invalidated while unremoved"); @@ -296,15 +297,15 @@ private static void capabilityCallbacks(JsonArray checks) { && currentFluidWrapper != oldFluidWrapper && currentItemWrapper != oldItemWrapper, "Native revival did not create fresh fluid and item wrappers; clearRemoved=" + clearRemoved); require(!oldFluid.isPresent() && !oldItems.isPresent(), "Native revival revalidated old optionals"); - require(contents.equals(tile.getTank().writeToNBT(new CompoundNBT())) - && exposedFluid.equals(currentFluidWrapper.getFluidInTank(0).writeToNBT(new CompoundNBT())) + require(contents.equals(tile.getTank().writeToNBT(new CompoundTag())) + && exposedFluid.equals(currentFluidWrapper.getFluidInTank(0).writeToNBT(new CompoundTag())) && currentItemWrapper.getSlots() == 0 && tile.getContainerSize() == 0 && tile.isEmpty(), "Native revival changed fluid identity, amount, tag or empty item contents"); } }); } - private static void run(ServerWorld world, String phase) throws Exception { + private static void run(ServerLevel world, String phase) throws Exception { require(MARKER.isFile() && world.getServer().getWorldData().getLevelName().equals("AdvantageWorksMigration"), "Unmarked world"); require(phase.equals("exercise") || phase.equals("restored"), "Unknown interop phase"); JsonArray checks = new JsonArray(); @@ -316,12 +317,12 @@ private static void run(ServerWorld world, String phase) throws Exception { } else { check(checks, "portable-tank/full-process-restart", () -> { FluidStack stack = fluid(world.getBlockEntity(TANK)).getFluidInTank(0); - require(stack.getFluid() == net.minecraft.fluid.Fluids.WATER && stack.getAmount() == 500, + require(stack.getFluid() == net.minecraft.world.level.material.Fluids.WATER && stack.getAmount() == 500, "Restart lost portable tank contents"); }); check(checks, "conveyor/full-process-restart", () -> { - IInventory output = (IInventory) world.getBlockEntity(BELT.east()); - require(output.getItem(0).getItem() == net.minecraft.item.Items.DIRT + Container output = (Container) world.getBlockEntity(BELT.east()); + require(output.getItem(0).getItem() == net.minecraft.world.item.Items.DIRT && output.getItem(0).getCount() == 16, "Restart changed exact conveyor output"); }); } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeTicketGeometry.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeTicketGeometry.java index ad5bd668..7db7cf26 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeTicketGeometry.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeTicketGeometry.java @@ -6,34 +6,34 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; -import net.minecraft.world.chunk.ChunkStatus; -import net.minecraft.world.server.ChunkHolder; -import net.minecraft.world.server.ChunkManager; -import net.minecraft.world.server.Ticket; -import net.minecraft.world.server.TicketManager; -import net.minecraft.world.server.TicketType; -import net.minecraft.world.server.ServerWorld; -import net.minecraft.world.server.ServerChunkProvider; -import net.minecraftforge.fml.common.ObfuscationReflectionHelper; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.Ticket; +import net.minecraft.server.level.DistanceManager; +import net.minecraft.server.level.TicketType; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraftforge.fml.util.ObfuscationReflectionHelper; /** Read-only engine observations; never adds tickets or loads witness chunks. */ final class NativeTicketGeometry { private NativeTicketGeometry() {} - static Map observe(ServerWorld world) { + static Map observe(ServerLevel world) { try { int fullLevel = -1, entityTickingLevel = -1; - for (int level = 0; level <= ChunkManager.MAX_CHUNK_DISTANCE; level++) { + for (int level = 0; level <= ChunkMap.MAX_CHUNK_DISTANCE; level++) { if (ChunkHolder.getStatus(level) == ChunkStatus.FULL) fullLevel = level; - if (ChunkHolder.getFullChunkStatus(level) == ChunkHolder.LocationType.ENTITY_TICKING) + if (ChunkHolder.getFullChunkStatus(level) == ChunkHolder.FullChunkStatus.ENTITY_TICKING) entityTickingLevel = level; } - ServerChunkProvider provider = world.getChunkSource(); - TicketManager manager = ObfuscationReflectionHelper.getPrivateValue( - ServerChunkProvider.class, provider, "field_217240_d"); + ServerChunkCache provider = world.getChunkSource(); + DistanceManager manager = ObfuscationReflectionHelper.getPrivateValue( + ServerChunkCache.class, provider, "f_8327_"); // Forge 36 adds a second map, forcedTickets; observe the vanilla tickets map only. Long2ObjectOpenHashMap tickets = ObfuscationReflectionHelper.getPrivateValue( - TicketManager.class, manager, "field_219377_e"); + DistanceManager.class, manager, "f_140761_"); Set forcedLevels = new TreeSet<>(); for (Object value : tickets.values()) { for (Object entry : (Iterable) value) { @@ -45,8 +45,8 @@ static Map observe(ServerWorld world) { result.put("fullLevel", fullLevel); result.put("entityTickingLevel", entityTickingLevel); result.put("maxDistance", ChunkStatus.maxDistance()); - result.put("lastHolderLevel", ChunkManager.MAX_CHUNK_DISTANCE); - result.put("holderRadius", ChunkManager.MAX_CHUNK_DISTANCE - entityTickingLevel); + result.put("lastHolderLevel", ChunkMap.MAX_CHUNK_DISTANCE); + result.put("holderRadius", ChunkMap.MAX_CHUNK_DISTANCE - entityTickingLevel); result.put("observedForcedLevels", new ArrayList<>(forcedLevels)); return result; } catch (Exception error) { diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/PowerOperationalCases.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/PowerOperationalCases.java index eb309199..425fbb14 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/PowerOperationalCases.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/PowerOperationalCases.java @@ -1,12 +1,12 @@ package zone.moddev.mc.advantageworks; import java.util.List; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; -import net.minecraft.util.Direction; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.AxisAlignedBB; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.AABB; final class PowerOperationalCases { static void register(List cases) { @@ -15,8 +15,8 @@ static void register(List cases) { "power/conveyor/locked-" + (sourceLocked ? "source" : "destination"), "Automation must not bypass a vanilla chest lock, at either end of the belt", 160, c -> { - conveyor(c, "poweradvantage:item_conveyor"); c.put(4, 2, 5, 0, "minecraft:dirt", 16); - net.minecraft.nbt.CompoundNBT lock = new net.minecraft.nbt.CompoundNBT(); lock.putString("Lock", "Works key"); + conveyor(c, "basepower:item_conveyor"); c.put(4, 2, 5, 0, "minecraft:dirt", 16); + net.minecraft.nbt.CompoundTag lock = new net.minecraft.nbt.CompoundTag(); lock.putString("Lock", "Works key"); c.seed(sourceLocked ? 4 : 6, 2, 5, lock); }, c -> { c.require(c.count(6, 2, 5, "minecraft:dirt") == 0, "locked route delivers no stock"); @@ -28,7 +28,7 @@ static void register(List cases) { "A finite 16 dirt reaches the chest in the declared direction, without loss or extra items", 200, c -> { BlockPos offset = BlockPos.ZERO.relative(direction); - c.block(8, 6, 8, "poweradvantage:item_conveyor"); c.facing(8, 6, 8, direction.getName()); + c.block(8, 6, 8, "basepower:item_conveyor"); c.facing(8, 6, 8, direction.getName()); c.block(8 - offset.getX(), 6 - offset.getY(), 8 - offset.getZ(), "minecraft:chest"); c.block(8 + offset.getX(), 6 + offset.getY(), 8 + offset.getZ(), "minecraft:chest"); c.put(8 - offset.getX(), 6 - offset.getY(), 8 - offset.getZ(), 0, "minecraft:dirt", 16); @@ -40,14 +40,14 @@ static void register(List cases) { })); } cases.add(new MachineScenario("power/conveyor/blocked", "Full output retains every dirt item in source or belt", 200, - c -> { conveyor(c, "poweradvantage:item_conveyor"); c.put(4, 2, 5, 0, "minecraft:dirt", 16); full(c, 6, 2, 5); }, + c -> { conveyor(c, "basepower:item_conveyor"); c.put(4, 2, 5, 0, "minecraft:dirt", 16); full(c, 6, 2, 5); }, c -> { c.require(c.count(4, 2, 5, "minecraft:dirt") + c.count(5, 2, 5, "minecraft:dirt") == 16, "blocked conveyor retains all inputs"); c.require(c.count(6, 2, 5, "minecraft:cobblestone") == 1728, "full output contents unchanged"); })); cases.add(new MachineScenario("power/conveyor/item-entity", "Dropped dirt is collected and delivered without a duplicate entity", 200, c -> { - c.block(5, 2, 5, "poweradvantage:item_conveyor"); c.facing(5, 2, 5, "east"); + c.block(5, 2, 5, "basepower:item_conveyor"); c.facing(5, 2, 5, "east"); c.block(6, 2, 5, "minecraft:chest"); c.block(4, 1, 5, "minecraft:stone"); c.block(3, 2, 5, "minecraft:stone"); c.block(4, 2, 4, "minecraft:stone"); c.block(4, 2, 6, "minecraft:stone"); BlockPos p = c.at(4, 2, 5); @@ -56,11 +56,11 @@ static void register(List cases) { c.world.addFreshEntity(dropped); }, c -> { c.require(c.count(6, 2, 5, "minecraft:dirt") == 16, "dropped input delivered"); - c.require(c.world.getEntitiesOfClass(ItemEntity.class, new AxisAlignedBB(c.at(3, 0, 4), c.at(8, 5, 7))).isEmpty(), "no duplicate/lost item entity remains"); + c.require(c.world.getEntitiesOfClass(ItemEntity.class, new AABB(c.at(3, 0, 4), c.at(8, 5, 7))).isEmpty(), "no duplicate/lost item entity remains"); })); cases.add(new MachineScenario("power/conveyor/nbt-preserved", "Distinct custom item data must not merge into or overwrite another stack", 200, c -> { - conveyor(c, "poweradvantage:item_conveyor"); + conveyor(c, "basepower:item_conveyor"); c.put(4, 2, 5, 0, "minecraft:dirt", 3); c.put(4, 2, 5, 1, "minecraft:dirt", 3); c.inventory(4, 2, 5).getItem(0).getOrCreateTag().putString("Witness", "A"); c.inventory(4, 2, 5).getItem(1).getOrCreateTag().putString("Witness", "B"); @@ -87,14 +87,14 @@ static void register(List cases) { }; for (String[] filter : filters) cases.add(new MachineScenario("power/filter/" + filter[0], "Eight matching items go down; eight nonmatching items go forward; bounded inputs exhaust", 300, - c -> { filter(c, "poweradvantage:item_filter_" + filter[0]); c.put(4, 2, 5, 0, filter[1], 8); c.put(4, 2, 5, 1, filter[2], 8); }, + c -> { filter(c, "basepower:item_filter_" + filter[0]); c.put(4, 2, 5, 0, filter[1], 8); c.put(4, 2, 5, 1, filter[2], 8); }, c -> filterOutcome(c, filter[1], filter[2], true))); for (int ghosts = 0; ghosts <= 3; ghosts++) { final int slots = ghosts; cases.add(new MachineScenario("power/filter/inventory-" + slots, "Zero references pass all; one or several references sort matching dirt, retaining reference slots", 300, c -> { - filter(c, "poweradvantage:item_filter_inventory"); + filter(c, "basepower:item_filter_inventory"); c.put(4, 2, 5, 0, "minecraft:dirt", 8); c.put(4, 2, 5, 1, "minecraft:apple", 8); if (slots > 0) c.put(5, 2, 5, 1, "minecraft:dirt", 1); if (slots > 1) { c.put(5, 2, 5, 2, "minecraft:cobblestone", 1); c.put(5, 2, 5, 3, "minecraft:coal", 1); } @@ -108,7 +108,7 @@ static void register(List cases) { cases.add(new MachineScenario("power/filter/overflow-" + flags, "Use lower output first, overflow forward when lower is full, retain input when both are full", 300, c -> { - filter(c, "poweradvantage:item_filter_overflow"); c.put(4, 2, 5, 0, "minecraft:dirt", 16); + filter(c, "basepower:item_filter_overflow"); c.put(4, 2, 5, 0, "minecraft:dirt", 16); if (lowerFull) full(c, 5, 1, 5); if (forwardFull) full(c, 6, 2, 5); }, c -> { int lower = c.count(5, 1, 5, "minecraft:dirt"), forward = c.count(6, 2, 5, "minecraft:dirt"); @@ -117,10 +117,10 @@ static void register(List cases) { c.require(c.count(4, 2, 5, "minecraft:dirt") + c.count(5, 2, 5, "minecraft:dirt") + lower + forward == 16, "overflow conservation even with both outputs blocked"); })); } - for (String fluid : new String[]{"minecraft:water", "poweradvantage:crude_oil", "poweradvantage:refined_oil"}) { + for (String fluid : new String[]{"minecraft:water", "basepower:crude_oil", "basepower:refined_oil"}) { cases.add(new MachineScenario("power/drain/" + fluid.replace(':', '_'), "Exactly one native fluid source travels through drain and pipe to storage with registry identity intact", 80, - c -> { drain(c, fluid, "poweradvantage:fluid_storage_tank", false); }, + c -> { drain(c, fluid, "basepower:fluid_storage_tank", false); }, c -> { c.require(c.world.isEmptyBlock(c.at(5, 2, 5)), "native source collected"); c.require(c.fluidAmount(7, 1, 5) == 1000 && c.fluidAmount(5, 1, 5) == 0, "exact source volume delivered"); @@ -129,24 +129,24 @@ static void register(List cases) { } cases.add(new MachineScenario("power/drain/non-source", "Flowing water without a source cannot create a bucket", 80, c -> { - drain(c, "minecraft:water", "poweradvantage:fluid_storage_tank", false); - c.world.setBlock(c.at(5, 2, 5), net.minecraft.block.Blocks.WATER.defaultBlockState() - .setValue(net.minecraft.block.FlowingFluidBlock.LEVEL, 7), 3); + drain(c, "minecraft:water", "basepower:fluid_storage_tank", false); + c.world.setBlock(c.at(5, 2, 5), net.minecraft.world.level.block.Blocks.WATER.defaultBlockState() + .setValue(net.minecraft.world.level.block.LiquidBlock.LEVEL, 7), 3); }, c -> c.require(c.fluidAmount(5, 1, 5) + c.fluidAmount(7, 1, 5) == 0, "no source means no collected millibuckets"))); for (boolean matching : new boolean[]{true, false}) cases.add(new MachineScenario("power/tank/filter-" + matching, "A bucket reference admits matching water or rejects it without losing the source volume", 80, - c -> { drain(c, "minecraft:water", "poweradvantage:fluid_metal_tank", false); c.put(7, 1, 5, 0, matching ? "minecraft:water_bucket" : "minecraft:lava_bucket", 1); }, + c -> { drain(c, "minecraft:water", "basepower:fluid_metal_tank", false); c.put(7, 1, 5, 0, matching ? "minecraft:water_bucket" : "minecraft:lava_bucket", 1); }, c -> { c.require(c.fluidAmount(7, 1, 5) == (matching ? 1000 : 0), "bucket filter controls actual network acceptance"); c.require(c.fluidAmount(7, 1, 5) + c.fluidAmount(5, 1, 5) == 1000, "rejected water is retained rather than deleted"); })); cases.add(new MachineScenario("power/tank/full", "Full metal storage never overfills or deletes the extra source bucket", 80, - c -> { drain(c, "minecraft:water", "poweradvantage:fluid_metal_tank", false); c.tank(7, 1, 5, "minecraft:water", 10000); }, + c -> { drain(c, "minecraft:water", "basepower:fluid_metal_tank", false); c.tank(7, 1, 5, "minecraft:water", 10000); }, c -> c.require(c.fluidAmount(7, 1, 5) == 10000 && c.fluidAmount(5, 1, 5) == 1000, "full output applies backpressure and conserves 11000 mB"))); for (boolean signal : new boolean[]{true, false}) cases.add(new MachineScenario("power/fluid-switch/" + signal, "Redstone isolates the fluid path; removing it permits transfer without replacing machinery", 100, c -> { - drain(c, "minecraft:water", "poweradvantage:fluid_storage_tank", true); + drain(c, "minecraft:water", "basepower:fluid_storage_tank", true); if (signal) c.block(6, 2, 5, "minecraft:redstone_block"); }, c -> { c.require(c.fluidAmount(7, 1, 5) == (signal ? 0 : 1000), "redstone controls fluid delivery"); @@ -155,7 +155,7 @@ static void register(List cases) { for (boolean blocked : new boolean[]{true, false}) cases.add(new MachineScenario("power/discharge/" + blocked, "A discharge places one bucket into air, but must never replace solid bedrock", 80, c -> { - c.block(5, 2, 5, "poweradvantage:fluid_discharge"); + c.block(5, 2, 5, "basepower:fluid_discharge"); for (Direction direction : Direction.values()) if (direction != Direction.UP) { BlockPos p = BlockPos.ZERO.relative(direction); c.block(5 + p.getX(), 1 + p.getY(), 5 + p.getZ(), "minecraft:bedrock"); @@ -164,7 +164,7 @@ static void register(List cases) { c.tank(5, 2, 5, "minecraft:water", 1000); }, c -> { c.require(c.fluidAmount(5, 2, 5) == (blocked ? 1000 : 0), "discharge respects available placement space"); - c.require(blocked ? c.world.getBlockState(c.at(5, 1, 5)).is(net.minecraft.block.Blocks.BEDROCK) + c.require(blocked ? c.world.getBlockState(c.at(5, 1, 5)).is(net.minecraft.world.level.block.Blocks.BEDROCK) : c.world.getFluidState(c.at(5, 1, 5)).isSource(), "solid host preserved or actual source placed"); })); } @@ -176,18 +176,18 @@ private static void stills(List cases) { cases.add(new MachineScenario("power/still/" + mode, "Finite crude oil produces refined oil at 2:1; fuel, valid input, capacity and redstone govern useful work", 620, c -> { - c.block(8, 4, 8, "poweradvantage:still"); - zone.moddev.mc.poweradvantage.machines.fluidmachines.StillTileEntity still = - (zone.moddev.mc.poweradvantage.machines.fluidmachines.StillTileEntity)c.tile(8, 4, 8); + c.block(8, 4, 8, "basepower:still"); + zone.moddev.mc.basepower.machines.fluidmachines.StillTileEntity still = + (zone.moddev.mc.basepower.machines.fluidmachines.StillTileEntity)c.tile(8, 4, 8); still.getInputTank().setFluid(new cyano.poweradvantage.api.fluid.FluidStack( - mode.equals("invalid") ? net.minecraft.fluid.Fluids.WATER - : net.minecraftforge.registries.ForgeRegistries.FLUIDS.getValue(new net.minecraft.util.ResourceLocation("poweradvantage:crude_oil")), 1000)); + mode.equals("invalid") ? net.minecraft.world.level.material.Fluids.WATER + : net.minecraftforge.registries.ForgeRegistries.FLUIDS.getValue(new net.minecraft.resources.ResourceLocation("basepower:crude_oil")), 1000)); if (!mode.equals("no-fuel")) c.put(8, 4, 8, 0, "minecraft:coal", 1); - if (output > 0) c.tank(8, 4, 8, "poweradvantage:refined_oil", output); + if (output > 0) c.tank(8, 4, 8, "basepower:refined_oil", output); if (mode.equals("redstone")) c.block(9, 4, 8, "minecraft:redstone_block"); }, c -> { - zone.moddev.mc.poweradvantage.machines.fluidmachines.StillTileEntity still = - (zone.moddev.mc.poweradvantage.machines.fluidmachines.StillTileEntity)c.tile(8, 4, 8); + zone.moddev.mc.basepower.machines.fluidmachines.StillTileEntity still = + (zone.moddev.mc.basepower.machines.fluidmachines.StillTileEntity)c.tile(8, 4, 8); c.require(still.getInputTank().getFluidAmount() == 1000 - product * 2, "exact raw input debit"); c.require(c.fluidAmount(8, 4, 8) == output + product, "exact bounded distillation product"); if (product == 0 && !mode.equals("no-fuel")) @@ -209,12 +209,12 @@ private static void filterOutcome(MachineContext c, String matching, String othe c.require(c.count(4, 2, 5, matching) + c.count(4, 2, 5, other) == 0 && c.inventory(5, 2, 5).getItem(0).isEmpty(), "finite source and filter buffer exhausted"); } private static void drain(MachineContext c, String fluid, String tank, boolean switched) { - c.block(5, 1, 5, "poweradvantage:fluid_drain"); + c.block(5, 1, 5, "basepower:fluid_drain"); for (Direction d : new Direction[]{Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST}) { BlockPos p = BlockPos.ZERO.relative(d); c.block(5 + p.getX(), 2, 5 + p.getZ(), "minecraft:bedrock"); } c.block(5, 2, 5, fluid); - c.block(6, 1, 5, switched ? "poweradvantage:fluid_switch" : "poweradvantage:fluid_pipe"); + c.block(6, 1, 5, switched ? "basepower:fluid_switch" : "basepower:fluid_pipe"); c.block(7, 1, 5, tank); } } 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 7abcaf4d..ba31f88c 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 @@ -10,25 +10,25 @@ import java.util.Arrays; import java.util.Collections; import java.util.Map; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.inventory.IInventory; -import net.minecraft.item.ItemStack; -import net.minecraft.item.BlockItem; -import net.minecraft.item.ItemUseContext; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.util.Direction; -import net.minecraft.util.ActionResultType; -import net.minecraft.util.math.AxisAlignedBB; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.tileentity.FurnaceTileEntity; -import net.minecraft.util.Hand; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.FurnaceBlockEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; import net.minecraftforge.registries.ForgeRegistries; -import zone.moddev.mc.poweradvantage.init.Fluids; -import zone.moddev.mc.poweradvantage.machines.fluidmachines.StillTileEntity; -import zone.moddev.mc.poweradvantage.util.FluidContainerHelper; +import zone.moddev.mc.basepower.init.Fluids; +import zone.moddev.mc.basepower.machines.fluidmachines.StillTileEntity; +import zone.moddev.mc.basepower.util.FluidContainerHelper; import static zone.moddev.mc.advantageworks.ProgressionProbe.*; /** Non-table processing edges are verified with live bounded machines here. */ @@ -38,42 +38,67 @@ final class ProcessProgression { static void reset() { evidence = new JsonArray(); crusherWitnesses.clear(); } static JsonArray witnesses() { return evidence; } - static void smelt(ServerWorld world, JsonObject fixture, String recipe, ItemStack input, ItemStack expected) { - FurnaceTileEntity furnace = (FurnaceTileEntity) fresh(world, fixture, + static void smelt(ServerLevel world, JsonObject fixture, String recipe, ItemStack input, ItemStack expected) { + FurnaceBlockEntity furnace = (FurnaceBlockEntity) fresh(world, fixture, new BlockPos(16394, 80, 16384), "minecraft:furnace"); furnace.setItem(0, input.copy()); furnace.setItem(1, stack("minecraft:coal")); - for (int tick = 0; tick < 201; tick++) furnace.tick(); + for (int tick = 0; tick < 201; tick++) net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity.serverTick( + world, furnace.getBlockPos(), furnace.getBlockState(), furnace); ItemStack output = furnace.getItem(2); require(furnace.getItem(0).isEmpty() && furnace.getItem(1).isEmpty() && !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.CompoundNBT())); + + ", expected=" + expected + ", saved=" + furnace.save(new net.minecraft.nbt.CompoundTag())); record("live_furnace:" + recipe, input.getCount(), output.getCount()); } - static void hammer(ServerWorld world, ServerPlayerEntity player, Map known, + static void hammer(ServerLevel world, ServerPlayer player, Map known, JsonObject fixture, String recipe, ItemStack input, ItemStack expected) { BlockPos pos = new BlockPos(16396, 80, 16384); fresh(world, fixture, pos, "minecraft:stone"); - AxisAlignedBB area = new AxisAlignedBB(pos.above()); + AABB area = new AABB(pos.above()); for (ItemEntity entity : world.getEntitiesOfClass(ItemEntity.class, area)) { entity.setItem(ItemStack.EMPTY); - entity.remove(); + entity.discard(); } String hammer = known.keySet().stream().filter(key -> key.startsWith("basemetals:") && key.endsWith("_crackhammer")).findFirst().orElseThrow(() -> new IllegalStateException("No craftable hammer")); - player.inventory.clearContent(); - player.setItemInHand(Hand.MAIN_HAND, stack(hammer)); + player.getInventory().clearContent(); + player.setItemInHand(InteractionHand.MAIN_HAND, stack(hammer)); player.setShiftKeyDown(false); ItemEntity source = new ItemEntity(world, pos.getX() + 0.5D, pos.getY() + 1.1D, pos.getZ() + 0.5D, input.copy()); - world.addFreshEntity(source); - ActionResultType action = player.getMainHandItem().getItem().useOn( - new ItemUseContext(player, Hand.MAIN_HAND, new net.minecraft.util.math.BlockRayTraceResult( - new net.minecraft.util.math.vector.Vector3d(pos.getX() + 0.5D, pos.getY() + 1D, pos.getZ() + 0.5D), + boolean accepted = world.addFreshEntity(source); + JsonObject attempt = new JsonObject(); + attempt.addProperty("protocol", "native117-hammer-query-v1"); + attempt.addProperty("recipe", recipe); + attempt.addProperty("sourceUuid", source.getUUID().toString()); + attempt.addProperty("accepted", accepted); + attempt.addProperty("inputNbt", input.save(new net.minecraft.nbt.CompoundTag()).toString()); + attempt.addProperty("expectedNbt", expected.save(new net.minecraft.nbt.CompoundTag()).toString()); + JsonObject before = hammerSnapshot(world, player, pos, area, source); + attempt.add("before", before); + attempt.addProperty("sourceMatchesInput", ItemStack.matches(source.getItem(), input)); + JsonObject selection = hammerRecipeSelection(world, player.getMainHandItem(), source.getItem(), recipe); + attempt.add("baseSelection", selection); + System.out.println("ADVHAMMER BEFORE " + attempt); + require(HammerQueryGate.selectorObserved(selection), + "Live hammer recipe-selection observer failed " + recipe); + require(HammerQueryGate.ready(accepted, source.isRemoved(), + attempt.get("sourceMatchesInput").getAsBoolean(), before), + "Live hammer setup not query-ready " + recipe); + InteractionResult action = player.getMainHandItem().getItem().useOn( + new UseOnContext(player, InteractionHand.MAIN_HAND, new net.minecraft.world.phys.BlockHitResult( + new net.minecraft.world.phys.Vec3(pos.getX() + 0.5D, pos.getY() + 1D, pos.getZ() + 0.5D), Direction.UP, pos, false))); - require(action == ActionResultType.SUCCESS && source.getItem().isEmpty() + attempt.addProperty("action", action.name()); + attempt.addProperty("success", action == InteractionResult.SUCCESS); + attempt.addProperty("sourceConsumed", source.getItem().isEmpty()); + attempt.addProperty("damageIsOne", player.getMainHandItem().getDamageValue() == 1); + attempt.add("after", hammerSnapshot(world, player, pos, area, source)); + System.out.println("ADVHAMMER AFTER " + attempt); + require(action == InteractionResult.SUCCESS && source.getItem().isEmpty() && player.getMainHandItem().getDamageValue() == 1, "Live hammer failed " + recipe); int produced = 0; for (ItemEntity entity : world.getEntitiesOfClass(ItemEntity.class, area)) { @@ -82,61 +107,132 @@ static void hammer(ServerWorld world, ServerPlayerEntity 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()); + 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()); + } + } catch (ReflectiveOperationException | RuntimeException | LinkageError error) { + selection.addProperty("status", "OBSERVER_ERROR"); + selection.addProperty("errorType", error.getClass().getName()); + selection.addProperty("error", error.toString()); + if (error.getCause() != null) selection.addProperty("cause", error.getCause().toString()); + } + return selection; + } + + private static JsonObject hammerSnapshot(ServerLevel world, ServerPlayer player, BlockPos pos, + AABB area, ItemEntity source) { + JsonObject sample = new JsonObject(); + sample.addProperty("dimension", world.dimension().location().toString()); + sample.addProperty("gameTime", world.getGameTime()); + sample.addProperty("area", area.toString()); + sample.addProperty("sourceRemoved", source.isRemoved()); + sample.addProperty("sourceBounds", source.getBoundingBox().toString()); + sample.addProperty("sourceIntersectsArea", source.getBoundingBox().intersects(area)); + 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("clientSide", world.isClientSide); + sample.addProperty("creative", player.isCreative()); + sample.addProperty("shift", player.isShiftKeyDown()); + sample.addProperty("hammerClass", player.getMainHandItem().getItem().getClass().getName()); + sample.addProperty("hammerNbt", player.getMainHandItem().save(new net.minecraft.nbt.CompoundTag()).toString()); + sample.addProperty("hammerDamage", player.getMainHandItem().getDamageValue()); + sample.addProperty("hammerMaxDamage", player.getMainHandItem().getMaxDamage()); + JsonArray items = new JsonArray(); + boolean sourceInAabb = false; + for (ItemEntity entity : world.getEntitiesOfClass(ItemEntity.class, area)) { + sourceInAabb |= entity == source; + JsonObject item = new JsonObject(); + item.addProperty("uuid", entity.getUUID().toString()); + item.addProperty("isSource", entity == source); + item.addProperty("removed", entity.isRemoved()); + item.addProperty("bounds", entity.getBoundingBox().toString()); + item.addProperty("stackNbt", entity.getItem().save(new net.minecraft.nbt.CompoundTag()).toString()); + items.add(item); + } + sample.addProperty("sourceInAabb", sourceInAabb); + sample.add("items", items); + return sample; + } + static boolean canCrush(Map known, ItemStack input) { if (input.getItem() instanceof BlockItem) { for (String id : known.keySet()) { if (id.startsWith("basemetals:") && id.endsWith("_crackhammer") && stack(id).getItem().isCorrectToolForDrops(((BlockItem) input.getItem()).getBlock().defaultBlockState())) return true; } - return (has(known, "steamadvantage:steam_crusher", "steamadvantage:steam_boiler_coal", "minecraft:water_bucket")) - || has(known, "electricadvantage:electric_crusher", "electricadvantage:hydroelectric_generator", "minecraft:water_bucket"); + return (has(known, "basesteam:steam_crusher", "basesteam:steam_boiler_coal", "minecraft:water_bucket")) + || has(known, "baseelectric:electric_crusher", "baseelectric:hydroelectric_generator", "minecraft:water_bucket"); } return true; } - static boolean advance(ServerWorld world, ServerPlayerEntity player, Map known, JsonObject fixture) { + static boolean advance(ServerLevel world, ServerPlayer player, Map known, JsonObject fixture) { boolean changed = false; - for (String machine : new String[] {"steamadvantage:steam_crusher", "electricadvantage:electric_crusher"}) { + for (String machine : new String[] {"basesteam:steam_crusher", "baseelectric:electric_crusher"}) { if (known.containsKey(machine) && crusherWitnesses.add(machine)) { for (String potato : new String[] {"minecraft:potato", "minecraft:poisonous_potato"}) crushPotato(world, fixture, machine, potato); } } - if (known.containsKey("electricadvantage:sulfur_ore") && !known.containsKey("electricadvantage:sulfur_powder")) { + if (known.containsKey("baseelectric:sulfur_ore") && !known.containsKey("baseelectric:sulfur_powder")) { BlockPos pos = new BlockPos(16392, 80, 16384); - fresh(world, fixture, pos, "electricadvantage:sulfur_ore"); - java.util.List drops = net.minecraft.block.Block.getDrops( + fresh(world, fixture, pos, "baseelectric:sulfur_ore"); + java.util.List drops = net.minecraft.world.level.block.Block.getDrops( world.getBlockState(pos), world, pos, world.getBlockEntity(pos)); - require(drops.size() == 1 && id(drops.get(0)).equals("electricadvantage:sulfur_powder") + require(drops.size() == 1 && id(drops.get(0)).equals("baseelectric:sulfur_powder") && drops.get(0).getCount() >= 1 && drops.get(0).getCount() <= 4, "Sulfur ore drops changed"); - changed |= acquire(known, player, drops.get(0), "mine_generated_sulfur_ore", Collections.singletonList("electricadvantage:sulfur_ore")); + changed |= acquire(known, player, drops.get(0), "mine_generated_sulfur_ore", Collections.singletonList("baseelectric:sulfur_ore")); } if (known.containsKey("minecraft:bucket")) { - for (String fluid : new String[] {"minecraft:water", "poweradvantage:crude_oil"}) { + for (String fluid : new String[] {"minecraft:water", "basepower:crude_oil"}) { String bucket = fluid.equals("minecraft:water") ? "minecraft:water_bucket" : fluid + "_bucket"; if (known.containsKey(bucket)) continue; BlockPos pos = new BlockPos(16390, 80, 16384); fresh(world, fixture, pos, fluid); - player.inventory.clearContent(); + player.getInventory().clearContent(); player.absMoveTo(pos.getX() + 0.5D, pos.getY() + 2, pos.getZ() + 0.5D, 0, 90); - player.setItemInHand(Hand.MAIN_HAND, stack("minecraft:bucket")); - ItemStack output = player.getMainHandItem().getItem().use(world, player, Hand.MAIN_HAND).getObject(); + player.setItemInHand(InteractionHand.MAIN_HAND, stack("minecraft:bucket")); + ItemStack output = player.getMainHandItem().getItem().use(world, player, InteractionHand.MAIN_HAND).getObject(); require(!output.isEmpty() && id(output).equals(bucket), "Native world-fluid pickup changed identity: " + fluid); require(world.getFluidState(pos).isEmpty(), "Bucket pickup left its source behind"); changed |= acquire(known, player, output, "bucket_collect:" + fluid, Collections.singletonList("minecraft:bucket")); record("bucket_collect:" + fluid, 1000, 1); } } - if (!known.containsKey("poweradvantage:refined_oil_bucket") && has(known, - "poweradvantage:still", "poweradvantage:crude_oil_bucket", "minecraft:bucket", "minecraft:coal")) { - StillTileEntity still = (StillTileEntity) fresh(world, fixture, new BlockPos(16386, 80, 16384), "poweradvantage:still"); - ((IInventory) still).setItem(0, stack("minecraft:coal")); + if (!known.containsKey("basepower:refined_oil_bucket") && has(known, + "basepower:still", "basepower:crude_oil_bucket", "minecraft:bucket", "minecraft:coal")) { + StillTileEntity still = (StillTileEntity) fresh(world, fixture, new BlockPos(16386, 80, 16384), "basepower:still"); + ((Container) still).setItem(0, stack("minecraft:coal")); still.powerUpdate(); for (int batch = 0; batch < 2; batch++) { require(still.getInputTank().fill(new FluidStack(Fluids.crude_oil, 1000), true) == 1000, "Still rejected crude oil"); @@ -144,22 +240,22 @@ static boolean advance(ServerWorld world, ServerPlayerEntity player, Map machine.getEnergy(power), "Powered crusher did not consume energy"); @@ -215,23 +311,20 @@ private static boolean has(Map known, String... ids) { return true; } - private static TileEntity fresh(ServerWorld world, JsonObject fixture, BlockPos pos, String block) { + private static BlockEntity fresh(ServerLevel world, JsonObject fixture, BlockPos pos, String block) { JsonObject bounds = fixture.getAsJsonObject("machineBounds"); int[] p = {pos.getX(), pos.getY(), pos.getZ()}; for (int i = 0; i < 3; i++) require(p[i] >= bounds.getAsJsonArray("min").get(i).getAsInt() && p[i] <= bounds.getAsJsonArray("max").get(i).getAsInt(), "Machine fixture escaped its bounds"); - world.setBlockAndUpdate(pos, net.minecraft.block.Blocks.AIR.defaultBlockState()); + world.setBlockAndUpdate(pos, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState()); world.removeBlockEntity(pos); - net.minecraft.block.Block material = ForgeRegistries.BLOCKS.getValue(new ResourceLocation(block)); + net.minecraft.world.level.block.Block material = ForgeRegistries.BLOCKS.getValue(new ResourceLocation(block)); require(material != null, "Missing machine/fluid " + block); world.setBlockAndUpdate(pos, material.defaultBlockState()); - // Commands run within a server tick; queued tile replacements can still expose - // the previous witness. Give each bounded fixture a clean native tile instance. - TileEntity clean = material.createTileEntity(material.defaultBlockState(), world); - if (clean != null) { - clean.setLevelAndPosition(world, pos); - world.setBlockEntity(pos, clean); - } + BlockEntity clean = world.getBlockEntity(pos); + require(!material.defaultBlockState().hasBlockEntity() || clean != null, "Native factory omitted the machine"); + if (clean != null) require(clean.getBlockPos().equals(pos) + && clean.getBlockState().equals(world.getBlockState(pos)), "Native factory returned stale placement context"); return clean; } 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 d3d1df7d..4676cfdf 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 @@ -22,32 +22,32 @@ import java.util.UUID; import net.minecraft.advancements.Advancement; import net.minecraft.advancements.CriteriaTriggers; -import net.minecraft.command.Commands; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.inventory.container.Container; -import net.minecraft.inventory.Inventory; -import net.minecraft.inventory.CraftingInventory; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.item.crafting.IRecipe; -import net.minecraft.item.crafting.Ingredient; -import net.minecraft.network.PacketDirection; -import net.minecraft.network.play.ServerPlayNetHandler; -import net.minecraft.network.NetworkManager; -import net.minecraft.network.IPacket; +import net.minecraft.commands.Commands; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.inventory.CraftingContainer; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.Packet; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.management.PlayerInteractionManager; +import net.minecraft.server.level.ServerPlayerGameMode; import net.minecraft.tags.ItemTags; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.server.ServerWorld; -import net.minecraft.loot.LootContext; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.storage.loot.LootContext; import net.minecraftforge.common.MinecraftForge; -import net.minecraft.item.crafting.IRecipeType; +import net.minecraft.world.item.crafting.RecipeType; import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.registries.ForgeRegistries; -import zone.moddev.mc.poweradvantage.PowerAdvantage; -import zone.moddev.mc.poweradvantage.RecipeMode; +import zone.moddev.mc.basepower.PowerAdvantage; +import zone.moddev.mc.basepower.RecipeMode; /** Manufacture from declared natural resources; no pre-supplied Advantage parts. */ final class ProgressionProbe { @@ -73,22 +73,22 @@ private void starting(RegisterCommandsEvent event) { }))); } - private void audit(MinecraftServer server, ServerWorld world, String label) throws Exception { + private void audit(MinecraftServer server, ServerLevel world, String label) throws Exception { require(MARKER.isFile() && world.getServer().getWorldData().getLevelName().equals("AdvantageWorksMigration") && label.matches("[A-Za-z0-9_-]+"), "Unmarked world or unsafe label"); JsonObject fixture = JSON.fromJson(new String(Files.readAllBytes(new File("progression-fixture.json").toPath()), StandardCharsets.UTF_8), JsonObject.class); - ServerPlayerEntity player = new ServerPlayerEntity(server, world, new GameProfile( - UUID.fromString("3ea512a5-4c41-42ac-901f-7fabd2b1cbde"), "ProgressionProbe"), new PlayerInteractionManager(world)); - player.connection = new ServerPlayNetHandler(server, new NetworkManager(PacketDirection.SERVERBOUND), player) { - @Override public void send(IPacket packet) {} + ServerPlayer player = new ServerPlayer(server, world, new GameProfile( + UUID.fromString("3ea512a5-4c41-42ac-901f-7fabd2b1cbde"), "ProgressionProbe")); + player.connection = new ServerGamePacketListenerImpl(server, new Connection(PacketFlow.SERVERBOUND), player) { + @Override public void send(Packet packet) {} }; for (Advancement advancement : server.getAdvancements().getAllAdvancements()) { if (owned(advancement.getId())) for (String criterion : advancement.getCriteria().keySet()) player.getAdvancements().revoke(advancement, criterion); } - List> oldBook = new ArrayList<>(); - for (IRecipe recipe : server.getRecipeManager().getRecipes()) if (owned(recipe.getId())) oldBook.add(recipe); + List> oldBook = new ArrayList<>(); + for (Recipe recipe : server.getRecipeManager().getRecipes()) if (owned(recipe.getId())) oldBook.add(recipe); player.resetRecipes(oldBook); ProcessProgression.reset(); Map known = new LinkedHashMap<>(); @@ -115,18 +115,18 @@ private void audit(MinecraftServer server, ServerWorld world, String label) thro } } } - List> recipes = new ArrayList<>(server.getRecipeManager().getRecipes()); + List> recipes = new ArrayList<>(server.getRecipeManager().getRecipes()); recipes.sort(Comparator.comparing(recipe -> recipe.getId().toString())); boolean changed = true; int rounds = 0; while (changed) { require(++rounds <= 100, "Progression closure did not converge"); changed = false; - for (IRecipe recipe : recipes) { + for (Recipe recipe : recipes) { if (executed.contains(recipe.getId().toString()) || recipe.getIngredients().isEmpty()) continue; - String type = net.minecraft.util.registry.Registry.RECIPE_TYPE.getKey(recipe.getType()).toString(); - boolean crafting = recipe.getType() == IRecipeType.CRAFTING; - boolean smelting = recipe.getType() == IRecipeType.SMELTING; + String type = net.minecraft.core.Registry.RECIPE_TYPE.getKey(recipe.getType()).toString(); + boolean crafting = recipe.getType() == RecipeType.CRAFTING; + boolean smelting = recipe.getType() == RecipeType.SMELTING; boolean crushing = type.equals("basemetals:crushing"); if (!crafting && !smelting && !crushing) continue; if (smelting && (!known.containsKey("minecraft:furnace") || !known.containsKey("minecraft:coal"))) continue; @@ -146,17 +146,17 @@ private void audit(MinecraftServer server, ServerWorld world, String label) thro if (crushing && !ProcessProgression.canCrush(known, ingredients.get(0))) continue; ItemStack result = ItemStack.EMPTY; if (crafting) { - Container container = new Container(null, 0) { - @Override public boolean stillValid(net.minecraft.entity.player.PlayerEntity p) { return true; } + AbstractContainerMenu container = new AbstractContainerMenu(null, 0) { + @Override public boolean stillValid(net.minecraft.world.entity.player.Player p) { return true; } }; for (int width = 1; width <= 3 && result.isEmpty(); width++) { if (ingredients.size() > width * 3) continue; - CraftingInventory grid = new CraftingInventory(container, 3, 3); + CraftingContainer grid = new CraftingContainer(container, 3, 3); for (int i = 0; i < ingredients.size(); i++) grid.setItem((i / width) * 3 + i % width, ingredients.get(i)); result = matchedResult(recipe, grid, world); } } else { - Inventory grid = new Inventory(1); + SimpleContainer grid = new SimpleContainer(1); grid.setItem(0, ingredients.get(0)); result = matchedResult(recipe, grid, world); } @@ -169,7 +169,7 @@ private void audit(MinecraftServer server, ServerWorld world, String label) thro } if (owned(recipe.getId()) && smelting) ProcessProgression.smelt(world, fixture, recipe.getId().toString(), ingredients.get(0), result); - if (owned(recipe.getId()) && crushing && !(ingredients.get(0).getItem() instanceof net.minecraft.item.BlockItem)) + if (owned(recipe.getId()) && crushing && !(ingredients.get(0).getItem() instanceof net.minecraft.world.item.BlockItem)) ProcessProgression.hammer(world, player, known, fixture, recipe.getId().toString(), ingredients.get(0), result); executed.add(recipe.getId().toString()); List inputs = new ArrayList<>(); @@ -189,7 +189,7 @@ private void audit(MinecraftServer server, ServerWorld world, String label) thro if (exclusions.has(id)) { row.addProperty("status", "INTENTIONAL_EXCLUSION"); row.addProperty("reason", exclusions.get(id).getAsString()); - } else if (PowerAdvantage.recipeMode == RecipeMode.APOCALYPTIC && id.equals("electricadvantage:photovoltaic_generator")) { + } else if (PowerAdvantage.recipeMode == RecipeMode.APOCALYPTIC && id.equals("baseelectric:photovoltaic_generator")) { row.addProperty("status", "INTENTIONAL_MODE_RESTRICTION"); row.addProperty("reason", "Legacy Apocalyptic mode forbids new photovoltaic generators; use hydro or steam generation"); } else if (known.containsKey(id)) { @@ -200,9 +200,9 @@ private void audit(MinecraftServer server, ServerWorld world, String label) thro } JsonObject report = new JsonObject(); JsonArray missingRecipes = new JsonArray(); - for (IRecipe recipe : recipes) { - if (!owned(recipe.getId()) || (recipe.getType() != IRecipeType.CRAFTING - && recipe.getType() != IRecipeType.SMELTING)) continue; + for (Recipe recipe : recipes) { + if (!owned(recipe.getId()) || (recipe.getType() != RecipeType.CRAFTING + && recipe.getType() != RecipeType.SMELTING)) continue; if (!executed.contains(recipe.getId().toString())) missingRecipes.add(recipe.getId().toString()); } if (missingRecipes.size() > 0) blocked += missingRecipes.size(); @@ -230,40 +230,46 @@ private void audit(MinecraftServer server, ServerWorld world, String label) thro Files.write(new File("progression-" + label + ".json").toPath(), JSON.toJson(report).getBytes(StandardCharsets.UTF_8)); } - static boolean acquire(Map known, ServerPlayerEntity player, ItemStack output, String route, List inputs) { + static boolean acquire(Map known, ServerPlayer player, ItemStack output, String route, List inputs) { if (output == null || output.isEmpty() || known.containsKey(id(output))) return false; JsonObject node = new JsonObject(); node.addProperty("via", route); node.addProperty("outputCount", output.getCount()); - node.add("inputs", JSON.toJsonTree(inputs)); + node.add("inputs", inputJson(inputs)); known.put(id(output), node); - player.inventory.clearContent(); - player.inventory.setItem(0, output.copy()); - CriteriaTriggers.INVENTORY_CHANGED.trigger(player, player.inventory, player.inventory.getItem(0)); + player.getInventory().clearContent(); + player.getInventory().setItem(0, output.copy()); + CriteriaTriggers.INVENTORY_CHANGED.trigger(player, player.getInventory(), player.getInventory().getItem(0)); return true; } + static JsonArray inputJson(List inputs) { + JsonArray result = new JsonArray(); + for (String input : inputs) result.add(input); + return result; + } + static ItemStack stack(String id) { Item item = ForgeRegistries.ITEMS.getValue(new ResourceLocation(id)); return item == null ? ItemStack.EMPTY : new ItemStack(item); } - private static LootContext chestContext(ServerWorld world, int seed) { + private static LootContext chestContext(ServerLevel world, int seed) { return new LootContext.Builder(world).withRandom(new Random(seed)) - .withParameter(net.minecraft.loot.LootParameters.ORIGIN, new net.minecraft.util.math.vector.Vector3d(0, 80, 0)) - .create(net.minecraft.loot.LootParameterSets.CHEST); + .withParameter(net.minecraft.world.level.storage.loot.parameters.LootContextParams.ORIGIN, new net.minecraft.world.phys.Vec3(0, 80, 0)) + .create(net.minecraft.world.level.storage.loot.parameters.LootContextParamSets.CHEST); } @SuppressWarnings("unchecked") - private static ItemStack matchedResult(IRecipe recipe, net.minecraft.inventory.IInventory inventory, ServerWorld world) { - IRecipe typed = (IRecipe) recipe; + private static ItemStack matchedResult(Recipe recipe, net.minecraft.world.Container inventory, ServerLevel world) { + Recipe typed = (Recipe) recipe; return typed.matches(inventory, world) ? typed.assemble(inventory) : ItemStack.EMPTY; } private static boolean advantage(ItemStack stack) { return !stack.isEmpty() && owned(stack.getItem().getRegistryName()); } private static boolean owned(ResourceLocation id) { - return id != null && (id.getNamespace().equals("poweradvantage") || id.getNamespace().equals("steamadvantage") - || id.getNamespace().equals("electricadvantage")); + return id != null && (id.getNamespace().equals("basepower") || id.getNamespace().equals("basesteam") + || id.getNamespace().equals("baseelectric")); } static String id(ItemStack stack) { return stack.getItem().getRegistryName().toString(); } static void require(boolean value, String reason) { if (!value) throw new IllegalStateException(reason); } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeBookProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeBookProbe.java index a4e175c6..39dfb81e 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeBookProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeBookProbe.java @@ -7,60 +7,60 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; -import net.minecraft.inventory.container.Container; -import net.minecraft.inventory.CraftingInventory; -import net.minecraft.item.ItemStack; -import net.minecraft.item.crafting.IRecipe; -import net.minecraft.network.PacketBuffer; -import net.minecraft.network.play.server.SUpdateRecipesPacket; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.CraftingContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.game.ClientboundUpdateRecipesPacket; import net.minecraft.server.MinecraftServer; -import net.minecraft.util.ResourceLocation; -import net.minecraft.world.server.ServerWorld; -import net.minecraft.item.crafting.IRecipeType; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.crafting.RecipeType; import static zone.moddev.mc.advantageworks.ProgressionProbe.*; /** Native crafting and server-to-client recipe payload, not a source-JSON simulation. */ final class RecipeBookProbe { - static void verify(MinecraftServer server, ServerWorld world, String label, JsonObject report) throws Exception { + static void verify(MinecraftServer server, ServerLevel world, String label, JsonObject report) throws Exception { JsonArray entries = new JsonArray(); - List> recipes = new ArrayList<>(); - for (IRecipe recipe : server.getRecipeManager().getRecipes()) { + List> recipes = new ArrayList<>(); + for (Recipe recipe : server.getRecipeManager().getRecipes()) { String namespace = recipe.getId().getNamespace(); - if (!(namespace.equals("poweradvantage") || namespace.equals("steamadvantage") || namespace.equals("electricadvantage"))) continue; - if (recipe.getType() != IRecipeType.CRAFTING && recipe.getType() != IRecipeType.SMELTING) continue; + if (!(namespace.equals("basepower") || namespace.equals("basesteam") || namespace.equals("baseelectric"))) continue; + if (recipe.getType() != RecipeType.CRAFTING && recipe.getType() != RecipeType.SMELTING) continue; String output = id(recipe.getResultItem()); require(output.equals(recipe.getGroup()), "Mixed-output recipe group: " + recipe.getId() + " -> " + recipe.getGroup()); JsonObject entry = new JsonObject(); entry.addProperty("id", recipe.getId().toString()); entry.addProperty("group", recipe.getGroup()); - entry.addProperty("type", net.minecraft.util.registry.Registry.RECIPE_TYPE.getKey(recipe.getType()).toString()); + entry.addProperty("type", net.minecraft.core.Registry.RECIPE_TYPE.getKey(recipe.getType()).toString()); entry.addProperty("output", output); entry.addProperty("count", recipe.getResultItem().getCount()); entries.add(entry); recipes.add(recipe); } report.add("bookRecipes", entries); - PacketBuffer buffer = new PacketBuffer(Unpooled.buffer()); + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); try { - new SUpdateRecipesPacket(recipes).write(buffer); + new ClientboundUpdateRecipesPacket(recipes).write(buffer); byte[] bytes = new byte[buffer.readableBytes()]; buffer.readBytes(bytes); Files.write(Paths.get("recipes-" + label + ".bin"), bytes); } finally { buffer.release(); } - IRecipe starch = server.getRecipeManager().byKey(new ResourceLocation("poweradvantage:starch_from_potato")).orElse(null); - require(starch != null && starch.getType() == IRecipeType.CRAFTING, "Missing native potato crafting recipe"); + Recipe starch = server.getRecipeManager().byKey(new ResourceLocation("basepower:starch_from_potato")).orElse(null); + require(starch != null && starch.getType() == RecipeType.CRAFTING, "Missing native potato crafting recipe"); int cases = 0; for (int width : new int[] {2, 3}) for (String potato : new String[] {"minecraft:potato", "minecraft:poisonous_potato"}) { for (int slot = 0; slot < width * width; slot++) for (int count : new int[] {1, 64}) { - CraftingInventory grid = grid(width); + CraftingContainer grid = grid(width); ItemStack input = stack(potato); input.setCount(count); grid.setItem(slot, input); require(starch.matches(grid, world), "Potato recipe rejected slot/stack " + slot + "/" + count); - IRecipe selected = world.getRecipeManager().getRecipeFor(IRecipeType.CRAFTING, grid, world).orElse(null); + Recipe selected = world.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, grid, world).orElse(null); require(selected != null && selected.getId().equals(starch.getId()), "Conflicting native potato crafting recipe"); ItemStack result = starch.assemble(grid); - require(id(result).equals("poweradvantage:starch") && result.getCount() == 1, "Starch output changed"); + require(id(result).equals("basepower:starch") && result.getCount() == 1, "Starch output changed"); require(grid.getItem(slot).getCount() == count, "Result preview mutated input"); ItemStack consumed = grid.removeItem(slot, 1); require(consumed.getCount() == 1 && grid.getItem(slot).getCount() == count - 1, @@ -69,13 +69,13 @@ static void verify(MinecraftServer server, ServerWorld world, String label, Json } } for (String item : new String[] {null, "minecraft:baked_potato", "minecraft:carrot", "minecraft:dirt"}) { - CraftingInventory grid = grid(3); + CraftingContainer grid = grid(3); if (item != null) grid.setItem(0, stack(item)); require(!starch.matches(grid, world), "Invalid starch input accepted: " + item); cases++; } for (String second : new String[] {"minecraft:potato", "minecraft:poisonous_potato", "minecraft:dirt"}) { - CraftingInventory grid = grid(3); + CraftingContainer grid = grid(3); grid.setItem(0, stack("minecraft:potato")); grid.setItem(1, stack(second)); require(!starch.matches(grid, world), "Extra occupied ingredient slot accepted"); @@ -84,9 +84,9 @@ static void verify(MinecraftServer server, ServerWorld world, String label, Json report.addProperty("potatoCraftingCases", cases); } - private static CraftingInventory grid(int width) { - return new CraftingInventory(new Container(null, 0) { - @Override public boolean stillValid(net.minecraft.entity.player.PlayerEntity player) { return true; } + private static CraftingContainer grid(int width) { + return new CraftingContainer(new AbstractContainerMenu(null, 0) { + @Override public boolean stillValid(net.minecraft.world.entity.player.Player player) { return true; } }, width, width); } } 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 7991815c..fb6029ba 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 @@ -18,21 +18,21 @@ import java.util.UUID; import net.minecraft.advancements.Advancement; import net.minecraft.advancements.CriteriaTriggers; -import net.minecraft.advancements.criterion.InventoryChangeTrigger; -import net.minecraft.command.Commands; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.item.crafting.IRecipe; -import net.minecraft.network.PacketDirection; -import net.minecraft.network.play.ServerPlayNetHandler; -import net.minecraft.network.NetworkManager; -import net.minecraft.network.IPacket; +import net.minecraft.advancements.critereon.InventoryChangeTrigger; +import net.minecraft.commands.Commands; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.Packet; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.management.PlayerInteractionManager; +import net.minecraft.server.level.ServerPlayerGameMode; import net.minecraft.tags.ItemTags; -import net.minecraft.util.ResourceLocation; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.entity.player.PlayerEvent; import net.minecraftforge.event.RegisterCommandsEvent; @@ -42,10 +42,26 @@ final class RecipeUnlockProbe { private static final Gson JSON = new GsonBuilder().setPrettyPrinting().create(); private static final File MARKER = new File("advantage-recipes.allow"); - private ServerPlayerEntity player; + private ServerPlayer player; + private Connection transport; static void register() { - if (MARKER.isFile()) MinecraftForge.EVENT_BUS.addListener(new RecipeUnlockProbe()::starting); + if (MARKER.isFile()) { + RecipeUnlockProbe probe = new RecipeUnlockProbe(); + MinecraftForge.EVENT_BUS.addListener(probe::starting); + MinecraftForge.EVENT_BUS.addListener(probe::stopped); + } + } + + static Connection nativeTransport() { + Connection connection = new Connection(PacketFlow.SERVERBOUND); + io.netty.channel.embedded.EmbeddedChannel channel = new io.netty.channel.embedded.EmbeddedChannel(connection); + require(connection.channel() == channel && channel.isActive(), "Native test channel did not activate"); + return connection; + } + + private void stopped(net.minecraftforge.fmlserverevents.FMLServerStoppedEvent event) { + if (transport != null) ((io.netty.channel.embedded.EmbeddedChannel) transport.channel()).finishAndReleaseAll(); } private void starting(RegisterCommandsEvent event) { @@ -67,18 +83,18 @@ private void starting(RegisterCommandsEvent event) { })))); } - private void run(MinecraftServer server, ServerWorld world, String operation, String label) throws Exception { + private void run(MinecraftServer server, ServerLevel world, String operation, String label) throws Exception { if (!MARKER.isFile() || !world.getServer().getWorldData().getLevelName().equals("AdvantageWorksMigration") || !label.matches("[a-zA-Z0-9_-]+")) throw new IllegalStateException("Unmarked world or unsafe label"); JsonObject fixture = JSON.fromJson(new String(Files.readAllBytes(new File("recipe-fixture.json").toPath()), StandardCharsets.UTF_8), JsonObject.class); if (player == null) { - player = new ServerPlayerEntity(server, world, new GameProfile( - UUID.fromString("b9bcb1ba-e8e3-44ec-a729-ef70587dc453"), "RecipeProbe"), - new PlayerInteractionManager(world)); + player = new ServerPlayer(server, world, new GameProfile( + UUID.fromString("b9bcb1ba-e8e3-44ec-a729-ef70587dc453"), "RecipeProbe")); // Real server player and recipe book, with a sink instead of a remote client. - player.connection = new ServerPlayNetHandler(server, new NetworkManager(PacketDirection.SERVERBOUND), player) { - @Override public void send(IPacket packet) {} + transport = nativeTransport(); + player.connection = new ServerGamePacketListenerImpl(server, transport, player) { + @Override public void send(Packet packet) {} }; } JsonArray cases = fixture.getAsJsonArray("recipes"); @@ -88,7 +104,7 @@ private void run(MinecraftServer server, ServerWorld world, String operation, St RecipeBookProbe.verify(server, world, label, result); for (JsonElement element : cases) { String id = element.getAsJsonObject().get("id").getAsString(); - require(id.matches("(power|steam|electric)advantage:[a-z0-9_]+"), "Unsafe recipe ID"); + require(id.matches("base(power|steam|electric):[a-z0-9_]+"), "Unsafe recipe ID: " + id); require(advancement(server, id) != null, "Missing loaded advancement: " + id); } if (operation.equals("sweep")) { @@ -96,32 +112,32 @@ private void run(MinecraftServer server, ServerWorld world, String operation, St JsonObject definition = element.getAsJsonObject(); String id = definition.get("id").getAsString(); Advancement advancement = advancement(server, id); - IRecipe recipe = server.getRecipeManager().byKey(new ResourceLocation(id)).orElse(null); + Recipe recipe = server.getRecipeManager().byKey(new ResourceLocation(id)).orElse(null); JsonObject row = new JsonObject(); row.addProperty("id", id); row.addProperty("recipeActive", recipe != null); int positives = 0, members = 0, emptyTags = 0; reset(advancement, recipe); - player.inventory.setItem(0, new ItemStack(item("minecraft:barrier"))); - CriteriaTriggers.INVENTORY_CHANGED.trigger(player, player.inventory, player.inventory.getItem(0)); + player.getInventory().setItem(0, new ItemStack(item("minecraft:barrier"))); + CriteriaTriggers.INVENTORY_CHANGED.trigger(player, player.getInventory(), player.getInventory().getItem(0)); require(!player.getAdvancements().getOrStartProgress(advancement).isDone(), "Unrelated item discovered " + id); for (Map.Entry criterion : definition.getAsJsonObject("ingredients").entrySet()) { - InventoryChangeTrigger.Instance condition = (InventoryChangeTrigger.Instance) + InventoryChangeTrigger.TriggerInstance condition = (InventoryChangeTrigger.TriggerInstance) advancement.getCriteria().get(criterion.getKey()).getTrigger(); - player.inventory.clearContent(); - require(!matchesInventory(condition, player.inventory), "Empty inventory matches " + id); - player.inventory.setItem(0, new ItemStack(item("minecraft:barrier"))); - require(!matchesInventory(condition, player.inventory), "Unrelated item matches " + id); + player.getInventory().clearContent(); + require(!matchesInventory(condition, player.getInventory()), "Empty inventory matches " + id); + player.getInventory().setItem(0, new ItemStack(item("minecraft:barrier"))); + require(!matchesInventory(condition, player.getInventory()), "Unrelated item matches " + id); List choices = choices(criterion.getValue().getAsJsonObject()); if (choices.isEmpty()) { emptyTags++; continue; } for (Item candidate : choices) { - player.inventory.setItem(0, new ItemStack(candidate)); - require(matchesInventory(condition, player.inventory), "Tag member rejected: " + id + " / " + candidate.getRegistryName()); + player.getInventory().setItem(0, new ItemStack(candidate)); + require(matchesInventory(condition, player.getInventory()), "Tag member rejected: " + id + " / " + candidate.getRegistryName()); members++; } reset(advancement, recipe); - player.inventory.setItem(0, new ItemStack(choices.get(0))); - CriteriaTriggers.INVENTORY_CHANGED.trigger(player, player.inventory, player.inventory.getItem(0)); + player.getInventory().setItem(0, new ItemStack(choices.get(0))); + CriteriaTriggers.INVENTORY_CHANGED.trigger(player, player.getInventory(), player.getInventory().getItem(0)); require(player.getAdvancements().getOrStartProgress(advancement).isDone(), "Inventory event did not discover " + id); require(recipe == null || player.getRecipeBook().contains(recipe), "Reward failed for " + id); positives++; @@ -166,17 +182,21 @@ private void run(MinecraftServer server, ServerWorld world, String operation, St } } } - player.inventory.clearContent(); + player.getInventory().clearContent(); player.getAdvancements().save(); - Native116PlayerIo.save(server.getPlayerList(), player); + Native117PlayerIo.save(server.getPlayerList(), player); result.add("materials", kit); result.add("additionalMaterials", additions); verifyDiscovered(server, cases, rows); } 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, + "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)); - require(player.inventory.isEmpty(), "Persistence test must not reacquire ingredients"); + result.addProperty("nativeForgeLoginEvent", true); + require(player.getInventory().isEmpty(), "Persistence test must not reacquire ingredients"); verifyDiscovered(server, cases, rows); } else throw new IllegalArgumentException("Unknown operation " + operation); result.addProperty("status", "PASS"); @@ -192,7 +212,7 @@ private void verifyDiscovered(MinecraftServer server, JsonArray definitions, Jso available |= !choices(criterion.getValue().getAsJsonObject()).isEmpty(); boolean done = player.getAdvancements().getOrStartProgress(advancement(server, id)).isDone(); require(!available || done, "Lost discovery: " + id); - IRecipe recipe = server.getRecipeManager().byKey(new ResourceLocation(id)).orElse(null); + Recipe recipe = server.getRecipeManager().byKey(new ResourceLocation(id)).orElse(null); require(recipe == null || !available || player.getRecipeBook().contains(recipe), "Lost recipe reward: " + id); JsonObject row = new JsonObject(); row.addProperty("id", id); @@ -204,13 +224,13 @@ private void verifyDiscovered(MinecraftServer server, JsonArray definitions, Jso } private void discover(Item item) { - player.inventory.clearContent(); - player.inventory.setItem(0, new ItemStack(item)); - CriteriaTriggers.INVENTORY_CHANGED.trigger(player, player.inventory, player.inventory.getItem(0)); + player.getInventory().clearContent(); + player.getInventory().setItem(0, new ItemStack(item)); + CriteriaTriggers.INVENTORY_CHANGED.trigger(player, player.getInventory(), player.getInventory().getItem(0)); } - private void reset(Advancement advancement, IRecipe recipe) { - player.inventory.clearContent(); + private void reset(Advancement advancement, Recipe recipe) { + player.getInventory().clearContent(); if (recipe != null) player.resetRecipes(Collections.singletonList(recipe)); for (String criterion : advancement.getCriteria().keySet()) player.getAdvancements().revoke(advancement, criterion); @@ -221,8 +241,8 @@ private static Advancement advancement(MinecraftServer server, String id) { return server.getAdvancements().getAdvancement(new ResourceLocation(name.getNamespace(), "recipes/" + name.getPath())); } - private static boolean matchesInventory(InventoryChangeTrigger.Instance condition, - net.minecraft.entity.player.PlayerInventory inventory) { + private static boolean matchesInventory(InventoryChangeTrigger.TriggerInstance condition, + net.minecraft.world.entity.player.Inventory inventory) { int full = 0, empty = 0, occupied = 0; for (int slot = 0; slot < inventory.getContainerSize(); slot++) { ItemStack stack = inventory.getItem(slot); 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 04dd6636..78a56311 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 @@ -15,22 +15,22 @@ import java.util.List; import java.util.Map; import java.util.UUID; -import net.minecraft.block.Block; -import net.minecraft.block.BlockState; -import net.minecraft.block.Blocks; -import net.minecraft.command.Commands; -import net.minecraft.inventory.IInventory; -import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.state.properties.BlockStateProperties; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.Direction; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.registry.Registry; -import net.minecraft.world.World; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.commands.Commands; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.world.level.Level; +import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.event.TickEvent; @@ -46,12 +46,12 @@ private enum Phase { FROZEN, PREPARING, SEEDING, RESTORING, OPERATING, DONE } private final String processToken = UUID.randomUUID().toString(); private final List witnesses = definitions(); - private final List suspended = new ArrayList<>(); + private final List suspended = new ArrayList<>(); private final List> seedTrace = new ArrayList<>(); private final List> ticks = new ArrayList<>(); private final List> readiness = new ArrayList<>(); private Phase phase = Phase.FROZEN; - private ServerWorld world; + private ServerLevel world; private String label; private boolean used; private int preparationIndex; @@ -84,7 +84,7 @@ private void commands(RegisterCommandsEvent event) { .then(Commands.argument("operation", StringArgumentType.word()) .then(Commands.argument("label", StringArgumentType.word()).executes(context -> { if (!MARKER.isFile() || used) throw new IllegalStateException("Missing marker or reused probe"); - ServerWorld current = context.getSource().getLevel(); + ServerLevel current = context.getSource().getLevel(); if (!current.getServer().getWorldData().getLevelName().equals("AdvantageWorksMigration")) throw new IllegalStateException("Not an authorized disposable world"); String requested = StringArgumentType.getString(context, "label"); @@ -113,19 +113,16 @@ private void commands(RegisterCommandsEvent event) { private boolean paused() { return phase != Phase.SEEDING && phase != Phase.OPERATING; } private void tick(TickEvent.WorldTickEvent event) { - if (!(event.world instanceof ServerWorld) || event.world.dimension() != World.OVERWORLD) return; - ServerWorld current = (ServerWorld) event.world; + if (!(event.world instanceof ServerLevel) || event.world.dimension() != Level.OVERWORLD) return; + ServerLevel current = (ServerLevel) event.world; if (event.phase == TickEvent.Phase.START) { tickStart = current.getGameTime(); if (paused()) { - suspended.addAll(current.tickableBlockEntities); - current.tickableBlockEntities.clear(); + suspended.addAll(NativeBlockEntityTicks.drain(current)); } return; } - for (TileEntity tile : suspended) - if (!tile.isRemoved() && !current.tickableBlockEntities.contains(tile)) current.tickableBlockEntities.add(tile); - suspended.clear(); + NativeBlockEntityTicks.restore(current, suspended); if (current != world || phase == Phase.DONE) return; try { if (phase == Phase.PREPARING) { @@ -140,7 +137,7 @@ private void tick(TickEvent.WorldTickEvent event) { seedTrace.add(observation); boolean ready = true; for (Witness witness : witnesses) { - CompoundNBT tag = tile(witness).save(new CompoundNBT()); + CompoundTag tag = tile(witness).save(new CompoundTag()); ready &= tag.getShort("progress") > 0 && tag.contains("targetX", 99) && ((PoweredEntity) tile(witness)).getEnergy(new ConduitType("electricity")) > 24; } @@ -186,16 +183,20 @@ private boolean ready(String stage) { boolean all = true; List> rows = new ArrayList<>(); for (Witness witness : witnesses) { - TileEntity tile = tile(witness); - boolean ticking = world.getChunkSource().isTickingChunk(witness.pos); - boolean listed = world.tickableBlockEntities.contains(tile) && !tile.isRemoved(); + 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)); Map row = new LinkedHashMap<>(); row.put("id", witness.id); row.put("pos", coordinates(witness.pos)); row.put("blockTicking", ticking); row.put("tickableListed", listed); + row.put("blockAndEntitiesLoaded", blockAndEntitiesLoaded); + row.put("entityTicking", entityTicking); rows.add(row); - all &= ticking && listed; + all &= nativeReady(ticking, listed, blockAndEntitiesLoaded, entityTicking); } Map sample = new LinkedHashMap<>(); sample.put("phase", stage); @@ -203,33 +204,38 @@ private boolean ready(String stage) { sample.put("cases", rows); readiness.add(sample); if (!all && readiness.size() >= READINESS_TICKS) - throw new AssertionError("Native block-ticking/list readiness timed out after " + READINESS_TICKS + " paused ticks"); + throw new AssertionError("Native block/list/entity readiness timed out after " + READINESS_TICKS + " paused ticks"); return all; } + static boolean nativeReady(boolean blockTicking, boolean tickableListed, + boolean blockAndEntitiesLoaded, boolean entityTicking) { + return blockTicking && tickableListed && blockAndEntitiesLoaded && entityTicking; + } + private void construct(Witness witness) { if (phase != Phase.PREPARING || FIXTURE.exists()) throw new IllegalStateException("Construction outside fresh setup"); force(witness); for (int dx = -1; dx <= 1; dx++) for (int dy = -1; dy <= 1; dy++) for (int dz = -1; dz <= 1; dz++) world.setBlock(witness.pos.offset(dx, dy, dz), Blocks.AIR.defaultBlockState(), 2); - Block block = Registry.BLOCK.get(new ResourceLocation("electricadvantage:electric_drill")); + Block block = Registry.BLOCK.get(new ResourceLocation("baseelectric:electric_drill")); if (block == Blocks.AIR) throw new IllegalStateException("Electric drill registry absent"); world.setBlock(witness.pos, block.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.NORTH), 2); for (int dx = -1; dx <= 1; dx++) for (int dy = -1; dy <= 1; dy++) world.setBlock(witness.pos.offset(dx, dy, -1), Blocks.OBSIDIAN.defaultBlockState(), 2); - TileEntity tile = tile(witness); + BlockEntity tile = tile(witness); ((PoweredEntity) tile).setEnergy(500, new ConduitType("electricity")); ItemStack dirt = new ItemStack(Items.DIRT, 7); dirt.getOrCreateTag().putString("AdvWorksWitness", witness.id); - ((IInventory) tile).setItem(0, dirt); - ((IInventory) tile).setItem(1, new ItemStack(Items.APPLE, 13)); + ((Container) tile).setItem(0, dirt); + ((Container) tile).setItem(1, new ItemStack(Items.APPLE, 13)); tile.setChanged(); constructionCalls++; } - private TileEntity tile(Witness witness) { - TileEntity tile = world.getBlockEntity(witness.pos); - if (!(tile instanceof PoweredEntity) || !(tile instanceof IInventory)) + private BlockEntity tile(Witness witness) { + BlockEntity tile = world.getBlockEntity(witness.pos); + if (!(tile instanceof PoweredEntity) || !(tile instanceof Container)) throw new IllegalStateException("Missing powered inventory at " + witness.pos); return tile; } @@ -266,8 +272,8 @@ private Map frame(int index) { private List> snapshots() { List> result = new ArrayList<>(); for (Witness witness : witnesses) { - TileEntity tile = tile(witness); - CompoundNBT nbt = tile.save(new CompoundNBT()); + BlockEntity tile = tile(witness); + CompoundTag nbt = tile.save(new CompoundTag()); BlockState state = world.getBlockState(witness.pos); Map row = new LinkedHashMap<>(); row.put("id", witness.id); @@ -297,7 +303,7 @@ private List> snapshots() { } private static boolean active(BlockState state) { - for (net.minecraft.state.Property property : state.getProperties()) + for (net.minecraft.world.level.block.state.properties.Property property : state.getProperties()) if (property.getName().equals("active")) return Boolean.TRUE.equals(state.getValue(property)); throw new IllegalStateException("Drill has no ACTIVE property"); } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/SourcePickupOperationalCases.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/SourcePickupOperationalCases.java index 98b68143..f0842aba 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/SourcePickupOperationalCases.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/SourcePickupOperationalCases.java @@ -1,8 +1,8 @@ package zone.moddev.mc.advantageworks; import java.util.List; -import net.minecraft.block.Blocks; -import net.minecraft.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; final class SourcePickupOperationalCases { static void register(List cases) { @@ -11,15 +11,15 @@ static void register(List cases) { "Native pickup removes exactly one bucket of water, preserving the waterlogged slab and retaining the collected fluid after restart", 400, c -> { if (machine.equals("power-drain")) { - c.block(8, 2, 8, "poweradvantage:fluid_drain"); - c.block(9, 2, 8, "poweradvantage:fluid_pipe"); - c.block(10, 2, 8, "poweradvantage:fluid_metal_tank"); + c.block(8, 2, 8, "basepower:fluid_drain"); + c.block(9, 2, 8, "basepower:fluid_pipe"); + c.block(10, 2, 8, "basepower:fluid_metal_tank"); for (int[] p : new int[][]{{7,3,8},{9,3,8},{8,3,7},{8,3,9},{8,4,8}}) c.block(p[0],p[1],p[2],"minecraft:bedrock"); slab(c, 8, 3, 8); } else { - c.block(8, 4, 8, machine.equals("steam-pump") ? "steamadvantage:steam_pump" : "electricadvantage:electric_pump"); - c.block(9, 4, 8, "poweradvantage:fluid_metal_tank"); + c.block(8, 4, 8, machine.equals("steam-pump") ? "basesteam:steam_pump" : "baseelectric:electric_pump"); + c.block(9, 4, 8, "basepower:fluid_metal_tank"); if (machine.equals("steam-pump")) c.steamSupply(7, 4, 8, 600); else c.electricSupply(7, 4, 8, 8000); for (int[] p : new int[][]{{8,0,8},{8,0,7},{8,0,9},{7,0,8},{9,0,7},{9,0,9},{10,0,8},{9,1,8}}) @@ -28,7 +28,7 @@ static void register(List cases) { } }, c -> { boolean drain = machine.equals("power-drain"); - net.minecraft.util.math.BlockPos source = drain ? c.at(8, 3, 8) : c.at(9, 0, 8); + net.minecraft.core.BlockPos source = drain ? c.at(8, 3, 8) : c.at(9, 0, 8); c.require(c.world.getBlockState(source).is(Blocks.OAK_SLAB), "Water pickup must never delete the host slab"); c.require(!c.world.getBlockState(source).getValue(BlockStateProperties.WATERLOGGED), "Host is genuinely drained, not an unchanged water source"); int stored = drain ? amount(c, 8, 2, 8) + amount(c, 10, 2, 8) diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamOperationalActors.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamOperationalActors.java index cc759e44..02731d05 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamOperationalActors.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamOperationalActors.java @@ -4,25 +4,25 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.UUID; -import net.minecraft.block.BlockState; -import net.minecraft.enchantment.Enchantment; -import net.minecraft.entity.EntityType; -import net.minecraft.entity.ai.attributes.Attributes; -import net.minecraft.entity.item.ArmorStandEntity; -import net.minecraft.entity.passive.CowEntity; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.network.IPacket; -import net.minecraft.network.NetworkManager; -import net.minecraft.network.PacketDirection; -import net.minecraft.network.play.ServerPlayNetHandler; -import net.minecraft.state.IntegerProperty; -import net.minecraft.util.ActionResultType; -import net.minecraft.util.Hand; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.GameType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.decoration.ArmorStand; +import net.minecraft.world.entity.animal.Cow; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.world.level.block.state.properties.IntegerProperty; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionHand; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.GameType; import net.minecraftforge.common.util.FakePlayer; import net.minecraftforge.registries.ForgeRegistries; @@ -37,22 +37,22 @@ static void register(List cases) { private static void elevator(List cases) { for (String mode : new String[]{"up", "down", "no-power", "scarce"}) { - final ArmorStandEntity[] passenger = new ArmorStandEntity[1]; + final ArmorStand[] passenger = new ArmorStand[1]; cases.add(new MachineScenario("steam/elevator-" + mode, "A real passenger moves to the physical stop, one move costs exactly 32 steam, " + "a steady signal never repeats the move, and insufficient steam leaves passenger/shaft unchanged.", 80, c -> { - c.block(8, 0, 8, "steamadvantage:steam_elevator"); + c.block(8, 0, 8, "basesteam:steam_elevator"); c.block(8, 9, 8, "minecraft:stone"); boolean down = mode.equals("down"); if (down) { for (int y = 1; y <= 6; y++) platform(c, y, y == 6 ? 4 : 0); - CompoundNBT patch = new CompoundNBT(); patch.putBoolean("up", true); + CompoundTag patch = new CompoundTag(); patch.putBoolean("up", true); c.seed(8, 0, 8, patch); } else c.block(9, 0, 8, "minecraft:redstone_block"); if (!mode.equals("no-power")) c.power(8, 0, 8, "steam", mode.equals("scarce") ? 31 : 64); BlockPos base = c.at(8, 0, 8); - ArmorStandEntity rider = new ArmorStandEntity(c.world, base.getX() + 0.5, + ArmorStand rider = new ArmorStand(c.world, base.getX() + 0.5, base.getY() + (down ? 7 : 1) + 0.00390625, base.getZ() + 0.5); rider.setNoGravity(true); rider.setInvulnerable(true); @@ -61,7 +61,7 @@ private static void elevator(List cases) { c.record("passenger/uuid", rider.getUUID().toString()); c.record("passenger/startY", rider.getY()); }, c -> { - ArmorStandEntity rider = passenger[0]; + ArmorStand rider = passenger[0]; c.require(rider != null && rider.isAlive(), "Passenger survives the movement test"); c.record("passenger/endY", rider.getY()); c.record("passenger/naturalTicks", rider.tickCount); @@ -78,14 +78,14 @@ private static void elevator(List cases) { BlockState state = c.world.getBlockState(c.at(8, y, 8)); c.record("shaft/" + y, state.toString()); if (movedUp && y <= 6) { - c.require("steamadvantage:platform".equals(String.valueOf(state.getBlock().getRegistryName())), + c.require("basesteam:platform".equals(String.valueOf(state.getBlock().getRegistryName())), "Raised shaft contains physical platform segment " + y); IntegerProperty height = (IntegerProperty) state.getBlock().getStateDefinition().getProperty("height"); c.require(height != null && state.getValue(height) == (y == 6 ? 4 : 0), "Raised shaft/top collision height is preserved at " + y); } else c.require(state.isAir(), "Shaft is clear at " + y + " after " + mode); } - c.require(c.world.getBlockState(c.at(8, 9, 8)).getBlock() == net.minecraft.block.Blocks.STONE, + c.require(c.world.getBlockState(c.at(8, 9, 8)).getBlock() == net.minecraft.world.level.block.Blocks.STONE, "Elevator preserves the real upper stop"); c.require(c.nbt(8, 0, 8).getBoolean("up") == movedUp, "Saved latch agrees with physical movement, not just an NBT flag"); @@ -94,7 +94,7 @@ private static void elevator(List cases) { } private static void platform(MachineContext c, int y, int height) { - c.block(8, y, 8, "steamadvantage:platform"); + c.block(8, y, 8, "basesteam:platform"); BlockState state = c.world.getBlockState(c.at(8, y, 8)); IntegerProperty property = (IntegerProperty) state.getBlock().getStateDefinition().getProperty("height"); if (property == null) throw new IllegalStateException("Missing native platform height"); @@ -103,7 +103,7 @@ private static void platform(MachineContext c, int y, int height) { private static final class Shot { FakePlayer player; - CowEntity target; + Cow target; ItemStack weapon; float expectedDamage; int expectedAmmo; @@ -122,23 +122,23 @@ private static void musket(List cases) { UUID id = UUID.nameUUIDFromBytes(("SteamOperational/" + mode + "/" + muzzle) .getBytes(StandardCharsets.UTF_8)); shot.player = new FakePlayer(c.world, new GameProfile(id, "SteamOperational")); - shot.player.connection = new ServerPlayNetHandler(c.world.getServer(), - new NetworkManager(PacketDirection.SERVERBOUND), shot.player) { - @Override public void send(IPacket packet) {} + shot.player.connection = new ServerGamePacketListenerImpl(c.world.getServer(), + new Connection(PacketFlow.SERVERBOUND), shot.player) { + @Override public void send(Packet packet) {} }; - shot.player.gameMode.setGameModeForPlayer(GameType.SURVIVAL); + shot.player.gameMode.changeGameModeForPlayer(GameType.SURVIVAL); shot.player.moveTo(muzzle.getX() + 0.5, muzzle.getY(), muzzle.getZ() + 0.5, 0, 0); - Item weapon = ForgeRegistries.ITEMS.getValue(new ResourceLocation("steamadvantage:musket")); - c.require(weapon != null && weapon != net.minecraft.item.Items.AIR, "Native musket is registered"); + Item weapon = ForgeRegistries.ITEMS.getValue(new ResourceLocation("basesteam:musket")); + c.require(weapon != null && weapon != net.minecraft.world.item.Items.AIR, "Native musket is registered"); shot.weapon = new ItemStack(weapon); - shot.player.setItemInHand(Hand.MAIN_HAND, shot.weapon); - if (!mode.equals("no-ammo")) enchant(c, shot.weapon, "steamadvantage:rapid_reload"); - if (mode.equals("powderless")) enchant(c, shot.weapon, "steamadvantage:powderless"); + shot.player.setItemInHand(InteractionHand.MAIN_HAND, shot.weapon); + if (!mode.equals("no-ammo")) enchant(c, shot.weapon, "basesteam:rapid_reload"); + if (mode.equals("powderless")) enchant(c, shot.weapon, "basesteam:powderless"); if (mode.equals("infinity")) enchant(c, shot.weapon, "minecraft:infinity"); if (mode.equals("last-cartridge") || mode.equals("infinity")) { - Item cartridge = ForgeRegistries.ITEMS.getValue(new ResourceLocation("steamadvantage:blackpowder_cartridge")); - c.require(cartridge != null && cartridge != net.minecraft.item.Items.AIR, "Cartridge is registered"); - shot.player.inventory.setItem(1, new ItemStack(cartridge)); + Item cartridge = ForgeRegistries.ITEMS.getValue(new ResourceLocation("basesteam:blackpowder_cartridge")); + c.require(cartridge != null && cartridge != net.minecraft.world.item.Items.AIR, "Cartridge is registered"); + shot.player.getInventory().setItem(1, new ItemStack(cartridge)); } shot.expectedAmmo = mode.equals("infinity") ? 1 : 0; shot.target = EntityType.COW.create(c.world); @@ -150,21 +150,21 @@ private static void musket(List cases) { c.require(c.world.addFreshEntity(shot.target), "Living target added to the real server world"); c.record("target/uuid", shot.target.getUUID().toString()); c.record("target/initialHealth", shot.target.getHealth()); - ActionResultType reload = weapon.use(c.world, shot.player, Hand.MAIN_HAND).getResult(); + InteractionResult reload = weapon.use(c.world, shot.player, InteractionHand.MAIN_HAND).getResult(); c.record("musket/reload-result", reload.toString()); if (mode.equals("no-ammo")) { - c.require(reload == ActionResultType.FAIL, "Unloaded musket rejects use without ammunition"); + c.require(reload == InteractionResult.FAIL, "Unloaded musket rejects use without ammunition"); shot.expectedDamage = 0; } else { - c.require(reload == ActionResultType.SUCCESS, "Rapid-reload interaction succeeds"); + c.require(reload == InteractionResult.SUCCESS, "Rapid-reload interaction succeeds"); c.require(shot.weapon.getOrCreateTag().getByte("loaded") == 1, "Reload interaction loads the chamber before the shot"); - c.record("musket/ammo-after-reload", shot.player.inventory.getItem(1).getCount()); - c.require(shot.player.inventory.getItem(1).getCount() == shot.expectedAmmo, + c.record("musket/ammo-after-reload", shot.player.getInventory().getItem(1).getCount()); + c.require(shot.player.getInventory().getItem(1).getCount() == shot.expectedAmmo, "Reload consumes exactly one cartridge unless an ammunition-saving enchantment applies"); - ActionResultType aim = weapon.use(c.world, shot.player, Hand.MAIN_HAND).getResult(); + InteractionResult aim = weapon.use(c.world, shot.player, InteractionHand.MAIN_HAND).getResult(); c.record("musket/aim-result", aim.toString()); - c.require(aim == ActionResultType.SUCCESS, "Loaded musket can aim even without a spare cartridge"); + c.require(aim == InteractionResult.SUCCESS, "Loaded musket can aim even without a spare cartridge"); c.require(shot.player.isUsingItem(), "Native player enters the use lifecycle"); shot.player.releaseUsingItem(); shot.expectedDamage = shotDamage(); @@ -173,14 +173,14 @@ private static void musket(List cases) { c.record("target/finalHealth", shot.target.getHealth()); c.record("target/naturalTicks", shot.target.tickCount); c.record("musket/durabilityDebit", shot.weapon.getDamageValue()); - c.record("musket/finalAmmo", shot.player.inventory.getItem(1).getCount()); + c.record("musket/finalAmmo", shot.player.getInventory().getItem(1).getCount()); c.require(shot.target.isAlive() && shot.target.tickCount >= 35, "Real target remains available after natural server ticks"); c.require(Math.abs(shot.target.getHealth() - (1000 - shot.expectedDamage)) < 0.001F, "Target damage is exactly the configured shot damage, or zero for rejected use"); c.require(shot.weapon.getDamageValue() == (mode.equals("no-ammo") ? 0 : 1), "Only a real shot debits one musket durability"); - c.require(shot.player.inventory.getItem(1).getCount() == shot.expectedAmmo, + c.require(shot.player.getInventory().getItem(1).getCount() == shot.expectedAmmo, "Firing does not charge ammunition a second time"); c.require(shot.weapon.getOrCreateTag().getByte("loaded") == 0, "The chamber is empty after firing or failed unladen use"); @@ -191,7 +191,7 @@ private static void musket(List cases) { private static float shotDamage() { try { - float damage = Class.forName("zone.moddev.mc.steamadvantage.SteamAdvantage", false, + float damage = Class.forName("zone.moddev.mc.basesteam.SteamAdvantage", false, SteamOperationalActors.class.getClassLoader()).getDeclaredField("MUSKET_DAMAGE").getFloat(null); if (!Float.isFinite(damage) || damage <= 0 || damage >= 1000) throw new IllegalStateException("Unsupported configured musket damage"); return damage; diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamOperationalCases.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamOperationalCases.java index efd253dd..ff671622 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamOperationalCases.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamOperationalCases.java @@ -4,10 +4,10 @@ import cyano.poweradvantage.api.fluid.FluidTank; import java.util.List; import java.util.function.Consumer; -import net.minecraft.fluid.Fluid; -import net.minecraft.item.ItemStack; -import net.minecraft.util.Direction; -import net.minecraft.util.ResourceLocation; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.item.ItemStack; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.fluids.capability.CapabilityFluidHandler; import net.minecraftforge.fluids.capability.IFluidHandler; import net.minecraftforge.registries.ForgeRegistries; @@ -36,7 +36,7 @@ private static void add(List cases, String id, String contract, } private static void machine(MachineContext c, String id) { - c.block(X, Y, Z, "steamadvantage:" + id); + c.block(X, Y, Z, "basesteam:" + id); } private static void supply(MachineContext c, float amount) { @@ -130,23 +130,23 @@ private static void boilers(List cases) { add(cases, "oil-production", "A finite 100 mB crude-oil charge converts 64 water to 80 steam at the historical 25-percent bonus.", 160, c -> { machine(c, "steam_boiler_oil"); fill(c, "minecraft:water", 64); - fill(c, "poweradvantage:crude_oil", 100); }, + fill(c, "basepower:crude_oil", 100); }, c -> { fluid(c, "getWaterTank", "minecraft:water", 0); - fluid(c, "getFuelTank", "poweradvantage:crude_oil", 0); + fluid(c, "getFuelTank", "basepower:crude_oil", 0); c.require(steam(c) == 80, "64 debited water produces exactly 80 steam at the oil boiler's 1.25 ratio"); }); add(cases, "oil-water-not-fuel", "Water is not liquid fuel and cannot burn itself.", 120, c -> { machine(c, "steam_boiler_oil"); fill(c, "minecraft:water", 64); }, c -> { fluid(c, "getWaterTank", "minecraft:water", 64); - fluid(c, "getFuelTank", "poweradvantage:crude_oil", 0); + fluid(c, "getFuelTank", "basepower:crude_oil", 0); c.require(steam(c) == 0, "Nonflammable water does not produce steam without fuel"); }); add(cases, "oil-dry", "Liquid fuel is preserved when no water is available.", 120, - c -> { machine(c, "steam_boiler_oil"); fill(c, "poweradvantage:crude_oil", 100); }, - c -> { fluid(c, "getFuelTank", "poweradvantage:crude_oil", 100); + c -> { machine(c, "steam_boiler_oil"); fill(c, "basepower:crude_oil", 100); }, + c -> { fluid(c, "getFuelTank", "basepower:crude_oil", 100); c.require(steam(c) == 0, "Dry oil boiler does not ignite"); }); add(cases, "oil-small-charge", "A 1 mB fuel aliquot can boil a finite three-water charge without inventing water.", 100, c -> { machine(c, "steam_boiler_oil"); fill(c, "minecraft:water", 3); - fill(c, "poweradvantage:crude_oil", 1); }, - c -> { fluid(c, "getFuelTank", "poweradvantage:crude_oil", 0); + fill(c, "basepower:crude_oil", 1); }, + c -> { fluid(c, "getFuelTank", "basepower:crude_oil", 0); fluid(c, "getWaterTank", "minecraft:water", 0); float value = steam(c); c.require(value >= 0 && value <= 3.75F, "Three water cannot create more than 3.75 steam at the historical oil bonus (including idle loss)"); @@ -266,42 +266,42 @@ private static void crusherAndStorage(List cases) { private static void still(List cases) { add(cases, "still-production", "Steam distills finite crude oil at the historical 2:1 ratio.", 180, - c -> { machine(c, "steam_still"); supply(c, 500); fill(c, "poweradvantage:crude_oil", 200); }, - c -> { fluid(c, "getInputTank", "poweradvantage:crude_oil", 0); - fluid(c, "getOutputTank", "poweradvantage:refined_oil", 100); + c -> { machine(c, "steam_still"); supply(c, 500); fill(c, "basepower:crude_oil", 200); }, + c -> { fluid(c, "getInputTank", "basepower:crude_oil", 0); + fluid(c, "getOutputTank", "basepower:refined_oil", 100); c.require(remainingSteam(c) <= 500 - 50 * 1.5F, "50 two-application distillations debit 75 steam"); }); add(cases, "still-no-power", "Valid crude oil remains untouched without steam.", 160, - c -> { machine(c, "steam_still"); fill(c, "poweradvantage:crude_oil", 200); }, - c -> { fluid(c, "getInputTank", "poweradvantage:crude_oil", 200); - fluid(c, "getOutputTank", "poweradvantage:refined_oil", 0); }); + c -> { machine(c, "steam_still"); fill(c, "basepower:crude_oil", 200); }, + c -> { fluid(c, "getInputTank", "basepower:crude_oil", 200); + fluid(c, "getOutputTank", "basepower:refined_oil", 0); }); add(cases, "still-full-output", "A full output cannot destroy crude oil or spend productive steam.", 160, - c -> { machine(c, "steam_still"); supply(c, 500); fill(c, "poweradvantage:crude_oil", 200); - reserve(c, "TankOut", "poweradvantage:refined_oil", 1000); }, - c -> { fluid(c, "getInputTank", "poweradvantage:crude_oil", 200); - fluid(c, "getOutputTank", "poweradvantage:refined_oil", 1000); + c -> { machine(c, "steam_still"); supply(c, 500); fill(c, "basepower:crude_oil", 200); + reserve(c, "TankOut", "basepower:refined_oil", 1000); }, + c -> { fluid(c, "getInputTank", "basepower:crude_oil", 200); + fluid(c, "getOutputTank", "basepower:refined_oil", 1000); c.require(remainingSteam(c) >= 500 - 2 * 160 * 0.0625F, "Full still spends no productive steam"); }); add(cases, "still-one-output-space", "The final single output space accepts one recipe, never discarding a second recipe.", 160, - c -> { machine(c, "steam_still"); supply(c, 500); fill(c, "poweradvantage:crude_oil", 200); - reserve(c, "TankOut", "poweradvantage:refined_oil", 999); }, - c -> { fluid(c, "getInputTank", "poweradvantage:crude_oil", 198); - fluid(c, "getOutputTank", "poweradvantage:refined_oil", 1000); + c -> { machine(c, "steam_still"); supply(c, 500); fill(c, "basepower:crude_oil", 200); + reserve(c, "TankOut", "basepower:refined_oil", 999); }, + c -> { fluid(c, "getInputTank", "basepower:crude_oil", 198); + fluid(c, "getOutputTank", "basepower:refined_oil", 1000); c.require(remainingSteam(c) <= 498.5F, "The single accepted operation debits steam"); }); add(cases, "still-incomplete-input", "An incomplete 2-mB recipe cannot turn one mB into product.", 160, c -> { machine(c, "steam_still"); supply(c, 500); - reserve(c, "TankIn", "poweradvantage:crude_oil", 1); }, - c -> { fluid(c, "getInputTank", "poweradvantage:crude_oil", 1); - fluid(c, "getOutputTank", "poweradvantage:refined_oil", 0); }); + reserve(c, "TankIn", "basepower:crude_oil", 1); }, + c -> { fluid(c, "getInputTank", "basepower:crude_oil", 1); + fluid(c, "getOutputTank", "basepower:refined_oil", 0); }); add(cases, "still-invalid-fluid", "A real capability rejects water as still feedstock.", 120, c -> { machine(c, "steam_still"); supply(c, 500); IFluidHandler handler = c.tile(X, Y, Z).getCapability( CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, Direction.UP) .orElseThrow(() -> new IllegalStateException("Missing still fluid capability")); - int accepted = handler.fill(new net.minecraftforge.fluids.FluidStack(net.minecraft.fluid.Fluids.WATER, 100), + int accepted = handler.fill(new net.minecraftforge.fluids.FluidStack(net.minecraft.world.level.material.Fluids.WATER, 100), IFluidHandler.FluidAction.EXECUTE); c.record("capability/rejected-water", accepted); c.require(accepted == 0, "Water is rejected by the still input capability"); }, c -> { fluid(c, "getInputTank", "minecraft:water", 0); - fluid(c, "getOutputTank", "poweradvantage:refined_oil", 0); }); + fluid(c, "getOutputTank", "basepower:refined_oil", 0); }); } private static void basin(MachineContext c) { @@ -331,18 +331,18 @@ private static boolean blockIs(MachineContext c, int x, int y, int z, String id) private static void pump(List cases) { add(cases, "pump-3x3-intake-lift", "The pump builds its lift, removes finite real sources and delivers matching fluid at elevation.", 320, c -> { machine(c, "steam_pump"); basin(c); supply(c, 600); - c.block(X + 1, Y, Z, "poweradvantage:fluid_storage_tank"); }, + c.block(X + 1, Y, Z, "basepower:fluid_storage_tank"); }, c -> { int removed = 9 - sources(c); int stored = c.fluidAmount(X + 1, Y, Z); int internal = amount(c, "getTank"); c.require(removed > 0 && stored >= 1000, "Real intake removal delivers at least a bucket four blocks above the intake"); c.require(stored + internal == removed * 1000, "Every removed source is conserved between receiver and pump"); - for (int y = 1; y <= 3; y++) c.require(blockIs(c, X, y, Z, "steamadvantage:pump_pipe_steam"), + for (int y = 1; y <= 3; y++) c.require(blockIs(c, X, y, Z, "basesteam:pump_pipe_steam"), "Natural pump ticks build lift segment " + y); c.require(remainingSteam(c) <= 600 - 3 * 5 - removed * (32 + 4), "Finite steam debits pipe placement and four-block lifting for every removed bucket"); }); add(cases, "pump-no-power", "A real 3x3 intake remains untouched without steam, including lift construction.", 180, c -> { machine(c, "steam_pump"); basin(c); - c.block(X + 1, Y, Z, "poweradvantage:fluid_storage_tank"); }, + c.block(X + 1, Y, Z, "basepower:fluid_storage_tank"); }, c -> { c.require(sources(c) == 9 && amount(c, "getTank") == 0 && c.fluidAmount(X + 1, Y, Z) == 0, "No power means no source removal or fluid output"); c.require(blockIs(c, X, Y - 1, Z, "minecraft:air"), "No steam means no lift construction"); }); @@ -352,7 +352,7 @@ private static void pump(List cases) { c.require(blockIs(c, X, Y - 1, Z, "minecraft:air"), "Insufficient steam cannot place the first pipe"); }); add(cases, "pump-full-output", "A full connected receiver cannot delete pump fluid or debit a new source.", 160, c -> { machine(c, "steam_pump"); basin(c); supply(c, 600); fill(c, "minecraft:lava", 1000); - c.block(X + 1, Y, Z, "poweradvantage:fluid_storage_tank"); + c.block(X + 1, Y, Z, "basepower:fluid_storage_tank"); c.tank(X + 1, Y, Z, "minecraft:lava", 4000); }, c -> { fluid(c, "getTank", "minecraft:lava", 1000); c.require(c.fluidAmount(X + 1, Y, Z) == 4000 && sources(c) == 9, diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamOperationalSupplementalCases.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamOperationalSupplementalCases.java index 988c2515..166f6158 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamOperationalSupplementalCases.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamOperationalSupplementalCases.java @@ -6,23 +6,23 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.UUID; -import net.minecraft.enchantment.Enchantment; -import net.minecraft.entity.EntityType; -import net.minecraft.entity.ai.attributes.Attributes; -import net.minecraft.entity.passive.CowEntity; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.network.IPacket; -import net.minecraft.network.NetworkManager; -import net.minecraft.network.PacketDirection; -import net.minecraft.network.play.ServerPlayNetHandler; -import net.minecraft.network.play.server.SEntityVelocityPacket; -import net.minecraft.util.ActionResultType; -import net.minecraft.util.Hand; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.vector.Vector3d; -import net.minecraft.world.GameType; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.animal.Cow; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionHand; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.level.GameType; import net.minecraftforge.common.util.FakePlayer; import net.minecraftforge.registries.ForgeRegistries; @@ -43,11 +43,11 @@ private static void priority(List cases) { "A finite source fills a normal consumer before backup storage. Disabling backup discharge " + "prevents later relay masking the original allocation; a full-consumer control proves backup connectivity.", 80, c -> { - c.block(8, 4, 8, "steamadvantage:steam_boiler_coal"); - c.block(9, 4, 8, "steamadvantage:steam_furnace"); - c.block(7, 4, 8, "steamadvantage:steam_pipe"); - c.block(6, 4, 8, "steamadvantage:steam_pipe"); - c.block(5, 4, 8, "steamadvantage:steam_tank"); + c.block(8, 4, 8, "basesteam:steam_boiler_coal"); + c.block(9, 4, 8, "basesteam:steam_furnace"); + c.block(7, 4, 8, "basesteam:steam_pipe"); + c.block(6, 4, 8, "basesteam:steam_pipe"); + c.block(5, 4, 8, "basesteam:steam_tank"); c.block(4, 4, 8, "minecraft:redstone_block"); c.power(8, 4, 8, "steam", 40); if (consumerFull) c.power(9, 4, 8, "steam", 200); @@ -80,7 +80,7 @@ private static void priority(List cases) { private static final class Shot { FakePlayer player; - CowEntity impact, collateral; + Cow impact, collateral; ItemStack weapon; float configuredDamage; int velocityPackets, immediateCollateralFire; @@ -94,11 +94,11 @@ private static void recoil(List cases, int level) { + "only when enchanted. This measures impulse/packet delivery, not a manually ticked fake player's travel.", 40, c -> { prepareShot(c, shot, "recoil-" + level, false); - if (level > 0) enchant(c, shot.weapon, "steamadvantage:recoil", level); + if (level > 0) enchant(c, shot.weapon, "basesteam:recoil", level); fire(c, shot); }, c -> { verifyShot(c, shot, false, false); - Vector3d motion = shot.player.getDeltaMovement(); + Vec3 motion = shot.player.getDeltaMovement(); c.record("recoil/deltaX", motion.x); c.record("recoil/deltaY", motion.y); c.record("recoil/deltaZ", motion.z); c.record("recoil/velocityPackets", shot.velocityPackets); c.require(Math.abs(motion.x) < 0.00001 && Math.abs(motion.y) < 0.00001 @@ -116,7 +116,7 @@ private static void explosive(List cases, int level) { + "Finite targets and the level-one blast remain inside the bay; no TNT or flammable terrain is seeded.", 40, c -> { prepareShot(c, shot, "explosive-" + level, true); - if (level > 0) enchant(c, shot.weapon, "steamadvantage:high_explosive", level); + if (level > 0) enchant(c, shot.weapon, "basesteam:high_explosive", level); fire(c, shot); shot.immediateCollateralFire = shot.collateral.getRemainingFireTicks(); shot.immediateCollateralHealth = shot.collateral.getHealth(); @@ -152,7 +152,7 @@ private static void occlusion(List cases) { fire(c, shot); }, c -> { verifyShot(c, shot, false, true); - c.require(c.world.getBlockState(c.at(16, 9, 13)).getBlock() == net.minecraft.block.Blocks.STONE, + c.require(c.world.getBlockState(c.at(16, 9, 13)).getBlock() == net.minecraft.world.level.block.Blocks.STONE, "Nonexplosive shot preserves the real stone obstruction"); })); } @@ -161,20 +161,20 @@ private static void prepareShot(MachineContext c, Shot shot, String id, boolean BlockPos muzzle = c.at(16, 8, 10); UUID uuid = UUID.nameUUIDFromBytes(("SteamOperationalExtra/" + id + "/" + muzzle).getBytes(StandardCharsets.UTF_8)); shot.player = new FakePlayer(c.world, new GameProfile(uuid, "SteamOpExtras")); - shot.player.connection = new ServerPlayNetHandler(c.world.getServer(), new NetworkManager(PacketDirection.SERVERBOUND), shot.player) { - @Override public void send(IPacket packet) { - if (packet instanceof SEntityVelocityPacket) shot.velocityPackets++; + shot.player.connection = new ServerGamePacketListenerImpl(c.world.getServer(), new Connection(PacketFlow.SERVERBOUND), shot.player) { + @Override public void send(Packet packet) { + if (packet instanceof ClientboundSetEntityMotionPacket) shot.velocityPackets++; } }; - shot.player.gameMode.setGameModeForPlayer(GameType.SURVIVAL); + shot.player.gameMode.changeGameModeForPlayer(GameType.SURVIVAL); shot.player.moveTo(muzzle.getX() + 0.5, muzzle.getY(), muzzle.getZ() + 0.5, 0, 0); - shot.player.setDeltaMovement(Vector3d.ZERO); - shot.weapon = new ItemStack(item(c, "steamadvantage:musket")); - shot.player.setItemInHand(Hand.MAIN_HAND, shot.weapon); - shot.player.inventory.setItem(1, new ItemStack(item(c, "steamadvantage:blackpowder_cartridge"), 2)); - enchant(c, shot.weapon, "steamadvantage:rapid_reload", 1); + shot.player.setDeltaMovement(Vec3.ZERO); + shot.weapon = new ItemStack(item(c, "basesteam:musket")); + shot.player.setItemInHand(InteractionHand.MAIN_HAND, shot.weapon); + shot.player.getInventory().setItem(1, new ItemStack(item(c, "basesteam:blackpowder_cartridge"), 2)); + enchant(c, shot.weapon, "basesteam:rapid_reload", 1); try { - shot.configuredDamage = Class.forName("zone.moddev.mc.steamadvantage.SteamAdvantage", false, + shot.configuredDamage = Class.forName("zone.moddev.mc.basesteam.SteamAdvantage", false, SteamOperationalSupplementalCases.class.getClassLoader()).getDeclaredField("MUSKET_DAMAGE").getFloat(null); } catch (ReflectiveOperationException error) { throw new IllegalStateException("Musket configuration contract is unavailable", error); @@ -191,8 +191,8 @@ private static void prepareShot(MachineContext c, Shot shot, String id, boolean for (int y = 8; y <= 11; y++) c.block(16, y, 24, "minecraft:obsidian"); } - private static CowEntity target(MachineContext c, double x, double y, double z) { - CowEntity target = EntityType.COW.create(c.world); + private static Cow target(MachineContext c, double x, double y, double z) { + Cow target = EntityType.COW.create(c.world); c.require(target != null, "Native living target is available"); BlockPos origin = c.at(0, 0, 0); target.getAttribute(Attributes.MAX_HEALTH).setBaseValue(1000); @@ -205,11 +205,11 @@ private static CowEntity target(MachineContext c, double x, double y, double z) private static void fire(MachineContext c, Shot shot) { Item weapon = shot.weapon.getItem(); - c.require(weapon.use(c.world, shot.player, Hand.MAIN_HAND).getResult() == ActionResultType.SUCCESS, + c.require(weapon.use(c.world, shot.player, InteractionHand.MAIN_HAND).getResult() == InteractionResult.SUCCESS, "Native rapid reload interaction succeeds"); - c.require(shot.player.inventory.getItem(1).getCount() == 1 + c.require(shot.player.getInventory().getItem(1).getCount() == 1 && shot.weapon.getOrCreateTag().getByte("loaded") == 1, "Exactly one finite cartridge loads the chamber"); - c.require(weapon.use(c.world, shot.player, Hand.MAIN_HAND).getResult() == ActionResultType.SUCCESS + c.require(weapon.use(c.world, shot.player, InteractionHand.MAIN_HAND).getResult() == InteractionResult.SUCCESS && shot.player.isUsingItem(), "Native use interaction enters the aiming lifecycle"); shot.player.releaseUsingItem(); } @@ -217,7 +217,7 @@ private static void fire(MachineContext c, Shot shot) { private static void verifyShot(MachineContext c, Shot shot, boolean explosive, boolean occluded) { c.record("impact/finalHealth", shot.impact.getHealth()); c.record("impact/naturalTicks", shot.impact.tickCount); - c.record("musket/ammo", shot.player.inventory.getItem(1).getCount()); + c.record("musket/ammo", shot.player.getInventory().getItem(1).getCount()); c.record("musket/durabilityDebit", shot.weapon.getDamageValue()); c.require(shot.impact.isAlive() && shot.impact.tickCount >= 35, "Impact target survives real natural server ticks"); if (occluded) c.require(shot.impact.getHealth() == 1000, "Solid wall completely prevents target damage"); @@ -225,13 +225,13 @@ else if (explosive) c.require(shot.impact.getHealth() <= 1000 - shot.configuredD "Explosive shot delivers at least configured direct impact damage"); else c.require(Math.abs(shot.impact.getHealth() - (1000 - shot.configuredDamage)) < 0.001F, "Unenchanted damage is exactly the configured direct impact"); - c.require(shot.weapon.getDamageValue() == 1 && shot.player.inventory.getItem(1).getCount() == 1, + c.require(shot.weapon.getDamageValue() == 1 && shot.player.getInventory().getItem(1).getCount() == 1, "Each shot consumes one durability and exactly one cartridge, without duplicating debit"); c.require(shot.weapon.getOrCreateTag().getByte("loaded") == 0, "Shot leaves an empty chamber"); inside(c, shot.impact); } - private static void inside(MachineContext c, CowEntity target) { + private static void inside(MachineContext c, Cow target) { BlockPos min = c.at(0, 0, 0), max = c.at(31, 31, 31); c.record("target/position/" + target.getUUID(), new double[]{target.getX(), target.getY(), target.getZ()}); c.require(target.getX() >= min.getX() && target.getX() < max.getX() + 1 @@ -242,7 +242,7 @@ private static void inside(MachineContext c, CowEntity target) { private static Item item(MachineContext c, String id) { Item item = ForgeRegistries.ITEMS.getValue(new ResourceLocation(id)); - c.require(item != null && item != net.minecraft.item.Items.AIR, "Registered item " + id); + c.require(item != null && item != net.minecraft.world.item.Items.AIR, "Registered item " + id); return item; } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamReloadControlCases.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamReloadControlCases.java index ba55685f..fcc7e2e4 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamReloadControlCases.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamReloadControlCases.java @@ -1,7 +1,7 @@ package zone.moddev.mc.advantageworks; import java.util.List; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.CompoundTag; /** One real loaded-machine tick, with exact work, control and resource observations. */ final class SteamReloadControlCases { @@ -16,7 +16,7 @@ static void register(List cases) { int[] stock = {0, 1, 32, 64}; for (int i = 0; i < stock.length; i++) { int x = 3 + i * 6; - c.block(x, Y, Z, "steamadvantage:steam_crusher"); + c.block(x, Y, Z, "basesteam:steam_crusher"); c.put(x, Y, Z, 0, "minecraft:stone", stock[i]); } }, holding(new int[]{3, 9, 15, 21}, c -> { @@ -41,10 +41,10 @@ private static void crusher(List cases, String name, boolean si cases.add(new MachineScenario("steam/lifecycle/crusher-first-loaded-" + name, "One native tick observes live redstone before touching saved crusher progress; no reload grace or replacement.", 1, c -> { - c.block(X, Y, Z, "steamadvantage:steam_crusher"); + c.block(X, Y, Z, "basesteam:steam_crusher"); if (signalled) c.block(X - 1, Y, Z, "minecraft:redstone_block"); c.put(X, Y, Z, 0, "minecraft:stone", 1); - CompoundNBT tag = new CompoundNBT(); tag.putShort("progress", (short)106); c.seed(X, Y, Z, tag); + CompoundTag tag = new CompoundTag(); tag.putShort("progress", (short)106); c.seed(X, Y, Z, tag); c.power(X, Y, Z, "steam", steam); c.require(c.nbt(X, Y, Z).getShort("progress") == 106, "Loaded work is 106 before the first native tick"); }, holding(new int[]{X}, c -> { @@ -66,10 +66,10 @@ private static void furnace(List cases, boolean signalled) { cases.add(new MachineScenario("steam/lifecycle/furnace-first-loaded-" + (signalled ? "signalled" : "unpowered"), "A burning, partly completed furnace uses the real control signal for its first steam heat bonus and work increment.", 1, c -> { - c.block(X, Y, Z, "steamadvantage:steam_furnace"); + c.block(X, Y, Z, "basesteam:steam_furnace"); if (signalled) c.block(X - 1, Y, Z, "minecraft:redstone_block"); c.put(X, Y, Z, 1, "minecraft:sand", 1); - CompoundNBT tag = new CompoundNBT(); + CompoundTag tag = new CompoundTag(); tag.putIntArray("Data", new int[]{Float.floatToRawIntBits(.25F), 0, 0, 10, 10, Float.floatToRawIntBits(1000)}); c.seed(X, Y, Z, tag); c.power(X, Y, Z, "steam", 40); c.require(c.nbt(X, Y, Z).getIntArray("Data")[3] == 10, "Loaded burning state exists before any tick"); @@ -112,10 +112,10 @@ private static java.util.function.Consumer holding(int[] columns private static void stopMeasuredTiles(MachineContext c, int... columns) { java.util.List> held = new java.util.ArrayList<>(); for (int x : columns) { - net.minecraft.tileentity.TileEntity actual = c.tile(x, Y, Z); + net.minecraft.world.level.block.entity.BlockEntity actual = c.tile(x, Y, Z); Object before = c.nbt(x, Y, Z); - removeMeasuredTile(c.world.tickableBlockEntities, actual); - c.require(!c.world.tickableBlockEntities.contains(actual), "Measured control tile is no longer tickable"); + NativeBlockEntityTicks.remove(c.world, actual); + c.require(!NativeBlockEntityTicks.contains(c.world, actual), "Measured control tile is no longer tickable"); c.require(c.tile(x, Y, Z) == actual, "Holding preserves the same tile in the world and chunk indexes"); c.require(before.equals(c.nbt(x, Y, Z)), "Holding writes no NBT or expected state"); java.util.Map witness = new java.util.LinkedHashMap<>(); 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 f1ecf0c7..081a129e 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 @@ -4,43 +4,44 @@ import cyano.poweradvantage.api.fluid.FluidNetworkApi; import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; import cyano.poweradvantage.api.migration.LegacyMigrationApi; -import net.minecraft.block.Block; -import net.minecraft.block.FenceGateBlock; -import net.minecraft.block.FlowingFluidBlock; -import net.minecraft.block.StandingSignBlock; -import net.minecraft.block.BlockState; -import net.minecraft.command.CommandException; -import net.minecraft.enchantment.Enchantment; -import net.minecraft.entity.Entity; -import net.minecraft.entity.EntityType; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.block.Blocks; -import net.minecraft.inventory.IInventory; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.FenceGateBlock; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.block.StandingSignBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.commands.CommandRuntimeException; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.Container; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; import net.minecraft.server.MinecraftServer; -import net.minecraft.state.Property; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.tileentity.SignTileEntity; -import net.minecraft.util.Direction; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.Registry; -import net.minecraft.util.math.AxisAlignedBB; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.text.Style; -import net.minecraft.util.text.StringTextComponent; -import net.minecraft.util.text.event.ClickEvent; -import net.minecraft.world.server.ServerWorld; -import net.minecraft.world.World; -import net.minecraft.world.biome.Biome; -import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.SignBlockEntity; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.Registry; +import net.minecraft.world.phys.AABB; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.chunk.LevelChunk; import net.minecraftforge.common.BiomeDictionary; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.TickEvent; import net.minecraftforge.fml.ModList; +import net.minecraftforge.fmlserverevents.FMLServerStoppingEvent; import net.minecraftforge.registries.ForgeRegistries; -import net.minecraft.fluid.Fluid; +import net.minecraft.world.level.material.Fluid; import cyano.poweradvantage.api.fluid.FluidStack; import java.io.File; @@ -70,6 +71,7 @@ final class WorksController { private WorksController() { MinecraftForge.EVENT_BUS.addListener(this::worldgenTick); + MinecraftForge.EVENT_BUS.addListener(this::worldgenStopping); } static WorksController getInstance() { @@ -78,23 +80,23 @@ static WorksController getInstance() { synchronized void loadDefinition() { if (definition != null) return; - definition = WorksDefinitionLoader.loadNative116(); + definition = WorksDefinitionLoader.loadNative117(); for (WorksDefinition.Station station : definition.stations) { stations.put(station.id.toUpperCase(Locale.ROOT), station); statuses.put(station.id, "NOT_BUILT"); } } - void requireAuthorizedWorld(MinecraftServer server) throws CommandException { + void requireAuthorizedWorld(MinecraftServer server) throws CommandRuntimeException { File marker = new File("advantage-works.allow"); - if (!WorksWorldGuard.authorized(server.getWorldPath(net.minecraft.world.storage.FolderName.ROOT), marker.isFile())) { + if (!WorksWorldGuard.authorized(server.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT), marker.isFile())) { throw commandError("Refusing to alter an unmarked world. Expected AdvantageWorks level and advantage-works.allow."); } } - synchronized String build(MinecraftServer server, String target) throws CommandException { + synchronized String build(MinecraftServer server, String target) throws CommandRuntimeException { loadDefinition(); - ServerWorld world = overworld(server); + ServerLevel world = overworld(server); if ("all".equalsIgnoreCase(target)) buildConcourse(world); List selected = select(target, true); int built = 0; @@ -114,9 +116,9 @@ synchronized String build(MinecraftServer server, String target) throws CommandE return "Advantage Works built " + built + " station(s); skipped " + skipped + " for missing mods."; } - synchronized String reset(MinecraftServer server, String target) throws CommandException { + synchronized String reset(MinecraftServer server, String target) throws CommandRuntimeException { List selected = select(target, true); - ServerWorld world = overworld(server); + ServerLevel world = overworld(server); int reset = 0; for (WorksDefinition.Station station : selected) { if (!requirementsPresent(station)) continue; @@ -129,7 +131,7 @@ synchronized String reset(MinecraftServer server, String target) throws CommandE return "Reset " + reset + " station(s)."; } - synchronized String start(MinecraftServer server, String target) throws CommandException { + synchronized String start(MinecraftServer server, String target) throws CommandRuntimeException { WorksDefinition.Station station = oneStation(target); if (!requirementsPresent(station)) { statuses.put(station.id, "SKIP"); @@ -139,7 +141,7 @@ synchronized String start(MinecraftServer server, String target) throws CommandE && !station.id.equals(activeExclusiveStation)) { throw commandError("Stop exclusive station " + activeExclusiveStation + " first."); } - ServerWorld world = overworld(server); + ServerLevel world = overworld(server); setStationChunksForced(world, station, true); applyActions(world, station, station.startActions); if (station.exclusive) activeExclusiveStation = station.id; @@ -148,9 +150,9 @@ synchronized String start(MinecraftServer server, String target) throws CommandE return "Started " + station.id + " - " + station.name; } - synchronized String stop(MinecraftServer server, String target) throws CommandException { + synchronized String stop(MinecraftServer server, String target) throws CommandRuntimeException { WorksDefinition.Station station = oneStation(target); - ServerWorld world = overworld(server); + ServerLevel world = overworld(server); if (requirementsPresent(station)) { applyActions(world, station, station.stopActions); removeTaggedEntities(world, station); @@ -161,7 +163,7 @@ synchronized String stop(MinecraftServer server, String target) throws CommandEx return "Stopped " + station.id + " - " + station.name; } - private static void setStationChunksForced(ServerWorld world, + private static void setStationChunksForced(ServerLevel world, WorksDefinition.Station station, boolean forced) { int minChunkX = (station.origin[0] + station.boundsMin[0]) >> 4; @@ -175,9 +177,9 @@ private static void setStationChunksForced(ServerWorld world, } } - synchronized String check(MinecraftServer server, String target) throws CommandException { + synchronized String check(MinecraftServer server, String target) throws CommandRuntimeException { List selected = select(target, true); - ServerWorld world = overworld(server); + ServerLevel world = overworld(server); Map summary = new LinkedHashMap<>(); for (WorksDefinition.Station station : selected) { String status = checkStation(world, station); @@ -191,13 +193,13 @@ synchronized String check(MinecraftServer server, String target) throws CommandE return "Advantage Works check: " + summary; } - synchronized String status(String target) throws CommandException { + synchronized String status(String target) throws CommandRuntimeException { if ("all".equalsIgnoreCase(target)) return statuses.toString(); WorksDefinition.Station station = oneStation(target); return station.id + " " + station.name + ": " + statuses.get(station.id); } - synchronized String checkpoint(MinecraftServer server, String target, String name) throws CommandException { + synchronized String checkpoint(MinecraftServer server, String target, String name) throws CommandRuntimeException { WorksDefinition.Station station = oneStation(target); Map payload = new LinkedHashMap<>(); payload.put("checkpoint", name); @@ -207,14 +209,14 @@ synchronized String checkpoint(MinecraftServer server, String target, String nam return "Checkpoint written for " + station.id + " to " + result.getPath(); } - synchronized String sampleWorldgen(MinecraftServer server, int radius) throws CommandException { + synchronized String sampleWorldgen(MinecraftServer server, int radius) throws CommandRuntimeException { if (radius < 1 || radius > 16) { throw commandError("Worldgen sample radius must be between 1 and 16 chunks"); } if (worldgenJob.current() != null) throw commandError("A worldgen sample is already running"); - ServerWorld world = overworld(server); + ServerLevel world = overworld(server); BlockPos spawn = world.getSharedSpawnPos(); - worldgenJob.start(new WorldgenSamplingJob(new NativeWorldgenAccess(world), + worldgenJob.start(new WorldgenSamplingJob(new Native117WorldgenAccess(world), spawn.getX(), spawn.getY(), spawn.getZ(), world.getSeaLevel(), radius)); return "Queued worldgen sample with radius " + radius + " for bounded server-tick sampling"; } @@ -227,6 +229,7 @@ private synchronized void worldgenTick(TickEvent.ServerTickEvent event) { try { if (job.failure() != null) throw job.failure(); Map payload = job.result(); + payload.put("chunkLoading", job.diagnostics()); File result = resultWriter.write("worldgen", "sample", payload); org.apache.logging.log4j.LogManager.getLogger(AdvantageWorksMod.MODID).info( "Worldgen sample wrote {} chunks to {}: {}", payload.get("chunksScanned"), @@ -237,6 +240,7 @@ private synchronized void worldgenTick(TickEvent.ServerTickEvent event) { Map payload = new LinkedHashMap<>(); payload.put("phase", job.phase().name()); payload.put("failure", failure.toString()); + payload.put("chunkLoading", job.diagnostics()); try { resultWriter.write("worldgen", "sample-failed", payload); } catch (RuntimeException writeFailure) { @@ -248,34 +252,12 @@ private synchronized void worldgenTick(TickEvent.ServerTickEvent event) { } } - private static final class NativeWorldgenAccess implements WorldgenSamplingJob.WorldAccess { - private final ServerWorld world; - private final BlockPos.Mutable cursor = new BlockPos.Mutable(); - - NativeWorldgenAccess(ServerWorld world) { this.world = world; } - - @Override public boolean matchesBiome(String expected, int x, int y, int z) { - return matchesExpectedBiome(world, expected, world.getBiome(cursor.set(x, y, z))); - } - - @Override public void populateChunk(int chunkX, int chunkZ) { world.getChunk(chunkX, chunkZ); } - - @Override public WorldgenSamplingJob.SampleChunk sampleChunk(int chunkX, int chunkZ) { - Chunk chunk = world.getChunk(chunkX, chunkZ); - Biome center = world.getBiome(new BlockPos((chunkX << 4) + 8, world.getSeaLevel(), (chunkZ << 4) + 8)); - return new WorldgenSamplingJob.SampleChunk() { - @Override public boolean centerMatches(String expected) { return matchesExpectedBiome(world, expected, center); } - @Override public String blockName(int x, int y, int z) { - ResourceLocation id = ForgeRegistries.BLOCKS.getKey( - chunk.getBlockState(cursor.set(x, y, z)).getBlock()); - return id == null ? null : id.toString(); - } - }; - } + private synchronized void worldgenStopping(FMLServerStoppingEvent event) { + worldgenJob.release(); } - private static boolean matchesExpectedBiome(ServerWorld world, String expected, Biome biome) { - net.minecraft.util.RegistryKey key = world.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY) + static boolean matchesExpectedBiome(ServerLevel world, String expected, Biome biome) { + net.minecraft.resources.ResourceKey key = world.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY) .getResourceKey(biome).orElseThrow(() -> new IllegalStateException("Unregistered native biome")); if ("DESERT".equals(expected)) { return BiomeDictionary.hasType(key, BiomeDictionary.Type.getType("SANDY")) @@ -296,7 +278,7 @@ List stationIdsWithAll() { return ids; } - private String checkStation(ServerWorld world, WorksDefinition.Station station) { + private String checkStation(ServerLevel world, WorksDefinition.Station station) { if (!requirementsPresent(station)) { statuses.put(station.id, "SKIP"); setStatusBlock(world, station, STATUS_ATTENTION); @@ -342,7 +324,7 @@ private String checkStation(ServerWorld world, WorksDefinition.Station station) return status; } - private void writeStationResult(ServerWorld world, WorksDefinition.Station station, String status, + private void writeStationResult(ServerLevel world, WorksDefinition.Station station, String status, List> assertions) { Map payload = new LinkedHashMap<>(); payload.put("name", station.name); @@ -357,7 +339,7 @@ private void writeStationResult(ServerWorld world, WorksDefinition.Station stati resultWriter.write(station.id, "latest", payload); } - private List> snapshot(ServerWorld world, WorksDefinition.Station station) { + private List> snapshot(ServerLevel world, WorksDefinition.Station station) { List> snapshot = new ArrayList<>(); Set positions = new LinkedHashSet<>(); for (WorksDefinition.Placement placement : station.placements) positions.add(absolute(station, placement.pos)); @@ -372,14 +354,14 @@ private List> snapshot(ServerWorld world, WorksDefinition.St block.put("pos", Arrays.asList(pos.getX(), pos.getY(), pos.getZ())); block.put("block", name == null ? "unknown" : name.toString()); block.put("state", state.toString()); - TileEntity tile = world.getBlockEntity(pos); - if (tile != null) block.put("nbt", tile.save(new CompoundNBT()).toString()); + BlockEntity tile = world.getBlockEntity(pos); + if (tile != null) block.put("nbt", tile.save(new CompoundTag()).toString()); snapshot.add(block); } return snapshot; } - private boolean evaluateAssertion(ServerWorld world, WorksDefinition.Station station, + private boolean evaluateAssertion(ServerLevel world, WorksDefinition.Station station, WorksDefinition.Assertion assertion) { if (assertion.block != null && block(assertion.block) == Blocks.AIR && !"minecraft:air".equals(assertion.block)) { @@ -410,9 +392,9 @@ private boolean evaluateAssertion(ServerWorld world, WorksDefinition.Station sta if ("fluidAmount".equals(assertion.type)) { long total = 0; for (int[] position : assertion.positions) { - TileEntity source = world.getBlockEntity(absolute(station, position)); + BlockEntity source = world.getBlockEntity(absolute(station, position)); if (source == null) return false; - total += AssertionChecks.fluidAmount(source.save(new CompoundNBT()), assertion.nbtPath, assertion.fluid); + total += AssertionChecks.fluidAmount(source.save(new CompoundTag()), assertion.nbtPath, assertion.fluid); } return compare(total, assertion.operator, assertion.value); } @@ -439,19 +421,19 @@ private boolean evaluateAssertion(ServerWorld world, WorksDefinition.Station sta && compare(source.getAnalogOutputSignal(state, world, pos), assertion.operator, assertion.value); } - TileEntity tile = world.getBlockEntity(pos); + BlockEntity tile = world.getBlockEntity(pos); if ("tileExists".equals(assertion.type)) return tile != null; if ("inventoryEmpty".equals(assertion.type)) { - if (!(tile instanceof IInventory)) return false; - return inventoryCount((IInventory) tile, assertion.item) == 0; + if (!(tile instanceof Container)) return false; + return inventoryCount((Container) tile, assertion.item) == 0; } if ("inventoryCount".equals(assertion.type)) { - if (!(tile instanceof IInventory)) return false; - return compare(inventoryCount((IInventory) tile, assertion.item), assertion.operator, assertion.count); + if (!(tile instanceof Container)) return false; + return compare(inventoryCount((Container) tile, assertion.item), assertion.operator, assertion.count); } if ("inventoryNbtNumber".equals(assertion.type)) { - if (!(tile instanceof IInventory) || assertion.slot == null) return false; - IInventory inventory = (IInventory) tile; + if (!(tile instanceof Container) || assertion.slot == null) return false; + Container inventory = (Container) tile; if (assertion.slot < 0 || assertion.slot >= inventory.getContainerSize()) return false; ItemStack stack = inventory.getItem(assertion.slot); if (stack.isEmpty() || !stack.hasTag()) return false; @@ -460,18 +442,18 @@ && compare(source.getAnalogOutputSignal(state, world, pos), } if ("nbtNumber".equals(assertion.type)) { if (tile == null) return false; - CompoundNBT nbt = tile.save(new CompoundNBT()); + CompoundTag nbt = tile.save(new CompoundTag()); 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 CompoundNBT()), assertion.nbtPath); + String text = stringAt(tile.save(new CompoundTag()), 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 CompoundNBT()).getIntArray("Energy"); + int[] energy = tile.save(new CompoundTag()).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); @@ -479,7 +461,7 @@ && compare(source.getAnalogOutputSignal(state, world, pos), throw new IllegalArgumentException("Unsupported assertion type " + assertion.type); } - private static int inventoryCount(IInventory inventory, String itemName) { + private static int inventoryCount(Container inventory, String itemName) { Item expected = itemName == null ? null : item(itemName); if (itemName != null && expected == null) throw new IllegalArgumentException("Unregistered assertion item " + itemName); int count = 0; @@ -490,10 +472,10 @@ private static int inventoryCount(IInventory inventory, String itemName) { return count; } - private static Number numberAt(CompoundNBT root, String path) { + private static Number numberAt(CompoundTag root, String path) { if (path == null || path.isEmpty()) return null; String[] parts = path.split("\\."); - CompoundNBT current = root; + CompoundTag current = root; for (int i = 0; i < parts.length - 1; i++) { if (!current.contains(parts[i], 10)) return null; current = current.getCompound(parts[i]); @@ -503,10 +485,10 @@ private static Number numberAt(CompoundNBT root, String path) { return current.getDouble(leaf); } - private static String stringAt(CompoundNBT root, String path) { + private static String stringAt(CompoundTag root, String path) { if (path == null || path.isEmpty()) return null; String[] parts = path.split("\\."); - CompoundNBT current = root; + CompoundTag current = root; for (int i = 0; i < parts.length - 1; i++) { if (!current.contains(parts[i], 10)) return null; current = current.getCompound(parts[i]); @@ -519,7 +501,7 @@ private static boolean compare(double actual, String operator, Number expected) return AssertionChecks.compare(actual, operator, expected); } - private void resetStation(ServerWorld world, WorksDefinition.Station station) { + private void resetStation(ServerLevel world, WorksDefinition.Station station) { clearStation(world, station); decorateStation(world, station); for (WorksDefinition.Placement placement : station.placements) applyPlacement(world, station, placement); @@ -527,7 +509,7 @@ private void resetStation(ServerWorld world, WorksDefinition.Station station) { setStatusBlock(world, station, STATUS_NEUTRAL); } - private void clearStation(ServerWorld world, WorksDefinition.Station station) { + private void clearStation(ServerLevel world, WorksDefinition.Station station) { BlockPos min = absolute(station, station.boundsMin); BlockPos max = absolute(station, station.boundsMax); removeTaggedEntities(world, station); @@ -536,18 +518,24 @@ private void clearStation(ServerWorld world, WorksDefinition.Station station) { } } - private void removeTaggedEntities(ServerWorld world, WorksDefinition.Station station) { + private void removeTaggedEntities(ServerLevel world, WorksDefinition.Station station) { BlockPos min = absolute(station, station.boundsMin); BlockPos max = absolute(station, station.boundsMax); for (Entity entity : world.getEntitiesOfClass(Entity.class, - new AxisAlignedBB(min, max.offset(1, 1, 1)))) { - if (station.id.equals(entity.getPersistentData().getString("AdvantageWorksStation"))) { - entity.remove(); + new AABB(min, max.offset(1, 1, 1)))) { + if (StationOwnership.matches(entity, station.id)) { + entity.discard(); } } } - private void decorateStation(ServerWorld world, WorksDefinition.Station station) { + static final class StationOwnership { + static boolean matches(Entity entity, String stationId) { + return stationId.equals(NativeEntityData.string(entity, "AdvantageWorksStation")); + } + } + + private void decorateStation(ServerLevel world, WorksDefinition.Station station) { int color = districtColor(station.district); int maxX = station.boundsMax[0]; int maxZ = station.boundsMax[2]; @@ -572,7 +560,7 @@ private void decorateStation(ServerWorld world, WorksDefinition.Station station) "/advworks stop " + station.id); } - private void encloseStation(ServerWorld world, WorksDefinition.Station station) { + private void encloseStation(ServerLevel world, WorksDefinition.Station station) { int minX = station.boundsMin[0]; int maxX = station.boundsMax[0]; int minZ = station.boundsMin[2]; @@ -598,7 +586,7 @@ private void encloseStation(ServerWorld world, WorksDefinition.Station station) Blocks.OAK_FENCE_GATE.defaultBlockState().setValue(FenceGateBlock.FACING, Direction.NORTH), 2); } - private void buildConcourse(ServerWorld world) { + private void buildConcourse(ServerLevel world) { for (int x = -15; x <= 15; x++) { for (int z = -15; z <= 15; z++) { world.setBlock(new BlockPos(x, 3, z), Blocks.STONE_BRICKS.defaultBlockState(), 2); @@ -616,7 +604,7 @@ private void buildConcourse(ServerWorld world) { placeSign(world, new BlockPos(0, 4, 3), "PROVING", "GROUNDS SOUTH", "", ""); } - private void applyActions(ServerWorld world, WorksDefinition.Station station, + private void applyActions(ServerLevel world, WorksDefinition.Station station, List actions) { for (WorksDefinition.Action action : actions) { if (!requirementsPresent(action.requiredMods)) continue; @@ -630,12 +618,12 @@ private void applyActions(ServerWorld world, WorksDefinition.Station station, world.setBlock(pos, Blocks.AIR.defaultBlockState(), 2); break; case "clearInventory": - TileEntity clearTile = world.getBlockEntity(pos); - if (clearTile instanceof IInventory) ((IInventory) clearTile).clearContent(); + BlockEntity clearTile = world.getBlockEntity(pos); + if (clearTile instanceof Container) ((Container) clearTile).clearContent(); break; case "setInventory": - TileEntity tile = world.getBlockEntity(pos); - if (!(tile instanceof IInventory)) throw new IllegalStateException("No inventory at " + pos); + BlockEntity tile = world.getBlockEntity(pos); + if (!(tile instanceof Container)) throw new IllegalStateException("No inventory at " + pos); Item stackItem = item(action.item); if (stackItem == null) throw new IllegalStateException("Unknown item " + action.item); ItemStack stack = itemStack(stackItem, action.count, action.damage); @@ -654,15 +642,15 @@ private void applyActions(ServerWorld world, WorksDefinition.Station station, } stack.enchant(enchantment, action.level); } - ((IInventory) tile).setItem(action.slot, stack); + ((Container) tile).setItem(action.slot, stack); tile.setChanged(); break; case "fillInventory": - TileEntity fillTile = world.getBlockEntity(pos); - if (!(fillTile instanceof IInventory)) throw new IllegalStateException("No inventory at " + pos); + BlockEntity fillTile = world.getBlockEntity(pos); + if (!(fillTile instanceof Container)) throw new IllegalStateException("No inventory at " + pos); Item fillItem = item(action.item); if (fillItem == null) throw new IllegalStateException("Unknown item " + action.item); - IInventory inventory = (IInventory) fillTile; + Container inventory = (Container) fillTile; ItemStack fillTemplate = itemStack(fillItem, 1, action.damage); int fillCount = Math.min(action.count, Math.min(fillTemplate.getMaxStackSize(), inventory.getMaxStackSize())); @@ -676,7 +664,7 @@ private void applyActions(ServerWorld world, WorksDefinition.Station station, mergeTileNbt(world, pos, action.nbt); break; case "setFluid": - TileEntity fluidTile = world.getBlockEntity(pos); + BlockEntity fluidTile = world.getBlockEntity(pos); Fluid fluid = fluid(action.fluid); if (fluid == null) throw new IllegalStateException("Unknown fluid " + action.fluid); int requested = (int) Math.min(Integer.MAX_VALUE, action.value); @@ -728,15 +716,15 @@ private void applyActions(ServerWorld world, WorksDefinition.Station station, break; case "weatherClear": world.getLevelData().setRaining(false); - ((net.minecraft.world.storage.IServerWorldInfo) world.getLevelData()).setThundering(false); + ((net.minecraft.world.level.storage.ServerLevelData) world.getLevelData()).setThundering(false); break; case "weatherRain": world.getLevelData().setRaining(true); - ((net.minecraft.world.storage.IServerWorldInfo) world.getLevelData()).setThundering(false); + ((net.minecraft.world.level.storage.ServerLevelData) world.getLevelData()).setThundering(false); break; case "weatherThunder": world.getLevelData().setRaining(true); - ((net.minecraft.world.storage.IServerWorldInfo) world.getLevelData()).setThundering(true); + ((net.minecraft.world.level.storage.ServerLevelData) world.getLevelData()).setThundering(true); break; default: throw new IllegalArgumentException("Unsupported action type " + type); @@ -744,7 +732,7 @@ private void applyActions(ServerWorld world, WorksDefinition.Station station, } } - private void applyPlacement(ServerWorld world, WorksDefinition.Station station, + private void applyPlacement(ServerLevel world, WorksDefinition.Station station, WorksDefinition.Placement placement) { if (!requirementsPresent(placement.requiredMods)) return; Block target = block(placement.block); @@ -757,24 +745,24 @@ private void applyPlacement(ServerWorld world, WorksDefinition.Station station, if (placement.nbt != null) mergeTileNbt(world, pos, placement.nbt); } - private void mergeTileNbt(ServerWorld world, BlockPos pos, JsonObject supplied) { - TileEntity tile = world.getBlockEntity(pos); + 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 { - CompoundNBT base = tile.save(new CompoundNBT()); - CompoundNBT additions = JsonNbtConverter.toCompound(supplied); + CompoundTag base = tile.save(new CompoundTag()); + CompoundTag additions = JsonNbtConverter.toCompound(supplied); base.merge(additions); base.putInt("x", pos.getX()); base.putInt("y", pos.getY()); base.putInt("z", pos.getZ()); - tile.load(tile.getBlockState(), base); + tile.load(base); tile.setChanged(); } catch (Exception ex) { throw new IllegalStateException("Cannot merge tile NBT at " + pos, ex); } } - private List select(String target, boolean allowGroups) throws CommandException { + private List select(String target, boolean allowGroups) throws CommandRuntimeException { loadDefinition(); if ("all".equalsIgnoreCase(target)) return new ArrayList<>(stations.values()); if (allowGroups) { @@ -790,7 +778,7 @@ private List select(String target, boolean allowGroups) return Collections.singletonList(oneStation(target)); } - private WorksDefinition.Station oneStation(String id) throws CommandException { + private WorksDefinition.Station oneStation(String id) throws CommandRuntimeException { loadDefinition(); WorksDefinition.Station station = stations.get(id.toUpperCase(Locale.ROOT)); if (station == null) throw commandError("Unknown Advantage Works station " + id); @@ -826,29 +814,29 @@ private static int statusColor(String status) { return STATUS_ATTENTION; } - private static void setStatusBlock(ServerWorld world, WorksDefinition.Station station, int meta) { + private static void setStatusBlock(ServerLevel world, WorksDefinition.Station station, int meta) { world.setBlock(absolute(station, new int[]{11, 1, 1}), wool(meta).defaultBlockState(), 2); } - private static void placeCommandSign(ServerWorld world, BlockPos pos, String label, String command) { + private static void placeCommandSign(ServerLevel world, BlockPos pos, String label, String command) { world.setBlock(pos, Blocks.OAK_SIGN.defaultBlockState().setValue(StandingSignBlock.ROTATION, 8), 2); - TileEntity tile = world.getBlockEntity(pos); - if (!(tile instanceof SignTileEntity)) return; - SignTileEntity sign = (SignTileEntity) tile; - StringTextComponent clickable = new StringTextComponent(label); + BlockEntity tile = world.getBlockEntity(pos); + if (!(tile instanceof SignBlockEntity)) return; + SignBlockEntity sign = (SignBlockEntity) tile; + TextComponent clickable = new TextComponent(label); clickable.setStyle(Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, command))); sign.setMessage(0, clickable); - sign.setMessage(1, new StringTextComponent("right-click")); + sign.setMessage(1, new TextComponent("right-click")); sign.setChanged(); } - private static void placeSign(ServerWorld world, BlockPos pos, String... lines) { + private static void placeSign(ServerLevel world, BlockPos pos, String... lines) { world.setBlock(pos, Blocks.OAK_SIGN.defaultBlockState().setValue(StandingSignBlock.ROTATION, 8), 2); - TileEntity tile = world.getBlockEntity(pos); - if (!(tile instanceof SignTileEntity)) return; - SignTileEntity sign = (SignTileEntity) tile; + BlockEntity tile = world.getBlockEntity(pos); + if (!(tile instanceof SignBlockEntity)) return; + SignBlockEntity sign = (SignBlockEntity) tile; for (int i = 0; i < 4; i++) { - sign.setMessage(i, new StringTextComponent(i < lines.length ? trimSign(lines[i]) : "")); + sign.setMessage(i, new TextComponent(i < lines.length ? trimSign(lines[i]) : "")); } sign.setChanged(); } @@ -867,8 +855,8 @@ private static void tagEntity(Entity entity, String stationId) { entity.getPersistentData().putString("AdvantageWorksStation", stationId); } - private static ServerWorld overworld(MinecraftServer server) throws CommandException { - ServerWorld world = server.getLevel(World.OVERWORLD); + private static ServerLevel overworld(MinecraftServer server) throws CommandRuntimeException { + ServerLevel world = server.getLevel(Level.OVERWORLD); if (world == null) throw commandError("Overworld is not loaded"); return world; } @@ -889,17 +877,17 @@ private static Item item(String name) { return ForgeRegistries.ITEMS.containsKey(key) ? ForgeRegistries.ITEMS.getValue(key) : null; } - private static CommandException commandError(String message) { - return new CommandException(new StringTextComponent(message)); + private static CommandRuntimeException commandError(String message) { + return new CommandRuntimeException(new TextComponent(message)); } private static BlockState legacyState(String name, Block block, int metadata) { - if (name != null && (name.startsWith("poweradvantage:") - || name.startsWith("steamadvantage:") || name.startsWith("electricadvantage:"))) { + if (name != null && (name.startsWith("basepower:") + || name.startsWith("basesteam:") || name.startsWith("baseelectric:"))) { return LegacyMigrationApi.decodeBlockState(new ResourceLocation(name), block, metadata); } - if (block instanceof FlowingFluidBlock) { - return block.defaultBlockState().setValue(FlowingFluidBlock.LEVEL, Integer.valueOf(metadata & 15)); + if (block instanceof LiquidBlock) { + return block.defaultBlockState().setValue(LiquidBlock.LEVEL, Integer.valueOf(metadata & 15)); } return block.defaultBlockState(); } @@ -913,7 +901,7 @@ private static ItemStack itemStack(Item item, int count, int damage) { private static Fluid fluid(String name) { if (name == null || name.trim().isEmpty()) return null; ResourceLocation id = name.indexOf(':') >= 0 ? new ResourceLocation(name) - : new ResourceLocation("poweradvantage", name); + : new ResourceLocation("basepower", name); Fluid value = Registry.FLUID.getOptional(id).orElse(null); if (value == null) { String migratedName = LegacyMigrationApi.fluidTarget(name); 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 f3c8edb5..8ef7378a 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 @@ -3,7 +3,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; -import net.minecraft.util.ResourceLocation; +import net.minecraft.resources.ResourceLocation; import java.io.InputStream; import java.io.InputStreamReader; @@ -34,8 +34,8 @@ static WorksDefinition load() { return load(RESOURCE, "1.15.2"); } - static WorksDefinition loadNative116() { - return load("/assets/advantageworkstest/works/works-1.16.5.json", "1.16.5"); + static WorksDefinition loadNative117() { + return load("/assets/advantageworkstest/works/works-1.17.1.json", "1.17.1"); } static WorksDefinition load(String resource, String minecraft) { 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 c594dac7..f230b151 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.16.5"); + document.put("minecraft", "1.17.1"); 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 02a55d21..3f586b0b 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 @@ -4,7 +4,9 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.CompletableFuture; /** The original sample order, with a bounded batch and a return to the server between phases. */ final class WorldgenSamplingJob { @@ -12,12 +14,15 @@ final class WorldgenSamplingJob { static final int PLAN_BATCH = 64; static final int POPULATION_BATCH = 4; static final int COUNT_BATCH = 2; + static final int MAX_IN_FLIGHT = 4; private static final int SEARCH_SIDE = 129; interface WorldAccess { boolean matchesBiome(String expected, int x, int y, int z); - void populateChunk(int chunkX, int chunkZ); - SampleChunk sampleChunk(int chunkX, int chunkZ); + CompletableFuture requestChunk(int chunkX, int chunkZ, boolean counting); + void releaseChunk(int chunkX, int chunkZ); + void close(); + default Map diagnostics() { return java.util.Collections.emptyMap(); } } interface SampleChunk { @@ -34,7 +39,10 @@ void start(WorldgenSamplingJob job) { active = job; } WorldgenSamplingJob current() { return active; } - void release() { active = null; } + void release() { + try { if (active != null) active.close(); } + finally { active = null; } + } } private final WorldAccess world; @@ -46,6 +54,7 @@ void start(WorldgenSamplingJob job) { private final Map counts = new LinkedHashMap<>(); private final Map violations = new LinkedHashMap<>(); private final Map boundaryBlocks = new LinkedHashMap<>(); + private final Map> pending = new LinkedHashMap<>(); private Phase phase = Phase.SPAWN; private int squareX, squareZ, squareIndex, searchIndex; private int locatedX, locatedZ; @@ -53,6 +62,7 @@ void start(WorldgenSamplingJob job) { private double bestDistance = Double.POSITIVE_INFINITY; private Iterator iterator; private RuntimeException failure; + private boolean closed; WorldgenSamplingJob(WorldAccess world, int spawnX, int spawnY, int spawnZ, int seaLevel, int radius) { if (radius < 1 || radius > 16) throw new IllegalArgumentException("Worldgen radius must be 1..16"); @@ -65,10 +75,10 @@ void start(WorldgenSamplingJob job) { squareX = spawnX >> 4; squareZ = spawnZ >> 4; centers.put("spawn", squareX + "," + squareZ); - expectedBiomes.put("poweradvantage:crude_oil", "DESERT"); + expectedBiomes.put("basepower:crude_oil", "DESERT"); expectedBiomes.put("mineralogy:crude_oil", "OCEAN"); - expectedBiomes.put("electricadvantage:li_ore", null); - expectedBiomes.put("electricadvantage:sulfur_ore", null); + expectedBiomes.put("baseelectric:li_ore", null); + expectedBiomes.put("baseelectric:sulfur_ore", null); expectedBiomes.put("mineralogy:sulfur_ore", null); for (String id : expectedBiomes.keySet()) { counts.put(id, 0L); @@ -97,11 +107,34 @@ void tick() { failure = exception; phase = Phase.FAILED; } + if (finished()) cleanup(); } Phase phase() { return phase; } boolean finished() { return phase == Phase.DONE || phase == Phase.FAILED; } RuntimeException failure() { return failure; } + Map diagnostics() { return world.diagnostics(); } + + void close() { + if (!finished()) { + failure = new IllegalStateException("Worldgen sample cancelled before completion"); + phase = Phase.FAILED; + } + cleanup(); + } + + private void cleanup() { + if (closed) return; + closed = true; + for (CompletableFuture future : pending.values()) future.cancel(false); + pending.clear(); + try { world.close(); } + catch (RuntimeException exception) { + if (failure == null) failure = exception; + else if (failure != exception) failure.addSuppressed(exception); + phase = Phase.FAILED; + } + } Map result() { if (phase != Phase.DONE) throw new IllegalStateException("Worldgen sample is not complete", failure); @@ -171,40 +204,61 @@ private void planHalo() { } private void populate() { - for (int work = 0; work < POPULATION_BATCH && iterator.hasNext(); work++) { - long packed = iterator.next(); - world.populateChunk((int) (packed >> 32), (int) packed); - } - if (!iterator.hasNext()) { + advanceChunks(false); + if (!iterator.hasNext() && pending.isEmpty()) { iterator = chunks.iterator(); phase = Phase.COUNT; } } private void count() { - for (int work = 0; work < COUNT_BATCH && iterator.hasNext(); work++) { - long packed = iterator.next(); + advanceChunks(true); + if (!iterator.hasNext() && pending.isEmpty()) phase = Phase.DONE; + } + + private void advanceChunks(boolean counting) { + // Inspect only completed futures; the server tick must never wait for native lighting. + for (CompletableFuture future : pending.values()) { + if (future.isDone()) Objects.requireNonNull(future.getNow(null), "Native FULL chunk was lost"); + } + int budget = counting ? COUNT_BATCH : POPULATION_BATCH; + Iterator>> completed = pending.entrySet().iterator(); + for (int work = 0; work < budget && completed.hasNext(); work++) { + Map.Entry> entry = completed.next(); + if (!entry.getValue().isDone()) break; + long packed = entry.getKey(); int chunkX = (int) (packed >> 32), chunkZ = (int) packed; - SampleChunk chunk = world.sampleChunk(chunkX, 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++) { - int x = minX + localX, z = minZ + localZ; - String name = chunk.blockName(x, y, z); - if (name == null || !counts.containsKey(name)) continue; - counts.put(name, counts.get(name) + 1L); - String expected = expectedBiomes.get(name); - if (expected != null && !chunk.centerMatches(expected)) { - violations.put(name, violations.get(name) + 1L); - } else if (expected != null && !world.matchesBiome(expected, x, y, z)) { - boundaryBlocks.put(name, boundaryBlocks.get(name) + 1L); - } + SampleChunk chunk = Objects.requireNonNull(entry.getValue().getNow(null), "Native FULL chunk was lost"); + if (counting) countChunk(chunk, chunkX, chunkZ); + completed.remove(); + world.releaseChunk(chunkX, chunkZ); + } + for (int work = 0; work < budget && pending.size() < MAX_IN_FLIGHT && iterator.hasNext(); work++) { + long packed = iterator.next(); + CompletableFuture future = Objects.requireNonNull( + world.requestChunk((int) (packed >> 32), (int) packed, counting), "Native chunk request was null"); + pending.put(packed, future); + } + } + + 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++) { + int x = minX + localX, z = minZ + localZ; + String name = chunk.blockName(x, y, z); + if (name == null || !counts.containsKey(name)) continue; + counts.put(name, counts.get(name) + 1L); + String expected = expectedBiomes.get(name); + if (expected != null && !chunk.centerMatches(expected)) { + violations.put(name, violations.get(name) + 1L); + } else if (expected != null && !world.matchesBiome(expected, x, y, z)) { + boundaryBlocks.put(name, boundaryBlocks.get(name) + 1L); } } } } - if (!iterator.hasNext()) phase = Phase.DONE; } private static long pack(int x, int z) { return ((long) x << 32) ^ (z & 0xffffffffL); } diff --git a/test-rig/src/main/resources/META-INF/mods.toml b/test-rig/src/main/resources/META-INF/mods.toml index b8d96d59..34f720ba 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="[36,)" +loaderVersion="[37,)" license="LGPL-3.0-only" [[mods]] @@ -10,22 +10,22 @@ authors="Minecraft Mod Development" description='''Server-side commissioning harness for the Advantage family. Not for distribution.''' [[dependencies.advantageworkstest]] -modId="poweradvantage" +modId="basepower" mandatory=true -versionRange="[2.4.7.116051,)" +versionRange="[2.4.9.117011,)" ordering="AFTER" side="SERVER" [[dependencies.advantageworkstest]] modId="forge" mandatory=true -versionRange="[36.2.34,)" +versionRange="[37.1.1,)" ordering="NONE" side="SERVER" [[dependencies.advantageworkstest]] modId="minecraft" mandatory=true -versionRange="[1.16.5]" +versionRange="[1.17.1]" ordering="NONE" side="SERVER" diff --git a/test-rig/src/main/resources/assets/advantageworkstest/native117-helper-contract.json b/test-rig/src/main/resources/assets/advantageworkstest/native117-helper-contract.json new file mode 100644 index 00000000..1722650c --- /dev/null +++ b/test-rig/src/main/resources/assets/advantageworkstest/native117-helper-contract.json @@ -0,0 +1,22 @@ +{ + "format": 1, + "policy": "native117-leash-knot-nosave-v1", + "minecraftVersion": "1.17.1", + "dataVersion": 2730, + "forgeVersion": "37.1.1", + "nativeClass": "net.minecraft.world.entity.decoration.LeashFenceKnotEntity", + "registryId": "minecraft:leash_knot", + "canSerialize": false, + "rootWriterAccepted": false, + "observationalUuidPreserved": true, + "observationalStateUnchanged": true, + "noPower": true, + "noServer": true, + "noTicks": true, + "nativeMethodsUnmodified": true, + "nativeClassSha256": { + "net.minecraft.world.entity.Entity": "9E560B90A9988865F90C625B9E72CDF2C4362449A94ACE40537328B05F3B6BC8", + "net.minecraft.world.entity.EntityType": "FFDE4F3F334A1297BA4F32E0992932272E12062AD0FC3B5157C7EC3735171B5F", + "net.minecraft.world.entity.decoration.LeashFenceKnotEntity": "CC5911E096CDD811DFF4D49EAE979EE3BE0224437D7DA0241081AB78FF1546E9" + } +} diff --git a/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.13.2.json b/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.13.2.json index 6a3aaa3a..6230977c 100644 --- a/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.13.2.json +++ b/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.13.2.json @@ -42,7 +42,7 @@ ], "exclusive": false, "requiredMods": [ - "poweradvantage" + "basepower" ], "placements": [ { @@ -51,7 +51,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_drain", + "block": "basepower:fluid_drain", "meta": 0 }, { @@ -60,7 +60,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_pipe", + "block": "basepower:fluid_pipe", "meta": 0 }, { @@ -69,7 +69,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_storage_tank", + "block": "basepower:fluid_storage_tank", "meta": 0 }, { @@ -78,7 +78,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_pipe", + "block": "basepower:fluid_pipe", "meta": 0 }, { @@ -87,7 +87,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_switch", + "block": "basepower:fluid_switch", "meta": 0 }, { @@ -96,7 +96,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_pipe", + "block": "basepower:fluid_pipe", "meta": 0 }, { @@ -105,7 +105,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_metal_tank", + "block": "basepower:fluid_metal_tank", "meta": 0 }, { @@ -114,7 +114,7 @@ 1, 8 ], - "block": "poweradvantage:fluid_discharge", + "block": "basepower:fluid_discharge", "meta": 0 }, { @@ -123,7 +123,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_storage_tank", + "block": "basepower:fluid_storage_tank", "meta": 0 }, { @@ -132,7 +132,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_metal_tank", + "block": "basepower:fluid_metal_tank", "meta": 0 }, { @@ -141,7 +141,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_pipe_terminal", + "block": "basepower:fluid_pipe_terminal", "meta": 0 }, { @@ -150,7 +150,7 @@ 1, 7 ], - "block": "poweradvantage:steel_frame", + "block": "basepower:steel_frame", "meta": 0 }, { @@ -159,7 +159,7 @@ 2, 7 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 }, { @@ -168,7 +168,7 @@ 2, 7 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 }, { @@ -177,30 +177,30 @@ 2, 7 ], - "block": "poweradvantage:infinite_quantum", + "block": "basepower:infinite_quantum", "meta": 0 }, { "pos": [20, 1, 14], - "block": "poweradvantage:converter_rf_steam", + "block": "basepower:converter_rf_steam", "meta": 0, "optional": true }, { "pos": [21, 1, 14], - "block": "poweradvantage:converter_rf_electricity", + "block": "basepower:converter_rf_electricity", "meta": 0, "optional": true }, { "pos": [22, 1, 14], - "block": "poweradvantage:converter_rf_quantum", + "block": "basepower:converter_rf_quantum", "meta": 0, "optional": true }, { "pos": [23, 1, 14], - "block": "poweradvantage:converter_tr_electricity", + "block": "basepower:converter_tr_electricity", "meta": 0, "optional": true }, @@ -210,7 +210,7 @@ 1, 10 ], - "block": "poweradvantage:crude_oil", + "block": "basepower:crude_oil", "meta": 0 }, { @@ -219,7 +219,7 @@ 1, 10 ], - "block": "poweradvantage:refined_oil", + "block": "basepower:refined_oil", "meta": 0 }, { @@ -241,7 +241,7 @@ 24 ], "slot": 0, - "item": "poweradvantage:starch", + "item": "basepower:starch", "count": 16 }, { @@ -252,7 +252,7 @@ 24 ], "slot": 1, - "item": "poweradvantage:bioplastic_ingot", + "item": "basepower:bioplastic_ingot", "count": 16 }, { @@ -263,7 +263,7 @@ 24 ], "slot": 2, - "item": "poweradvantage:sprocket", + "item": "basepower:sprocket", "count": 16 }, { @@ -274,7 +274,7 @@ 24 ], "slot": 3, - "item": "poweradvantage:rotator_tool", + "item": "basepower:rotator_tool", "count": 1 }, { @@ -315,7 +315,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_drain" + "block": "basepower:fluid_drain" }, { "id": "portable-tank-tile", @@ -347,7 +347,7 @@ "id": "power-items", "type": "registryItem", "pos": null, - "item": "poweradvantage:rotator_tool" + "item": "basepower:rotator_tool" }, { "id": "orespawn-integration", @@ -360,28 +360,28 @@ "id": "rf-steam-converter", "type": "registryBlock", "pos": null, - "block": "poweradvantage:converter_rf_steam", + "block": "basepower:converter_rf_steam", "optional": true }, { "id": "rf-electric-converter", "type": "registryBlock", "pos": null, - "block": "poweradvantage:converter_rf_electricity", + "block": "basepower:converter_rf_electricity", "optional": true }, { "id": "rf-quantum-converter", "type": "registryBlock", "pos": null, - "block": "poweradvantage:converter_rf_quantum", + "block": "basepower:converter_rf_quantum", "optional": true }, { "id": "tech-reborn-converter", "type": "registryBlock", "pos": null, - "block": "poweradvantage:converter_tr_electricity", + "block": "basepower:converter_tr_electricity", "optional": true } ], @@ -415,7 +415,7 @@ ], "exclusive": true, "requiredMods": [ - "poweradvantage" + "basepower" ], "placements": [ { @@ -424,7 +424,7 @@ 1, 8 ], - "block": "poweradvantage:fluid_drain", + "block": "basepower:fluid_drain", "meta": 0 }, { @@ -433,7 +433,7 @@ 1, 8 ], - "block": "poweradvantage:fluid_pipe", + "block": "basepower:fluid_pipe", "meta": 0 }, { @@ -442,7 +442,7 @@ 1, 8 ], - "block": "poweradvantage:fluid_metal_tank", + "block": "basepower:fluid_metal_tank", "meta": 0 }, { @@ -451,7 +451,7 @@ 1, 8 ], - "block": "poweradvantage:fluid_metal_tank", + "block": "basepower:fluid_metal_tank", "meta": 0 }, { @@ -460,7 +460,7 @@ 1, 8 ], - "block": "poweradvantage:fluid_pipe", + "block": "basepower:fluid_pipe", "meta": 0 }, { @@ -469,7 +469,7 @@ 1, 8 ], - "block": "poweradvantage:fluid_discharge", + "block": "basepower:fluid_discharge", "meta": 0 }, { @@ -628,7 +628,7 @@ ], "exclusive": false, "requiredMods": [ - "poweradvantage" + "basepower" ], "placements": [ { @@ -637,7 +637,7 @@ 1, 8 ], - "block": "poweradvantage:fluid_storage_tank", + "block": "basepower:fluid_storage_tank", "meta": 0 }, { @@ -646,7 +646,7 @@ 1, 8 ], - "block": "poweradvantage:fluid_pipe", + "block": "basepower:fluid_pipe", "meta": 0 }, { @@ -655,7 +655,7 @@ 1, 8 ], - "block": "poweradvantage:still", + "block": "basepower:still", "meta": 0 }, { @@ -664,7 +664,7 @@ 1, 8 ], - "block": "poweradvantage:fluid_pipe", + "block": "basepower:fluid_pipe", "meta": 0 }, { @@ -673,7 +673,7 @@ 1, 8 ], - "block": "poweradvantage:fluid_metal_tank", + "block": "basepower:fluid_metal_tank", "meta": 0 }, { @@ -687,31 +687,31 @@ }, { "pos": [4, 1, 14], - "block": "poweradvantage:fluid_storage_tank", + "block": "basepower:fluid_storage_tank", "meta": 0, "requiredMods": ["mineralogy"] }, { "pos": [5, 1, 14], - "block": "poweradvantage:fluid_pipe", + "block": "basepower:fluid_pipe", "meta": 0, "requiredMods": ["mineralogy"] }, { "pos": [6, 1, 14], - "block": "poweradvantage:still", + "block": "basepower:still", "meta": 0, "requiredMods": ["mineralogy"] }, { "pos": [7, 1, 14], - "block": "poweradvantage:fluid_pipe", + "block": "basepower:fluid_pipe", "meta": 0, "requiredMods": ["mineralogy"] }, { "pos": [8, 1, 14], - "block": "poweradvantage:fluid_metal_tank", + "block": "basepower:fluid_metal_tank", "meta": 0, "requiredMods": ["mineralogy"] } @@ -733,19 +733,19 @@ { "type": "setFluid", "pos": [6, 1, 8], - "fluid": "poweradvantage:crude_oil", + "fluid": "basepower:crude_oil", "value": 0 }, { "type": "setFluid", "pos": [8, 1, 8], - "fluid": "poweradvantage:refined_oil", + "fluid": "basepower:refined_oil", "value": 0 }, { "type": "setFluid", "pos": [4, 1, 8], - "fluid": "poweradvantage:crude_oil", + "fluid": "basepower:crude_oil", "value": 2000 }, { @@ -769,7 +769,7 @@ { "type": "setFluid", "pos": [8, 1, 14], - "fluid": "poweradvantage:refined_oil", + "fluid": "basepower:refined_oil", "value": 0, "requiredMods": ["mineralogy"] }, @@ -851,7 +851,7 @@ "type": "fluidAmount", "positions": [[4, 1, 8], [6, 1, 8], [8, 1, 8]], "nbtPath": "Tank", - "fluid": "poweradvantage:refined_oil", + "fluid": "basepower:refined_oil", "value": 1000 }, { @@ -875,7 +875,7 @@ "type": "fluidAmount", "positions": [[4, 1, 14], [6, 1, 14], [8, 1, 14]], "nbtPath": "Tank", - "fluid": "poweradvantage:refined_oil", + "fluid": "basepower:refined_oil", "value": 1000, "requiredMods": ["mineralogy"] } @@ -909,7 +909,7 @@ ], "exclusive": false, "requiredMods": [ - "poweradvantage" + "basepower" ], "placements": [ { @@ -927,7 +927,7 @@ 1, 6 ], - "block": "poweradvantage:item_conveyor", + "block": "basepower:item_conveyor", "meta": 5 }, { @@ -936,7 +936,7 @@ 1, 6 ], - "block": "poweradvantage:item_conveyor", + "block": "basepower:item_conveyor", "meta": 5 }, { @@ -963,7 +963,7 @@ 1, 9 ], - "block": "poweradvantage:item_filter_inventory", + "block": "basepower:item_filter_inventory", "meta": 5 }, { @@ -999,7 +999,7 @@ 1, 12 ], - "block": "poweradvantage:item_filter_block", + "block": "basepower:item_filter_block", "meta": 5 }, { @@ -1008,7 +1008,7 @@ 1, 12 ], - "block": "poweradvantage:item_filter_food", + "block": "basepower:item_filter_food", "meta": 5 }, { @@ -1017,7 +1017,7 @@ 1, 12 ], - "block": "poweradvantage:item_filter_fuel", + "block": "basepower:item_filter_fuel", "meta": 5 }, { @@ -1026,7 +1026,7 @@ 1, 12 ], - "block": "poweradvantage:item_filter_ore", + "block": "basepower:item_filter_ore", "meta": 5 }, { @@ -1035,7 +1035,7 @@ 1, 12 ], - "block": "poweradvantage:item_filter_plant", + "block": "basepower:item_filter_plant", "meta": 5 }, { @@ -1044,7 +1044,7 @@ 1, 12 ], - "block": "poweradvantage:item_filter_smelt", + "block": "basepower:item_filter_smelt", "meta": 5 }, { @@ -1071,7 +1071,7 @@ 1, 9 ], - "block": "poweradvantage:item_filter_overflow", + "block": "basepower:item_filter_overflow", "meta": 5 }, { @@ -1107,7 +1107,7 @@ 1, 6 ], - "block": "poweradvantage:item_conveyor", + "block": "basepower:item_conveyor", "meta": 1 }, { @@ -1116,7 +1116,7 @@ 2, 6 ], - "block": "poweradvantage:item_conveyor", + "block": "basepower:item_conveyor", "meta": 5 }, { @@ -1134,7 +1134,7 @@ 1, 12 ], - "block": "poweradvantage:item_conveyor", + "block": "basepower:item_conveyor", "meta": 5 }, { @@ -1143,7 +1143,7 @@ 1, 12 ], - "block": "poweradvantage:item_conveyor", + "block": "basepower:item_conveyor", "meta": 5 }, { @@ -1162,7 +1162,7 @@ }, { "pos": [3, 1, 15], - "block": "poweradvantage:item_filter_inventory", + "block": "basepower:item_filter_inventory", "meta": 5 }, { @@ -1182,7 +1182,7 @@ }, { "pos": [3, 1, 18], - "block": "poweradvantage:item_filter_inventory", + "block": "basepower:item_filter_inventory", "meta": 5 }, { @@ -1232,7 +1232,7 @@ }, { "pos": [13, 1, 15], - "block": "poweradvantage:item_filter_overflow", + "block": "basepower:item_filter_overflow", "meta": 5 }, { @@ -1252,7 +1252,7 @@ }, { "pos": [13, 1, 18], - "block": "poweradvantage:item_filter_overflow", + "block": "basepower:item_filter_overflow", "meta": 5 }, { @@ -1272,7 +1272,7 @@ }, { "pos": [13, 1, 21], - "block": "poweradvantage:item_filter_overflow", + "block": "basepower:item_filter_overflow", "meta": 5 }, { @@ -1630,7 +1630,7 @@ 1, 6 ], - "block": "poweradvantage:item_conveyor", + "block": "basepower:item_conveyor", "meta": 5 }, { @@ -1720,8 +1720,8 @@ ], "exclusive": false, "requiredMods": [ - "poweradvantage", - "steamadvantage" + "basepower", + "basesteam" ], "placements": [ { @@ -1730,7 +1730,7 @@ 1, 7 ], - "block": "steamadvantage:steam_boiler_coal", + "block": "basesteam:steam_boiler_coal", "meta": 0 }, { @@ -1739,7 +1739,7 @@ 1, 7 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -1748,7 +1748,7 @@ 1, 7 ], - "block": "steamadvantage:steam_tank", + "block": "basesteam:steam_tank", "meta": 0 }, { @@ -1757,7 +1757,7 @@ 1, 12 ], - "block": "steamadvantage:steam_boiler_electric", + "block": "basesteam:steam_boiler_electric", "meta": 0 }, { @@ -1766,7 +1766,7 @@ 1, 12 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -1775,7 +1775,7 @@ 1, 12 ], - "block": "steamadvantage:steam_tank", + "block": "basesteam:steam_tank", "meta": 0 }, { @@ -1784,7 +1784,7 @@ 1, 7 ], - "block": "steamadvantage:steam_boiler_geothermal", + "block": "basesteam:steam_boiler_geothermal", "meta": 0 }, { @@ -1793,7 +1793,7 @@ 1, 7 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -1802,7 +1802,7 @@ 1, 7 ], - "block": "steamadvantage:steam_tank", + "block": "basesteam:steam_tank", "meta": 0 }, { @@ -1811,7 +1811,7 @@ 1, 12 ], - "block": "steamadvantage:steam_boiler_oil", + "block": "basesteam:steam_boiler_oil", "meta": 0 }, { @@ -1820,7 +1820,7 @@ 1, 12 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -1829,7 +1829,7 @@ 1, 12 ], - "block": "steamadvantage:steam_tank", + "block": "basesteam:steam_tank", "meta": 0 }, { @@ -1862,7 +1862,7 @@ 20 ], "slot": 0, - "item": "steamadvantage:steam_governor", + "item": "basesteam:steam_governor", "count": 4 }, { @@ -1886,7 +1886,7 @@ 1, 6 ], - "block": "poweradvantage:fluid_storage_tank", + "block": "basepower:fluid_storage_tank", "meta": 0 }, { @@ -1906,7 +1906,7 @@ 1, 13 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 }, { @@ -1926,7 +1926,7 @@ 1, 13 ], - "block": "poweradvantage:fluid_storage_tank", + "block": "basepower:fluid_storage_tank", "meta": 0 }, { @@ -2053,8 +2053,8 @@ ], "exclusive": false, "requiredMods": [ - "poweradvantage", - "steamadvantage" + "basepower", + "basesteam" ], "placements": [ { @@ -2063,7 +2063,7 @@ 1, 8 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -2072,7 +2072,7 @@ 1, 8 ], - "block": "steamadvantage:steam_tank", + "block": "basesteam:steam_tank", "meta": 0 }, { @@ -2081,7 +2081,7 @@ 1, 8 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -2090,7 +2090,7 @@ 1, 8 ], - "block": "steamadvantage:steam_furnace", + "block": "basesteam:steam_furnace", "meta": 0 }, { @@ -2099,7 +2099,7 @@ 1, 8 ], - "block": "steamadvantage:steam_drill", + "block": "basesteam:steam_drill", "meta": 1 }, { @@ -2108,7 +2108,7 @@ 1, 8 ], - "block": "steamadvantage:steam_crusher", + "block": "basesteam:steam_crusher", "meta": 0 }, { @@ -2117,7 +2117,7 @@ 1, 7 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -2126,7 +2126,7 @@ 1, 6 ], - "block": "steamadvantage:steam_switch", + "block": "basesteam:steam_switch", "meta": 0 }, { @@ -2135,7 +2135,7 @@ 1, 9 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -2144,7 +2144,7 @@ 1, 10 ], - "block": "steamadvantage:steam_drill", + "block": "basesteam:steam_drill", "meta": 3 }, { @@ -2163,7 +2163,7 @@ 1, 8 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -2172,7 +2172,7 @@ 1, 8 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -2181,7 +2181,7 @@ 1, 8 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -2190,7 +2190,7 @@ 1, 8 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -2199,7 +2199,7 @@ 1, 8 ], - "block": "steamadvantage:steam_tank", + "block": "basesteam:steam_tank", "meta": 0 } ], @@ -2212,7 +2212,7 @@ 1, 8 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 }, { @@ -2222,7 +2222,7 @@ 1, 8 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 } ], @@ -2253,7 +2253,7 @@ 1, 8 ], - "block": "steamadvantage:steam_pipe" + "block": "basesteam:steam_pipe" }, { "id": "steam-switch", @@ -2263,7 +2263,7 @@ 1, 6 ], - "block": "steamadvantage:steam_switch" + "block": "basesteam:steam_switch" }, { "id": "middle-drill", @@ -2305,8 +2305,8 @@ ], "exclusive": false, "requiredMods": [ - "poweradvantage", - "steamadvantage" + "basepower", + "basesteam" ], "placements": [ { @@ -2315,7 +2315,7 @@ 1, 8 ], - "block": "steamadvantage:steam_boiler_coal", + "block": "basesteam:steam_boiler_coal", "meta": 0 }, { @@ -2324,7 +2324,7 @@ 1, 8 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -2333,7 +2333,7 @@ 1, 8 ], - "block": "steamadvantage:steam_tank", + "block": "basesteam:steam_tank", "meta": 0 }, { @@ -2342,7 +2342,7 @@ 1, 8 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -2351,7 +2351,7 @@ 1, 8 ], - "block": "steamadvantage:steam_furnace", + "block": "basesteam:steam_furnace", "meta": 0 }, { @@ -2360,7 +2360,7 @@ 1, 8 ], - "block": "steamadvantage:steam_crusher", + "block": "basesteam:steam_crusher", "meta": 0 }, { @@ -2369,7 +2369,7 @@ 1, 10 ], - "block": "steamadvantage:steam_switch", + "block": "basesteam:steam_switch", "meta": 0 } ], @@ -2394,7 +2394,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_storage_tank", + "block": "basepower:fluid_storage_tank", "meta": 0 }, { @@ -2475,8 +2475,8 @@ ], "exclusive": false, "requiredMods": [ - "poweradvantage", - "steamadvantage" + "basepower", + "basesteam" ], "placements": [ { @@ -2494,7 +2494,7 @@ 1, 7 ], - "block": "steamadvantage:steam_furnace", + "block": "basesteam:steam_furnace", "meta": 0 }, { @@ -2512,7 +2512,7 @@ 1, 6 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -2530,7 +2530,7 @@ 1, 13 ], - "block": "steamadvantage:steam_crusher", + "block": "basesteam:steam_crusher", "meta": 0 }, { @@ -2548,7 +2548,7 @@ 1, 12 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -2557,7 +2557,7 @@ 1, 13 ], - "block": "steamadvantage:steam_still", + "block": "basesteam:steam_still", "meta": 0 }, { @@ -2566,7 +2566,7 @@ 1, 13 ], - "block": "steamadvantage:steam_crusher", + "block": "basesteam:steam_crusher", "meta": 0 } ], @@ -2591,7 +2591,7 @@ 1, 5 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 }, { @@ -2601,7 +2601,7 @@ 1, 11 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 }, { @@ -2611,7 +2611,7 @@ 1, 12 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 }, { @@ -2739,8 +2739,8 @@ "exclusive": true, "enclosed": true, "requiredMods": [ - "poweradvantage", - "steamadvantage" + "basepower", + "basesteam" ], "placements": [ { @@ -2749,7 +2749,7 @@ 2, 6 ], - "block": "steamadvantage:steam_drill", + "block": "basesteam:steam_drill", "meta": 0 }, { @@ -2758,7 +2758,7 @@ 2, 6 ], - "block": "steamadvantage:steam_drill", + "block": "basesteam:steam_drill", "meta": 1 }, { @@ -2767,7 +2767,7 @@ 2, 6 ], - "block": "steamadvantage:steam_drill", + "block": "basesteam:steam_drill", "meta": 2 }, { @@ -2776,7 +2776,7 @@ 2, 6 ], - "block": "steamadvantage:steam_drill", + "block": "basesteam:steam_drill", "meta": 3 }, { @@ -2785,7 +2785,7 @@ 2, 6 ], - "block": "steamadvantage:steam_drill", + "block": "basesteam:steam_drill", "meta": 4 }, { @@ -2794,7 +2794,7 @@ 2, 6 ], - "block": "steamadvantage:steam_drill", + "block": "basesteam:steam_drill", "meta": 5 }, { @@ -2803,7 +2803,7 @@ 3, 6 ], - "block": "steamadvantage:steam_track", + "block": "basesteam:steam_track", "meta": 0 }, { @@ -2812,7 +2812,7 @@ 1, 6 ], - "block": "steamadvantage:steam_track", + "block": "basesteam:steam_track", "meta": 0 }, { @@ -2821,7 +2821,7 @@ 2, 7 ], - "block": "steamadvantage:steam_track", + "block": "basesteam:steam_track", "meta": 0 }, { @@ -2830,7 +2830,7 @@ 2, 5 ], - "block": "steamadvantage:steam_track", + "block": "basesteam:steam_track", "meta": 0 }, { @@ -2839,7 +2839,7 @@ 2, 6 ], - "block": "steamadvantage:steam_track", + "block": "basesteam:steam_track", "meta": 0 }, { @@ -2848,7 +2848,7 @@ 2, 6 ], - "block": "steamadvantage:steam_track", + "block": "basesteam:steam_track", "meta": 0 }, { @@ -2857,7 +2857,7 @@ 1, 6 ], - "block": "steamadvantage:drillbit", + "block": "basesteam:drillbit", "meta": 0 }, { @@ -2866,7 +2866,7 @@ 3, 6 ], - "block": "steamadvantage:drillbit", + "block": "basesteam:drillbit", "meta": 1 }, { @@ -2875,7 +2875,7 @@ 2, 5 ], - "block": "steamadvantage:drillbit", + "block": "basesteam:drillbit", "meta": 2 }, { @@ -2884,7 +2884,7 @@ 2, 7 ], - "block": "steamadvantage:drillbit", + "block": "basesteam:drillbit", "meta": 3 }, { @@ -2893,7 +2893,7 @@ 2, 6 ], - "block": "steamadvantage:drillbit", + "block": "basesteam:drillbit", "meta": 4 }, { @@ -2902,7 +2902,7 @@ 2, 6 ], - "block": "steamadvantage:drillbit", + "block": "basesteam:drillbit", "meta": 5 }, { @@ -2969,7 +2969,7 @@ 2, 7 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 }, { @@ -2979,7 +2979,7 @@ 2, 7 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 }, { @@ -2989,7 +2989,7 @@ 2, 9 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 }, { @@ -2999,7 +2999,7 @@ 2, 3 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 }, { @@ -3009,7 +3009,7 @@ 2, 7 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 }, { @@ -3019,7 +3019,7 @@ 2, 7 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 } ], @@ -3082,7 +3082,7 @@ 2, 6 ], - "block": "steamadvantage:steam_drill", + "block": "basesteam:steam_drill", "meta": 0 }, { @@ -3093,7 +3093,7 @@ 2, 6 ], - "block": "steamadvantage:steam_drill", + "block": "basesteam:steam_drill", "meta": 1 }, { @@ -3104,7 +3104,7 @@ 2, 6 ], - "block": "steamadvantage:steam_drill", + "block": "basesteam:steam_drill", "meta": 2 }, { @@ -3115,7 +3115,7 @@ 2, 6 ], - "block": "steamadvantage:steam_drill", + "block": "basesteam:steam_drill", "meta": 3 }, { @@ -3126,7 +3126,7 @@ 2, 6 ], - "block": "steamadvantage:steam_drill", + "block": "basesteam:steam_drill", "meta": 4 }, { @@ -3137,7 +3137,7 @@ 2, 6 ], - "block": "steamadvantage:steam_drill", + "block": "basesteam:steam_drill", "meta": 5 } ], @@ -3172,8 +3172,8 @@ "exclusive": true, "enclosed": true, "requiredMods": [ - "poweradvantage", - "steamadvantage" + "basepower", + "basesteam" ], "placements": [ { @@ -3182,7 +3182,7 @@ 5, 10 ], - "block": "steamadvantage:steam_pump", + "block": "basesteam:steam_pump", "meta": 0 }, { @@ -3191,7 +3191,7 @@ 4, 10 ], - "block": "steamadvantage:pump_pipe_steam", + "block": "basesteam:pump_pipe_steam", "meta": 0 }, { @@ -3200,7 +3200,7 @@ 3, 10 ], - "block": "steamadvantage:pump_pipe_steam", + "block": "basesteam:pump_pipe_steam", "meta": 0 }, { @@ -3209,7 +3209,7 @@ 5, 10 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -3218,7 +3218,7 @@ 5, 10 ], - "block": "poweradvantage:fluid_metal_tank", + "block": "basepower:fluid_metal_tank", "meta": 0 }, { @@ -3227,7 +3227,7 @@ 5, 20 ], - "block": "steamadvantage:steam_pump", + "block": "basesteam:steam_pump", "meta": 0 }, { @@ -3449,7 +3449,7 @@ 5, 11 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 } ], @@ -3490,7 +3490,7 @@ 4, 10 ], - "block": "steamadvantage:pump_pipe_steam" + "block": "basesteam:pump_pipe_steam" }, { "id": "pump-fluid-persisted", @@ -3534,8 +3534,8 @@ "exclusive": true, "enclosed": true, "requiredMods": [ - "poweradvantage", - "steamadvantage" + "basepower", + "basesteam" ], "placements": [ { @@ -3544,7 +3544,7 @@ 1, 12 ], - "block": "steamadvantage:steam_elevator", + "block": "basesteam:steam_elevator", "meta": 0 }, { @@ -3553,7 +3553,7 @@ 1, 12 ], - "block": "steamadvantage:steam_pipe", + "block": "basesteam:steam_pipe", "meta": 0 }, { @@ -3629,7 +3629,7 @@ 1, 12 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 } ], @@ -3661,7 +3661,7 @@ 2, 12 ], - "block": "steamadvantage:platform" + "block": "basesteam:platform" } ], "manualChecks": [ @@ -3694,8 +3694,8 @@ "exclusive": true, "enclosed": true, "requiredMods": [ - "poweradvantage", - "steamadvantage" + "basepower", + "basesteam" ], "placements": [ { @@ -3807,7 +3807,7 @@ 5 ], "slot": 0, - "item": "steamadvantage:musket", + "item": "basesteam:musket", "count": 1 }, { @@ -3818,7 +3818,7 @@ 5 ], "slot": 1, - "item": "steamadvantage:musket", + "item": "basesteam:musket", "count": 1, "nbt": { "loaded": 1 @@ -3832,7 +3832,7 @@ 5 ], "slot": 2, - "item": "steamadvantage:blackpowder_cartridge", + "item": "basesteam:blackpowder_cartridge", "count": 64 }, { @@ -3843,7 +3843,7 @@ 5 ], "slot": 3, - "item": "steamadvantage:steam_governor", + "item": "basesteam:steam_governor", "count": 8 }, { @@ -3854,61 +3854,61 @@ 5 ], "slot": 4, - "item": "steamadvantage:steam_drill_bit", + "item": "basesteam:steam_drill_bit", "count": 8 }, { "type": "setInventory", "pos": [3, 1, 5], "slot": 5, - "item": "steamadvantage:musket", + "item": "basesteam:musket", "count": 1, - "enchantment": "steamadvantage:rapid_reload", + "enchantment": "basesteam:rapid_reload", "level": 1 }, { "type": "setInventory", "pos": [3, 1, 5], "slot": 6, - "item": "steamadvantage:musket", + "item": "basesteam:musket", "count": 1, - "enchantment": "steamadvantage:powderless", + "enchantment": "basesteam:powderless", "level": 1 }, { "type": "setInventory", "pos": [3, 1, 5], "slot": 7, - "item": "steamadvantage:musket", + "item": "basesteam:musket", "count": 1, - "enchantment": "steamadvantage:high_explosive", + "enchantment": "basesteam:high_explosive", "level": 1 }, { "type": "setInventory", "pos": [3, 1, 5], "slot": 8, - "item": "steamadvantage:musket", + "item": "basesteam:musket", "count": 1, - "enchantment": "steamadvantage:high_explosive", + "enchantment": "basesteam:high_explosive", "level": 5 }, { "type": "setInventory", "pos": [3, 1, 5], "slot": 9, - "item": "steamadvantage:musket", + "item": "basesteam:musket", "count": 1, - "enchantment": "steamadvantage:recoil", + "enchantment": "basesteam:recoil", "level": 1 }, { "type": "setInventory", "pos": [3, 1, 5], "slot": 10, - "item": "steamadvantage:musket", + "item": "basesteam:musket", "count": 1, - "enchantment": "steamadvantage:recoil", + "enchantment": "basesteam:recoil", "level": 5 } ], @@ -3919,13 +3919,13 @@ "id": "musket-item", "type": "registryItem", "pos": null, - "item": "steamadvantage:musket" + "item": "basesteam:musket" }, { "id": "cartridge-item", "type": "registryItem", "pos": null, - "item": "steamadvantage:blackpowder_cartridge" + "item": "basesteam:blackpowder_cartridge" }, { "id": "range-chest", @@ -3940,7 +3940,7 @@ "id": "stocked-muskets", "type": "inventoryCount", "pos": [3, 1, 5], - "item": "steamadvantage:musket", + "item": "basesteam:musket", "count": 8, "operator": "min" } @@ -3975,8 +3975,8 @@ "exclusive": true, "enclosed": true, "requiredMods": [ - "poweradvantage", - "electricadvantage" + "basepower", + "baseelectric" ], "placements": [ { @@ -3985,7 +3985,7 @@ 1, 7 ], - "block": "electricadvantage:photovoltaic_generator", + "block": "baseelectric:photovoltaic_generator", "meta": 0 }, { @@ -3994,7 +3994,7 @@ 1, 7 ], - "block": "electricadvantage:electric_conduit", + "block": "baseelectric:electric_conduit", "meta": 0 }, { @@ -4003,7 +4003,7 @@ 1, 7 ], - "block": "electricadvantage:electric_battery_array", + "block": "baseelectric:electric_battery_array", "meta": 0 }, { @@ -4228,7 +4228,7 @@ 4, 12 ], - "block": "electricadvantage:hydroelectric_generator", + "block": "baseelectric:hydroelectric_generator", "meta": 0 }, { @@ -4237,7 +4237,7 @@ 4, 12 ], - "block": "electricadvantage:electric_battery_array", + "block": "baseelectric:electric_battery_array", "meta": 0 }, { @@ -4246,7 +4246,7 @@ 1, 7 ], - "block": "electricadvantage:steam_powered_generator", + "block": "baseelectric:steam_powered_generator", "meta": 0 }, { @@ -4255,7 +4255,7 @@ 1, 7 ], - "block": "electricadvantage:electric_conduit", + "block": "baseelectric:electric_conduit", "meta": 0 }, { @@ -4264,7 +4264,7 @@ 1, 7 ], - "block": "electricadvantage:electric_battery_array", + "block": "baseelectric:electric_battery_array", "meta": 0 } ], @@ -4276,7 +4276,7 @@ 1, 7 ], - "item": "electricadvantage:lithium_battery", + "item": "baseelectric:lithium_battery", "slot": 0, "count": 1, "nbt": { @@ -4290,7 +4290,7 @@ 4, 12 ], - "item": "electricadvantage:lithium_battery", + "item": "baseelectric:lithium_battery", "slot": 0, "count": 1, "nbt": { @@ -4304,7 +4304,7 @@ 1, 7 ], - "item": "electricadvantage:lithium_battery", + "item": "baseelectric:lithium_battery", "slot": 0, "count": 1, "nbt": { @@ -4328,7 +4328,7 @@ 4, 12 ], - "item": "electricadvantage:lithium_battery", + "item": "baseelectric:lithium_battery", "slot": 0, "count": 1, "nbt": { @@ -4342,7 +4342,7 @@ 1, 7 ], - "block": "poweradvantage:infinite_steam", + "block": "basepower:infinite_steam", "meta": 0 }, { @@ -4400,7 +4400,7 @@ 4, 12 ], - "block": "electricadvantage:hydroelectric_generator", + "block": "baseelectric:hydroelectric_generator", "meta": 10 }, { @@ -4455,8 +4455,8 @@ ], "exclusive": false, "requiredMods": [ - "poweradvantage", - "electricadvantage" + "basepower", + "baseelectric" ], "placements": [ { @@ -4465,7 +4465,7 @@ 1, 7 ], - "block": "electricadvantage:electric_battery_array", + "block": "baseelectric:electric_battery_array", "meta": 0 }, { @@ -4474,7 +4474,7 @@ 1, 7 ], - "block": "electricadvantage:electric_conduit", + "block": "baseelectric:electric_conduit", "meta": 0 }, { @@ -4483,7 +4483,7 @@ 1, 7 ], - "block": "electricadvantage:electric_switch", + "block": "baseelectric:electric_switch", "meta": 0 }, { @@ -4492,7 +4492,7 @@ 1, 7 ], - "block": "electricadvantage:electric_conduit", + "block": "baseelectric:electric_conduit", "meta": 0 }, { @@ -4501,7 +4501,7 @@ 1, 7 ], - "block": "electricadvantage:electric_crusher", + "block": "baseelectric:electric_crusher", "meta": 0 }, { @@ -4510,7 +4510,7 @@ 1, 7 ], - "block": "electricadvantage:electric_furnace", + "block": "baseelectric:electric_furnace", "meta": 0 }, { @@ -4519,7 +4519,7 @@ 1, 7 ], - "block": "electricadvantage:electric_fabricator", + "block": "baseelectric:electric_fabricator", "meta": 0 }, { @@ -4541,7 +4541,7 @@ 7 ], "slot": 0, - "item": "electricadvantage:lead_acid_battery", + "item": "baseelectric:lead_acid_battery", "count": 1, "nbt": { "energy": 0 @@ -4555,7 +4555,7 @@ 7 ], "slot": 1, - "item": "electricadvantage:nickel_hydride_battery", + "item": "baseelectric:nickel_hydride_battery", "count": 1, "nbt": { "energy": 250 @@ -4569,7 +4569,7 @@ 7 ], "slot": 2, - "item": "electricadvantage:alkaline_battery", + "item": "baseelectric:alkaline_battery", "count": 1 }, { @@ -4580,7 +4580,7 @@ 7 ], "slot": 3, - "item": "electricadvantage:lithium_battery", + "item": "baseelectric:lithium_battery", "count": 1 }, { @@ -4591,7 +4591,7 @@ 12 ], "slot": 0, - "item": "electricadvantage:blank_circuit_board", + "item": "baseelectric:blank_circuit_board", "count": 8 }, { @@ -4602,7 +4602,7 @@ 12 ], "slot": 1, - "item": "electricadvantage:control_circuit", + "item": "baseelectric:control_circuit", "count": 8 }, { @@ -4613,7 +4613,7 @@ 12 ], "slot": 2, - "item": "electricadvantage:integrated_circuit", + "item": "baseelectric:integrated_circuit", "count": 8 }, { @@ -4624,7 +4624,7 @@ 12 ], "slot": 3, - "item": "electricadvantage:psu", + "item": "baseelectric:psu", "count": 8 }, { @@ -4635,7 +4635,7 @@ 12 ], "slot": 4, - "item": "electricadvantage:silicon_ingot", + "item": "baseelectric:silicon_ingot", "count": 8 }, { @@ -4646,7 +4646,7 @@ 12 ], "slot": 5, - "item": "electricadvantage:silicon_mix", + "item": "baseelectric:silicon_mix", "count": 8 }, { @@ -4657,7 +4657,7 @@ 12 ], "slot": 6, - "item": "electricadvantage:solder_mix", + "item": "baseelectric:solder_mix", "count": 8 }, { @@ -4668,7 +4668,7 @@ 12 ], "slot": 7, - "item": "electricadvantage:solder", + "item": "baseelectric:solder", "count": 8 }, { @@ -4679,7 +4679,7 @@ 12 ], "slot": 8, - "item": "electricadvantage:li_ingot", + "item": "baseelectric:li_ingot", "count": 8 }, { @@ -4690,7 +4690,7 @@ 12 ], "slot": 9, - "item": "electricadvantage:li_powder", + "item": "baseelectric:li_powder", "count": 8 }, { @@ -4701,7 +4701,7 @@ 12 ], "slot": 10, - "item": "electricadvantage:li_nugget", + "item": "baseelectric:li_nugget", "count": 8 }, { @@ -4712,7 +4712,7 @@ 12 ], "slot": 11, - "item": "electricadvantage:sulfur_powder", + "item": "baseelectric:sulfur_powder", "count": 8 }, { @@ -4723,14 +4723,14 @@ 12 ], "slot": 12, - "item": "electricadvantage:petrolplastic_ingot", + "item": "baseelectric:petrolplastic_ingot", "count": 8 }, { "type": "setInventory", "pos": [3, 1, 12], "slot": 13, - "item": "electricadvantage:lead_acid_battery", + "item": "baseelectric:lead_acid_battery", "count": 1, "nbt": {"energy": 0} }, @@ -4738,7 +4738,7 @@ "type": "setInventory", "pos": [3, 1, 12], "slot": 14, - "item": "electricadvantage:lead_acid_battery", + "item": "baseelectric:lead_acid_battery", "count": 1, "nbt": {"energy": 500} }, @@ -4746,14 +4746,14 @@ "type": "setInventory", "pos": [3, 1, 12], "slot": 15, - "item": "electricadvantage:lead_acid_battery", + "item": "baseelectric:lead_acid_battery", "count": 1 }, { "type": "setInventory", "pos": [3, 1, 12], "slot": 16, - "item": "electricadvantage:nickel_hydride_battery", + "item": "baseelectric:nickel_hydride_battery", "count": 1, "nbt": {"energy": 0} }, @@ -4761,7 +4761,7 @@ "type": "setInventory", "pos": [3, 1, 12], "slot": 17, - "item": "electricadvantage:nickel_hydride_battery", + "item": "baseelectric:nickel_hydride_battery", "count": 1, "nbt": {"energy": 1000} }, @@ -4769,14 +4769,14 @@ "type": "setInventory", "pos": [3, 1, 12], "slot": 18, - "item": "electricadvantage:nickel_hydride_battery", + "item": "baseelectric:nickel_hydride_battery", "count": 1 }, { "type": "setInventory", "pos": [3, 1, 12], "slot": 19, - "item": "electricadvantage:alkaline_battery", + "item": "baseelectric:alkaline_battery", "count": 1, "nbt": {"energy": 0} }, @@ -4784,7 +4784,7 @@ "type": "setInventory", "pos": [3, 1, 12], "slot": 20, - "item": "electricadvantage:alkaline_battery", + "item": "baseelectric:alkaline_battery", "count": 1, "nbt": {"energy": 2000} }, @@ -4792,14 +4792,14 @@ "type": "setInventory", "pos": [3, 1, 12], "slot": 21, - "item": "electricadvantage:alkaline_battery", + "item": "baseelectric:alkaline_battery", "count": 1 }, { "type": "setInventory", "pos": [3, 1, 12], "slot": 22, - "item": "electricadvantage:lithium_battery", + "item": "baseelectric:lithium_battery", "count": 1, "nbt": {"energy": 0} }, @@ -4807,7 +4807,7 @@ "type": "setInventory", "pos": [3, 1, 12], "slot": 23, - "item": "electricadvantage:lithium_battery", + "item": "baseelectric:lithium_battery", "count": 1, "nbt": {"energy": 4000} }, @@ -4815,7 +4815,7 @@ "type": "setInventory", "pos": [3, 1, 12], "slot": 24, - "item": "electricadvantage:lithium_battery", + "item": "baseelectric:lithium_battery", "count": 1 } ], @@ -4827,7 +4827,7 @@ 1, 7 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 } ], @@ -4859,13 +4859,13 @@ 1, 7 ], - "block": "electricadvantage:electric_switch" + "block": "baseelectric:electric_switch" }, { "id": "lithium-battery", "type": "registryItem", "pos": null, - "item": "electricadvantage:lithium_battery" + "item": "baseelectric:lithium_battery" }, { "id": "lead-empty-nbt", @@ -4933,8 +4933,8 @@ ], "exclusive": false, "requiredMods": [ - "poweradvantage", - "electricadvantage" + "basepower", + "baseelectric" ], "placements": [ { @@ -4943,7 +4943,7 @@ 1, 6 ], - "block": "electricadvantage:electric_crusher", + "block": "baseelectric:electric_crusher", "meta": 0 }, { @@ -4952,7 +4952,7 @@ 1, 6 ], - "block": "electricadvantage:electric_furnace", + "block": "baseelectric:electric_furnace", "meta": 0 }, { @@ -4961,7 +4961,7 @@ 1, 6 ], - "block": "electricadvantage:electric_oven", + "block": "baseelectric:electric_oven", "meta": 0 }, { @@ -4970,7 +4970,7 @@ 1, 6 ], - "block": "electricadvantage:electric_fabricator", + "block": "baseelectric:electric_fabricator", "meta": 0 }, { @@ -4988,7 +4988,7 @@ 1, 10 ], - "block": "poweradvantage:item_conveyor", + "block": "basepower:item_conveyor", "meta": 5 }, { @@ -5006,7 +5006,7 @@ 1, 14 ], - "block": "electricadvantage:li_ore", + "block": "baseelectric:li_ore", "meta": 0 }, { @@ -5015,7 +5015,7 @@ 1, 14 ], - "block": "electricadvantage:sulfur_ore", + "block": "baseelectric:sulfur_ore", "meta": 0 } ], @@ -5028,7 +5028,7 @@ 1, 5 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 }, { @@ -5038,7 +5038,7 @@ 1, 5 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 }, { @@ -5048,7 +5048,7 @@ 1, 5 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 }, { @@ -5058,7 +5058,7 @@ 1, 5 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 }, { @@ -5185,8 +5185,8 @@ ], "exclusive": true, "requiredMods": [ - "poweradvantage", - "electricadvantage" + "basepower", + "baseelectric" ], "placements": [ { @@ -5195,7 +5195,7 @@ 5, 7 ], - "block": "electricadvantage:electric_pump", + "block": "baseelectric:electric_pump", "meta": 0 }, { @@ -5204,7 +5204,7 @@ 4, 7 ], - "block": "electricadvantage:pump_pipe_electric", + "block": "baseelectric:pump_pipe_electric", "meta": 0 }, { @@ -5213,7 +5213,7 @@ 5, 7 ], - "block": "electricadvantage:electric_conduit", + "block": "baseelectric:electric_conduit", "meta": 0 }, { @@ -5222,7 +5222,7 @@ 5, 7 ], - "block": "poweradvantage:fluid_metal_tank", + "block": "basepower:fluid_metal_tank", "meta": 0 }, { @@ -5231,7 +5231,7 @@ 1, 7 ], - "block": "electricadvantage:electric_still", + "block": "baseelectric:electric_still", "meta": 0 }, { @@ -5240,7 +5240,7 @@ 1, 7 ], - "block": "electricadvantage:plastic_refinery", + "block": "baseelectric:plastic_refinery", "meta": 0 }, { @@ -5249,7 +5249,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_storage_tank", + "block": "basepower:fluid_storage_tank", "meta": 0 }, { @@ -5258,7 +5258,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_storage_tank", + "block": "basepower:fluid_storage_tank", "meta": 0 }, { @@ -5267,7 +5267,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_storage_tank", + "block": "basepower:fluid_storage_tank", "meta": 0 }, { @@ -5320,7 +5320,7 @@ 5, 6 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 }, { @@ -5330,7 +5330,7 @@ 1, 6 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 }, { @@ -5340,7 +5340,7 @@ 1, 6 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 } ], @@ -5414,7 +5414,7 @@ 4, 7 ], - "block": "electricadvantage:pump_pipe_electric" + "block": "baseelectric:pump_pipe_electric" } ], "manualChecks": [ @@ -5446,8 +5446,8 @@ ], "exclusive": false, "requiredMods": [ - "poweradvantage", - "electricadvantage" + "basepower", + "baseelectric" ], "placements": [ { @@ -5456,7 +5456,7 @@ 1, 8 ], - "block": "electricadvantage:growth_chamber_controller", + "block": "baseelectric:growth_chamber_controller", "meta": 0 }, { @@ -5465,7 +5465,7 @@ 1, 8 ], - "block": "electricadvantage:growth_chamber", + "block": "baseelectric:growth_chamber", "meta": 0 }, { @@ -5474,7 +5474,7 @@ 1, 8 ], - "block": "electricadvantage:growth_chamber", + "block": "baseelectric:growth_chamber", "meta": 0 }, { @@ -5483,7 +5483,7 @@ 1, 8 ], - "block": "electricadvantage:growth_chamber", + "block": "baseelectric:growth_chamber", "meta": 0 }, { @@ -5492,7 +5492,7 @@ 1, 9 ], - "block": "electricadvantage:growth_chamber", + "block": "baseelectric:growth_chamber", "meta": 0 }, { @@ -5501,7 +5501,7 @@ 1, 7 ], - "block": "electricadvantage:electric_conduit", + "block": "baseelectric:electric_conduit", "meta": 0 }, { @@ -5510,7 +5510,7 @@ 1, 8 ], - "block": "poweradvantage:fluid_storage_tank", + "block": "basepower:fluid_storage_tank", "meta": 0 }, { @@ -5587,7 +5587,7 @@ 1, 7 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 } ], @@ -5660,8 +5660,8 @@ "exclusive": true, "enclosed": true, "requiredMods": [ - "poweradvantage", - "electricadvantage" + "basepower", + "baseelectric" ], "placements": [ { @@ -5670,7 +5670,7 @@ 2, 6 ], - "block": "electricadvantage:electric_drill", + "block": "baseelectric:electric_drill", "property": "facing", "expectedText": "down" }, @@ -5680,7 +5680,7 @@ 2, 6 ], - "block": "electricadvantage:electric_drill", + "block": "baseelectric:electric_drill", "meta": 1 }, { @@ -5689,7 +5689,7 @@ 2, 6 ], - "block": "electricadvantage:electric_drill", + "block": "baseelectric:electric_drill", "meta": 2 }, { @@ -5698,7 +5698,7 @@ 2, 6 ], - "block": "electricadvantage:electric_drill", + "block": "baseelectric:electric_drill", "meta": 3 }, { @@ -5707,7 +5707,7 @@ 2, 6 ], - "block": "electricadvantage:electric_drill", + "block": "baseelectric:electric_drill", "meta": 4 }, { @@ -5716,7 +5716,7 @@ 2, 6 ], - "block": "electricadvantage:electric_drill", + "block": "baseelectric:electric_drill", "meta": 5 }, { @@ -5725,7 +5725,7 @@ 3, 6 ], - "block": "electricadvantage:electric_track", + "block": "baseelectric:electric_track", "meta": 0 }, { @@ -5734,7 +5734,7 @@ 1, 6 ], - "block": "electricadvantage:electric_track", + "block": "baseelectric:electric_track", "meta": 0 }, { @@ -5743,7 +5743,7 @@ 2, 7 ], - "block": "electricadvantage:electric_track", + "block": "baseelectric:electric_track", "meta": 0 }, { @@ -5752,7 +5752,7 @@ 2, 5 ], - "block": "electricadvantage:electric_track", + "block": "baseelectric:electric_track", "meta": 0 }, { @@ -5761,7 +5761,7 @@ 2, 6 ], - "block": "electricadvantage:electric_track", + "block": "baseelectric:electric_track", "meta": 0 }, { @@ -5770,7 +5770,7 @@ 2, 6 ], - "block": "electricadvantage:electric_track", + "block": "baseelectric:electric_track", "meta": 0 }, { @@ -5837,7 +5837,7 @@ 2, 7 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 }, { @@ -5847,7 +5847,7 @@ 2, 7 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 }, { @@ -5857,7 +5857,7 @@ 2, 9 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 }, { @@ -5867,7 +5867,7 @@ 2, 3 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 }, { @@ -5877,7 +5877,7 @@ 2, 7 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 }, { @@ -5887,7 +5887,7 @@ 2, 7 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 } ], @@ -5950,7 +5950,7 @@ 2, 6 ], - "block": "electricadvantage:electric_drill", + "block": "baseelectric:electric_drill", "property": "facing", "expectedText": "down" }, @@ -5962,7 +5962,7 @@ 2, 6 ], - "block": "electricadvantage:electric_drill", + "block": "baseelectric:electric_drill", "property": "facing", "expectedText": "up" }, @@ -5974,7 +5974,7 @@ 2, 6 ], - "block": "electricadvantage:electric_drill", + "block": "baseelectric:electric_drill", "property": "facing", "expectedText": "north" }, @@ -5986,7 +5986,7 @@ 2, 6 ], - "block": "electricadvantage:electric_drill", + "block": "baseelectric:electric_drill", "property": "facing", "expectedText": "south" }, @@ -5998,7 +5998,7 @@ 2, 6 ], - "block": "electricadvantage:electric_drill", + "block": "baseelectric:electric_drill", "property": "facing", "expectedText": "west" }, @@ -6010,7 +6010,7 @@ 2, 6 ], - "block": "electricadvantage:electric_drill", + "block": "baseelectric:electric_drill", "property": "facing", "expectedText": "east" } @@ -6046,8 +6046,8 @@ "exclusive": true, "enclosed": true, "requiredMods": [ - "poweradvantage", - "electricadvantage" + "basepower", + "baseelectric" ], "placements": [ { @@ -6056,7 +6056,7 @@ 1, 7 ], - "block": "electricadvantage:led_bar", + "block": "baseelectric:led_bar", "meta": 0 }, { @@ -6065,7 +6065,7 @@ 1, 7 ], - "block": "electricadvantage:electric_conduit", + "block": "baseelectric:electric_conduit", "meta": 0 }, { @@ -6074,7 +6074,7 @@ 1, 7 ], - "block": "electricadvantage:electric_switch", + "block": "baseelectric:electric_switch", "meta": 0 }, { @@ -6083,7 +6083,7 @@ 1, 7 ], - "block": "electricadvantage:laser_turret", + "block": "baseelectric:laser_turret", "meta": 0 }, { @@ -6092,7 +6092,7 @@ 1, 7 ], - "block": "electricadvantage:electric_conduit", + "block": "baseelectric:electric_conduit", "meta": 0 }, { @@ -6101,7 +6101,7 @@ 1, 20 ], - "block": "electricadvantage:laser_turret_evil", + "block": "baseelectric:laser_turret_evil", "meta": 0 }, { @@ -6159,7 +6159,7 @@ 1, 7 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 }, { @@ -6169,7 +6169,7 @@ 1, 7 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 } ], @@ -6249,8 +6249,8 @@ ], "exclusive": true, "requiredMods": [ - "poweradvantage", - "electricadvantage" + "basepower", + "baseelectric" ], "placements": [ { @@ -6259,7 +6259,7 @@ 1, 10 ], - "block": "electricadvantage:electric_fabricator", + "block": "baseelectric:electric_fabricator", "meta": 0 }, { @@ -6268,7 +6268,7 @@ 1, 10 ], - "block": "electricadvantage:electric_conduit", + "block": "baseelectric:electric_conduit", "meta": 0 }, { @@ -6358,7 +6358,7 @@ 1, 9 ], - "block": "poweradvantage:infinite_electricity", + "block": "basepower:infinite_electricity", "meta": 0 } ], @@ -6431,7 +6431,7 @@ ], "exclusive": false, "requiredMods": [ - "poweradvantage" + "basepower" ], "placements": [ { @@ -6440,7 +6440,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_pipe", + "block": "basepower:fluid_pipe", "meta": 0 }, { @@ -6449,7 +6449,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_storage_tank", + "block": "basepower:fluid_storage_tank", "meta": 0 }, { @@ -6523,7 +6523,7 @@ 1, 7 ], - "block": "poweradvantage:fluid_pipe" + "block": "basepower:fluid_pipe" }, { "id": "api-tank-filled", diff --git a/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.17.1.json b/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.17.1.json new file mode 100644 index 00000000..d95358b4 --- /dev/null +++ b/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.17.1.json @@ -0,0 +1,6 @@ +{ + "format": 1, + "minecraft": "1.17.1", + "title": "Advantage Works 1.17.1 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 45d57c9f..420e95ee 100644 --- a/test-rig/src/main/resources/pack.mcmeta +++ b/test-rig/src/main/resources/pack.mcmeta @@ -1,6 +1,6 @@ { "pack": { - "description": "Advantage Works 1.16.5 test resources", - "pack_format": 6 + "description": "Advantage Works 1.17.1 test resources", + "pack_format": 7 } } 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 adb15922..9e24ff31 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 = readNative116Profile(arguments.profile); + Profile profile = readNative117Profile(arguments.profile); if (arguments.serverPort != null) profile.serverPort = arguments.serverPort; File testRig = findTestRig(arguments.profile); File powerRoot = testRig.getParentFile().getCanonicalFile(); @@ -76,7 +76,11 @@ public static void main(String[] args) throws Exception { profile.preparedSeed = preparedServerSeed(testRig, profile, arguments); if (arguments.validateOnly) { List> inputs = stageMods(testRig, null, profile.mods); - System.out.println("Native 1.16.5 profile preflight only (no world opened): " + GSON.toJson(inputs)); + if ("packaged".equals(profile.mode)) { + 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)); return; } String stamp = timestamp(); @@ -106,7 +110,7 @@ public static void main(String[] args) throws Exception { System.out.println("Advantage Works runtime: " + runtime); try { - Process first = launch(profile, powerRoot, runtime, results); + Process first = launch(profile, powerRoot, runtime, results, "first-run"); File latestLog = new File(runtime, "logs/latest.log"); waitForReady(first, latestLog, profile.startupTimeoutSeconds); if (profile.buildWorks) { @@ -142,9 +146,8 @@ public static void main(String[] args) throws Exception { if (profile.worldgenSampleRadius > 0) requireExitUntil(first, firstShutdownDeadline); else requireExit(first, profile.shutdownTimeoutSeconds); auditPreparedServer(profile, runtime, "first-run-after"); - requireNoCommandFailures(latestLog, "first-run"); + archiveBoot(profile, "first-run"); if (profile.buildWorks) requireAcceptableStations(results, lifecycleStations); - copyIfPresent(latestLog, new File(results, "first-run.log")); File firstWorldgen = new File(results, "worldgen-first-run.json"); if (profile.worldgenSampleRadius > 0) { copyIfPresent(new File(results, "worldgen-sample.json"), firstWorldgen); @@ -154,11 +157,9 @@ public static void main(String[] args) throws Exception { profile.firstProfileAssertions); requireFileAssertions(runtime, results, "first-run", profile.firstFileAssertions); applyProfileMutations(runtime, profile); - if (latestLog.exists() && !latestLog.delete()) { - throw new IllegalStateException("Cannot clear first-run log before restart: " + latestLog); - } + profile.boots.get(0).logs.clearLiveLogs(); - Process second = launch(profile, powerRoot, runtime, results); + Process second = launch(profile, powerRoot, runtime, results, "restart-run"); waitForReady(second, latestLog, profile.startupTimeoutSeconds); Thread.sleep(stationRunSeconds * 1000L); if (profile.buildWorks) { @@ -178,8 +179,7 @@ public static void main(String[] args) throws Exception { if (profile.worldgenSampleRadius > 0) requireExitUntil(second, secondShutdownDeadline); else requireExit(second, profile.shutdownTimeoutSeconds); auditPreparedServer(profile, runtime, "restart-run-after"); - requireNoCommandFailures(latestLog, "restart-run"); - copyIfPresent(latestLog, new File(results, "restart-run.log")); + archiveBoot(profile, "restart-run"); File restartWorldgen = new File(results, "worldgen-restart-run.json"); if (profile.worldgenSampleRadius > 0) { copyIfPresent(new File(results, "worldgen-sample.json"), restartWorldgen); @@ -198,6 +198,7 @@ public static void main(String[] args) throws Exception { } requireAcceptableStations(results, lifecycleStations); } + for (Boot boot : profile.boots) boot.logs.requireArchiveUnchanged(); System.out.println("Automated assertion checks complete; manual procedures remain unverified: " + results); return; } @@ -209,15 +210,29 @@ public static void main(String[] args) throws Exception { input.start(); requireExit(first, 0); auditPreparedServer(profile, runtime, "interactive-after"); + archiveBoot(profile, "first-run"); + for (Boot boot : profile.boots) boot.logs.requireArchiveUnchanged(); } catch (Exception failure) { if (profile.preparedSeed != null) { profile.preparedSeed.closeAfterFailure(profile, runtime, failure); + } else { + closeBootsAfterFailure(profile, failure); + } + for (Boot boot : profile.boots) { + if (!boot.logs.archived) { + try { + if (boot.process != null && boot.process.isAlive()) { + throw new IllegalStateException("Cannot archive a running server's logs"); + } + boot.logs.archive(); + } catch (Exception logFailure) { failure.addSuppressed(logFailure); } + } } throw failure; } } - private static Process launch(Profile profile, File powerRoot, File runtime, File results) throws Exception { + private static Process launch(Profile profile, File powerRoot, File runtime, File results, String phase) throws Exception { if (profile.preparedSeed != null) { auditPreparedServer(profile, runtime, "boot-" + (profile.preparedSeed.processes.size() + 1) + "-before"); } @@ -232,17 +247,19 @@ private static Process launch(Profile profile, File powerRoot, File runtime, Fil command.add("--no-daemon"); workingDirectory = powerRoot; } else { - if (profile.launchCommand != null && !profile.launchCommand.isEmpty()) { + if ("1.17.1".equals(profile.minecraftVersion)) { + command.addAll(packagedCommand(profile, runtime)); + } else if (profile.launchCommand != null && !profile.launchCommand.isEmpty()) { for (String token : profile.launchCommand) { command.add(expand(token).replace("${runtime}", runtime.getAbsolutePath()) .replace("${powerRoot}", powerRoot.getAbsolutePath())); } } else { - command.add(javaExecutable(java8Home(profile)).getAbsolutePath()); + command.add(javaExecutable(runtimeJavaHome(profile)).getAbsolutePath()); command.add("-Xms512M"); command.add("-Xmx2G"); command.add("-jar"); - command.add("forge-" + profile.forge.version + ".jar"); + command.add(serverLauncherPath(profile)); command.add("nogui"); } workingDirectory = runtime; @@ -251,14 +268,16 @@ private static Process launch(Profile profile, File powerRoot, File runtime, Fil ProcessBuilder builder = new ProcessBuilder(command); builder.directory(workingDirectory); builder.redirectErrorStream(true); + Boot boot = new Boot(new BootLogs(runtime, results, phase)); + profile.boots.add(boot); + builder.redirectOutput(boot.logs.console); builder.environment().put("JAVA_HOME", "development".equals(profile.mode) - ? gradleJavaHome(profile).getAbsolutePath() : java8Home(profile).getAbsolutePath()); + ? gradleJavaHome(profile).getAbsolutePath() : runtimeJavaHome(profile).getAbsolutePath()); builder.environment().put("ADVANTAGE_WORKS_RUNTIME", runtime.getAbsolutePath()); Process process = builder.start(); + boot.process = process; if (profile.preparedSeed != null) profile.preparedSeed.processes.add(process); - Thread output = new Thread(() -> drainOutput(process), "advantage-works-server-output"); - output.setDaemon(true); - output.start(); + System.out.println("Server console: " + boot.logs.console); Runtime.getRuntime().addShutdownHook(new Thread(() -> { if (process.isAlive()) { try { @@ -272,6 +291,221 @@ private static Process launch(Profile profile, File powerRoot, File runtime, Fil return process; } + 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"); + } + File java = javaExecutable(runtimeJavaHome(profile)); + String launcher = serverLauncherPath(profile); + requireModularLauncher(runtime, launcher); + File loggingJar = new File(runtime, "libraries/net/minecraftforge/fmlloader/" + + profile.forge.version + "/fmlloader-" + profile.forge.version + ".jar"); + PreparedServerSeed.requirePlain(loggingJar.toPath()); + try (ZipFile jar = new ZipFile(loggingJar)) { + if (jar.getEntry("log4j2.xml") == null) throw new IllegalStateException("Forge logging configuration missing"); + } + return Arrays.asList(java.getAbsolutePath(), "-Xms512M", "-Xmx2G", "--add-opens", + "java.base/java.lang.invoke=ALL-UNNAMED", + "-Dlog4j.configurationFile=jar:" + loggingJar.toURI().toASCIIString() + "!/log4j2.xml", + "-Dfml.queryResult=confirm", "@" + launcher, "nogui"); + } + + static void requireModularLauncher(File runtime, String launcher) throws Exception { + Path root = runtime.toPath().toAbsolutePath().normalize(); + PreparedServerSeed.requirePlain(root); + Path args = root.resolve(launcher); + PreparedServerSeed.requirePlain(args); + 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")); + Set seen = new LinkedHashSet<>(); + for (String line : Files.readAllLines(args, StandardCharsets.UTF_8)) { + if (line.isEmpty()) continue; + String key = line; + String paths = null; + if (line.startsWith("-p ")) { key = "module-path"; paths = line.substring(3); } + else if (line.startsWith("-DlegacyClassPath=")) { key = "legacy-class-path"; paths = line.substring(18); } + else if (line.matches("-DignoreList=[A-Za-z0-9_.+-]+(?:,[A-Za-z0-9_.+-]+)*")) key = "ignore-list"; + else if (!expected.contains(line)) throw new IllegalArgumentException("Unexpected Forge modular argument: " + line); + if (!seen.add(key)) throw new IllegalArgumentException("Duplicate Forge modular argument: " + key); + if (paths != null) { + String separator = launcher.endsWith("win_args.txt") ? ";" : ":"; + for (String path : paths.split(separator, -1)) { + if (!path.matches("libraries/[A-Za-z0-9_.+/-]+\\.jar") + || Arrays.asList(path.split("/", -1)).stream().anyMatch(p -> p.isEmpty() || p.equals(".") || p.equals(".."))) { + throw new IllegalArgumentException("Escaping or non-library Forge code path: " + path); + } + Path code = root.resolve(path); + PreparedServerSeed.requirePlain(code); + if (!Files.isRegularFile(code)) throw new IllegalArgumentException("Missing Forge code: " + path); + } + } + } + expected.addAll(Arrays.asList("module-path", "legacy-class-path", "ignore-list")); + if (!seen.equals(expected)) throw new IllegalArgumentException("Incomplete Forge 37 modular arguments"); + } + + private static void archiveBoot(Profile profile, String phase) throws Exception { + for (Boot boot : profile.boots) { + if (boot.logs.phase.equals(phase)) { + if (boot.process.isAlive()) throw new IllegalStateException("Cannot archive a running server's logs"); + boot.logs.archive(); + return; + } + } + throw new IllegalStateException("Missing boot record: " + phase); + } + + private static void closeBootsAfterFailure(Profile profile, Exception primary) { + for (Boot boot : profile.boots) { + if (boot.process == null) continue; + try { + withWorldgenFailureCleanup(boot.process, + System.nanoTime() + TimeUnit.SECONDS.toNanos(Math.max(1, Math.min(300, profile.shutdownTimeoutSeconds))), + System::nanoTime, () -> { throw primary; }); + } catch (Exception failure) { + if (failure != primary) primary.addSuppressed(failure); + } + } + } + + static final class Boot { + final BootLogs logs; + Process process; + Boot(BootLogs logs) { this.logs = logs; } + } + + /** Captures each process separately; no earlier boot can supply a later boot's evidence. */ + static final class BootLogs { + 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|" + + "^\\s+at\\s+\\S+\\([^\\r\\n]*\\)\\s*$|^\\s*\\.\\.\\.\\s+\\d+\\s+more\\s*$"); + // Pinned Mixin 0.8.4 catches this cleanup IOException and logs only its message at DEBUG. + static final Pattern MIXIN_DEBUG_CLEANUP = Pattern.compile( + "^\\[(?:[0-9]{2}:[0-9]{2}:[0-9]{2}|[0-9]{2}[A-Za-z]{3}[0-9]{4} " + + "[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3})\\] " + + "\\[main/DEBUG\\] \\[mixin/\\]: Error cleaning class output directory: \\.mixin\\.out$"); + static final String MIXIN_DEBUG_REASON = "Mixin 0.8.4 ExtensionClassExporter catches debug-export " + + "cleanup IOException and logs its message at DEBUG"; + final File runtime, results, console; + final String phase; + final Map archivedHashes = new LinkedHashMap<>(); + String receiptSha256; + boolean archived, clean; + + BootLogs(File runtime, File results, String phase) throws Exception { + if (!phase.matches("[a-z0-9-]+")) throw new IllegalArgumentException("Unsafe boot phase"); + this.runtime = runtime; + this.results = results; + this.phase = phase; + PreparedServerSeed.requirePlain(runtime.toPath()); + PreparedServerSeed.requirePlain(results.toPath()); + Path logDirectory = new File(runtime, "logs").toPath(); + if (Files.exists(logDirectory, java.nio.file.LinkOption.NOFOLLOW_LINKS)) PreparedServerSeed.requirePlain(logDirectory); + console = new File(results, phase + "-console.log"); + for (String name : Arrays.asList("latest", "debug")) { + if (Files.exists(new File(runtime, "logs/" + name + ".log").toPath(), java.nio.file.LinkOption.NOFOLLOW_LINKS)) { + throw new IllegalStateException("Stale live log before boot: " + name); + } + if (Files.exists(new File(results, phase + "-" + name + ".log").toPath())) { + throw new IllegalStateException("Existing archived boot log: " + name); + } + } + if (new File(results, phase + "-logs.json").exists()) throw new IllegalStateException("Existing boot log receipt"); + Files.createFile(console.toPath()); + } + + void archive() throws Exception { + if (archived) throw new IllegalStateException("Boot logs already archived: " + phase); + archived = true; + List failures = new ArrayList<>(); + List> logs = new ArrayList<>(); + for (String kind : Arrays.asList("console", "latest", "debug")) { + File source = kind.equals("console") ? console : new File(runtime, "logs/" + kind + ".log"); + File target = new File(results, phase + "-" + kind + ".log"); + Map row = new LinkedHashMap<>(); + row.put("kind", kind); + row.put("file", target.getName()); + try { + PreparedServerSeed.requirePlain(source.toPath()); + if (!source.equals(target)) Files.copy(source.toPath(), target.toPath()); + row.put("size", Files.size(target.toPath())); + String hash = sha256(target); + row.put("sha256", hash); + archivedHashes.put(kind, hash); + boolean ready = false, stopped = false; + int count = 0; + List> nonFatalDiagnostics = new ArrayList<>(); + try (BufferedReader reader = Files.newBufferedReader(target.toPath(), StandardCharsets.UTF_8)) { + String line; + while ((line = reader.readLine()) != null) { + ready |= line.contains("Done ("); + stopped |= line.contains("Stopping server"); + if (MIXIN_DEBUG_CLEANUP.matcher(line).matches()) { + Map diagnostic = new LinkedHashMap<>(); + diagnostic.put("line", line); + diagnostic.put("reason", MIXIN_DEBUG_REASON); + nonFatalDiagnostics.add(diagnostic); + } else if (ERROR.matcher(line).find()) { + if (count++ < 5) failures.add(kind + ": " + line); + } + } + } + if (!ready || !stopped) failures.add(kind + ": missing startup/shutdown markers"); + row.put("errorLines", count); + row.put("nonFatalDiagnostics", nonFatalDiagnostics); + row.put("ready", ready); + row.put("stopped", stopped); + } catch (Exception failure) { + row.put("failure", failure.toString()); + failures.add(kind + ": " + failure); + } + logs.add(row); + } + clean = failures.isEmpty(); + Map receipt = new LinkedHashMap<>(); + receipt.put("phase", phase); + receipt.put("status", clean ? "PASS" : "FAIL"); + receipt.put("logs", logs); + receipt.put("failures", failures); + Files.write(new File(results, phase + "-logs.json").toPath(), + GSON.toJson(receipt).getBytes(StandardCharsets.UTF_8), java.nio.file.StandardOpenOption.CREATE_NEW); + receiptSha256 = sha256(new File(results, phase + "-logs.json")); + if (!clean) throw new IllegalStateException("Boot log validation failed for " + phase + ": " + failures); + } + + void requireArchiveUnchanged() throws Exception { + if (!archived || !clean) throw new IllegalStateException("Unverified boot logs"); + for (String kind : Arrays.asList("console", "latest", "debug")) { + File archivedLog = new File(results, phase + "-" + kind + ".log"); + PreparedServerSeed.requirePlain(archivedLog.toPath()); + if (!archivedHashes.get(kind).equals(sha256(archivedLog))) { + throw new IllegalStateException("Archived log changed: " + kind); + } + } + File receipt = new File(results, phase + "-logs.json"); + PreparedServerSeed.requirePlain(receipt.toPath()); + if (!receiptSha256.equals(sha256(receipt))) throw new IllegalStateException("Boot log receipt changed"); + } + + void clearLiveLogs() throws Exception { + requireArchiveUnchanged(); + for (String name : Arrays.asList("latest", "debug")) { + File live = new File(runtime, "logs/" + name + ".log"); + PreparedServerSeed.requirePlain(live.toPath()); + if (!sha256(live).equals(archivedHashes.get(name))) { + throw new IllegalStateException("Log changed after archive: " + name); + } + } + for (String name : Arrays.asList("latest", "debug")) Files.delete(new File(runtime, "logs/" + name + ".log").toPath()); + } + } + private static void waitForReady(Process process, File log, int timeoutSeconds) throws Exception { long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds); while (System.nanoTime() < deadline) { @@ -494,13 +728,6 @@ private static void verifyModIdentity(File jar, String expectedId, String expect } } - private static void requireNoCommandFailures(File log, String phase) { - if (contains(log, "Advantage Works command failed")) { - throw new IllegalStateException("Advantage Works command failed during " + phase - + "; inspect " + log); - } - } - private static String tomlString(String section, String key) { Matcher matcher = Pattern.compile("(?m)^\\s*" + Pattern.quote(key) + "\\s*=\\s*\"([^\"]*)\"\\s*(?:#.*)?$").matcher(section); @@ -520,7 +747,9 @@ static PreparedServerSeed preparedServerSeed(File testRig, Profile profile, Argu } return new PreparedServerSeed(preparedPath(testRig, arguments.preparedServerSeed), preparedPath(testRig, arguments.preparedServerSeedInventory), arguments.preparedServerSeedSha256, - "forge-" + profile.forge.version + ".jar", "minecraft_server." + profile.minecraftVersion + ".jar"); + serverLauncherPath(profile), "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"); } private static Path preparedPath(File base, File path) { @@ -551,7 +780,7 @@ static void auditPreparedServer(Profile profile, File runtime, String phase) thr } static final class PreparedServerSeed { - static final String INVENTORY = "native116-code-inventory.json"; + static final String INVENTORY = "native117-runner-code-inventory.json"; static final int MAX_INVENTORY_BYTES = 1024 * 1024; static final int MAX_FILES = 4096; static final long MAX_CODE_BYTES = 2L * 1024 * 1024 * 1024; @@ -712,7 +941,8 @@ void stage(File runtime) throws Exception { verifySource(); Path target = runtime.toPath().toAbsolutePath().normalize(); requirePlain(target); - if (Files.exists(target.resolve("libraries")) || Files.exists(target.resolve(INVENTORY))) { + if (Files.exists(target.resolve("libraries"), java.nio.file.LinkOption.NOFOLLOW_LINKS) + || Files.exists(target.resolve(INVENTORY), java.nio.file.LinkOption.NOFOLLOW_LINKS)) { throw new IllegalStateException("Prepared runtime already contains code or an inventory"); } File[] existing = runtime.listFiles((dir, name) -> name.toLowerCase(Locale.ROOT).endsWith(".jar")); @@ -793,11 +1023,11 @@ static void preparePackagedServer(File testRig, File runtime, Profile profile) t if (!profile.forge.sha256.equalsIgnoreCase(actual)) { throw new IllegalStateException("Forge installer SHA-256 mismatch: " + actual); } - File launcher = new File(runtime, "forge-" + profile.forge.version + ".jar"); + File launcher = new File(runtime, serverLauncherPath(profile)); if (launcher.isFile()) return; ProcessBuilder builder = new ProcessBuilder( - javaExecutable(java8Home(profile)).getAbsolutePath(), + javaExecutable(runtimeJavaHome(profile)).getAbsolutePath(), "-jar", installer.getAbsolutePath(), "--installServer"); builder.directory(runtime); builder.redirectErrorStream(true); @@ -822,6 +1052,31 @@ 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"; + 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 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"); + File home = configured == null || configured.trim().isEmpty() + ? new File(System.getProperty("java.home")) : new File(expand(configured)); + File release = new File(home, "release"); + java.util.Properties metadata = new java.util.Properties(); + 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); + } + return home.getCanonicalFile(); + } + private static File java8Home(Profile profile) throws Exception { String configured = profile.java8Home; if (configured == null || configured.trim().isEmpty()) { @@ -950,6 +1205,19 @@ static Profile readProfile(File file) throws Exception { return readProfile(file, "1.15.2", "1.15.2-31.2.57"); } + static Profile readNative117Profile(File file) throws Exception { + Profile profile = readProfile(file, "1.17.1", "1.17.1-37.1.1"); + if (!"UNQUALIFIED".equals(profile.qualificationStatus)) { + throw new IllegalArgumentException("Native117 destination must start UNQUALIFIED"); + } + for (ModInput input : profile.mods) { + if (input.version == null || !input.version.endsWith(".117011")) { + throw new IllegalArgumentException("Non-native117 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)) { @@ -987,6 +1255,10 @@ 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) + && profile.launchCommand != null && !profile.launchCommand.isEmpty()) { + throw new IllegalArgumentException("Native117 packaged launches cannot override the pinned command"); + } for (ModInput input : profile.mods) { if (input.sha256 == null || !input.sha256.matches("[A-Fa-f0-9]{64}")) { throw new IllegalStateException("Pending native destination SHA-256 pin: " + input.id); @@ -1460,13 +1732,13 @@ else if ("--server-port".equals(args[i]) && i + 1 < args.length) { static final class Profile { String name; String mode; - String runtimeBase = "build/works-native116"; + String runtimeBase = "build/works-native117"; int serverPort = 25565; int startupTimeoutSeconds = 120; int shutdownTimeoutSeconds = 60; int stationRunSeconds = 5; String levelName = "AdvantageWorks"; - String levelSeed = "advantage-works-1.16.5-v1"; + String levelSeed = "advantage-works-1.17.1-v1"; String levelType = "advantageworkstest:test_flat"; String generatorSettings = ""; boolean buildWorks = true; @@ -1474,6 +1746,7 @@ static final class Profile { boolean requireStableWorldgenCounts = true; List stableWorldgenCountExclusions = new ArrayList<>(); String java8Home; + String java16Home; String gradleJavaHome; String recipeMode = "NORMAL"; String sourceCommit; @@ -1485,6 +1758,7 @@ static final class Profile { String legacySourceProfile; ForgeInput forge; transient PreparedServerSeed preparedSeed; + transient List boots = new ArrayList<>(); List launchCommand = new ArrayList<>(); List mods = new ArrayList<>(); List configFixtures = new ArrayList<>(); diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/AssertionChecksTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/AssertionChecksTest.java index 10f73fbf..01ec7955 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/AssertionChecksTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/AssertionChecksTest.java @@ -3,7 +3,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Map; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.CompoundTag; import org.junit.Test; import static org.junit.Assert.*; @@ -27,24 +27,24 @@ public class AssertionChecksTest { } @Test(expected = IllegalArgumentException.class) public void missingTankCannotSatisfyZero() { - AssertionChecks.fluidAmount(new CompoundNBT(), "Tank", null); + AssertionChecks.fluidAmount(new CompoundTag(), "Tank", null); } @Test(expected = IllegalArgumentException.class) public void missingAmountCannotSatisfyZero() { - CompoundNBT tile = new CompoundNBT(); - tile.put("Tank", new CompoundNBT()); + CompoundTag tile = new CompoundTag(); + tile.put("Tank", new CompoundTag()); AssertionChecks.fluidAmount(tile, "Tank", null); } @Test(expected = IllegalArgumentException.class) public void emptyFalseIsNotAnEmptyTank() { - CompoundNBT tile = new CompoundNBT(), tank = new CompoundNBT(); + CompoundTag tile = new CompoundTag(), tank = new CompoundTag(); tank.putBoolean("Empty", false); tile.put("Tank", tank); AssertionChecks.fluidAmount(tile, "Tank", null); } @Test public void explicitEmptyAndMeasuredFluidAreDifferentFromMissingEvidence() { - CompoundNBT tile = new CompoundNBT(), tank = new CompoundNBT(); + CompoundTag tile = new CompoundTag(), tank = new CompoundTag(); tank.putBoolean("Empty", true); tile.put("Tank", tank); assertEquals(0, AssertionChecks.fluidAmount(tile, "Tank", null)); @@ -52,7 +52,7 @@ public class AssertionChecksTest { tank.putInt("Amount", 1000); tank.putString("FluidName", "minecraft:water"); assertEquals(1000, AssertionChecks.fluidAmount(tile, "Tank", "minecraft:water")); - assertEquals(0, AssertionChecks.fluidAmount(tile, "Tank", "poweradvantage:crude_oil")); + assertEquals(0, AssertionChecks.fluidAmount(tile, "Tank", "basepower:crude_oil")); } private Map row(String status) { return Collections.singletonMap("status", status); } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/AsyncWorldgenChunksTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/AsyncWorldgenChunksTest.java new file mode 100644 index 00000000..79ebcf16 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/AsyncWorldgenChunksTest.java @@ -0,0 +1,147 @@ +package zone.moddev.mc.advantageworks; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import static org.junit.Assert.*; + +public class AsyncWorldgenChunksTest { + @Test public void nativeDispatchIsOffOwnerThreadButTicketsStayOnOwnerThread() throws Exception { + Backend backend = new Backend(4); + ExecutorService executor = Executors.newSingleThreadExecutor(); + AsyncWorldgenChunks chunks = new AsyncWorldgenChunks<>(backend, executor); + try { + for (int x = 0; x < 4; x++) assertFalse(chunks.request(x, -1).isDone()); + assertTrue(backend.entered.await(5, TimeUnit.SECONDS)); + assertEquals(4, backend.retains); + assertEquals(4, chunks.diagnostics().get("peakInFlight")); + try { chunks.request(4, -1); fail("Fifth native request was allowed"); } + catch (IllegalStateException expected) { assertTrue(expected.getMessage().contains("excess")); } + try { chunks.request(0, -1); fail("Duplicate native request was allowed"); } + catch (IllegalStateException expected) { assertTrue(expected.getMessage().contains("Duplicate")); } + chunks.release(0, -1); + assertFalse("Shared native futures must not be cancelled", backend.futures.get(0).isCancelled()); + } finally { + chunks.close(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + assertEquals(4, backend.releases); + assertEquals(0, chunks.diagnostics().get("activeTickets")); + assertEquals(true, chunks.diagnostics().get("executorShutdown")); + chunks.close(); + assertEquals(4, backend.releases); + } + + @Test public void directServerThreadExecutorIsARealNegativeControl() throws Exception { + Backend backend = new Backend(1); + InlineExecutor executor = new InlineExecutor(); + AsyncWorldgenChunks chunks = new AsyncWorldgenChunks<>(backend, executor); + try { + CompletableFuture future = chunks.request(0, 0); + assertTrue(future.isCompletedExceptionally()); + try { future.get(5, TimeUnit.SECONDS); fail("Blocking native API branch was accepted"); } + catch (java.util.concurrent.ExecutionException expected) { + assertTrue(expected.getCause().getMessage().contains("off the server thread")); + } + assertTrue(backend.futures.isEmpty()); + } finally { chunks.close(); } + assertEquals(1, backend.releases); + } + + @Test public void nativeExceptionIsNotConvertedToReadinessOrEmptyTerrain() throws Exception { + Backend backend = new Backend(1); + ExecutorService executor = Executors.newSingleThreadExecutor(); + AsyncWorldgenChunks chunks = new AsyncWorldgenChunks<>(backend, executor); + try { + CompletableFuture future = chunks.request(0, 0); + assertTrue(backend.entered.await(5, TimeUnit.SECONDS)); + backend.futures.get(0).completeExceptionally(new IllegalStateException("native lost chunk")); + try { future.get(5, TimeUnit.SECONDS); fail("Native exception was hidden"); } + catch (java.util.concurrent.ExecutionException expected) { + assertEquals("native lost chunk", expected.getCause().getMessage()); + } + } finally { chunks.close(); assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); } + assertEquals(1, backend.releases); + } + + @Test public void zeroNullNativeResultFailsAndTicketIsReleased() throws Exception { + Backend backend = new Backend(1); + ExecutorService executor = Executors.newSingleThreadExecutor(); + AsyncWorldgenChunks chunks = new AsyncWorldgenChunks<>(backend, executor); + try { + CompletableFuture future = chunks.request(0, 0); + assertTrue(backend.entered.await(5, TimeUnit.SECONDS)); + backend.futures.get(0).complete(null); + try { future.get(5, TimeUnit.SECONDS); fail("Null native chunk was accepted"); } + catch (java.util.concurrent.ExecutionException expected) { + assertTrue(expected.getCause().getMessage().contains("chunk was lost")); + } + } finally { chunks.close(); assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); } + assertEquals(1, backend.releases); + } + + @Test public void rejectedDispatchReleasesItsTicketAndDoesNotLeaveAnActiveRequest() { + Backend backend = new Backend(1); + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.shutdown(); + AsyncWorldgenChunks chunks = new AsyncWorldgenChunks<>(backend, executor); + try { + try { chunks.request(0, 0); fail("Rejected dispatch was accepted"); } + catch (java.util.concurrent.RejectedExecutionException expected) { } + assertEquals(0, chunks.diagnostics().get("activeTickets")); + } finally { chunks.close(); } + assertEquals(1, backend.retains); + assertEquals(1, backend.releases); + } + + @Test public void lateNativeCompletionAfterStopCannotResurrectRequestsOrTickets() throws Exception { + Backend backend = new Backend(1); + ExecutorService executor = Executors.newSingleThreadExecutor(); + AsyncWorldgenChunks chunks = new AsyncWorldgenChunks<>(backend, executor); + CompletableFuture result = chunks.request(0, 0); + assertTrue(backend.entered.await(5, TimeUnit.SECONDS)); + chunks.close(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + backend.futures.get(0).complete("late native chunk"); + assertTrue(result.isCancelled()); + assertEquals(1, backend.releases); + assertEquals(0, chunks.diagnostics().get("activeTickets")); + try { chunks.request(1, 0); fail("Closed dispatcher was reused"); } + catch (IllegalStateException expected) { assertTrue(expected.getMessage().contains("closed")); } + } + + private static final class Backend implements AsyncWorldgenChunks.Backend { + final Thread owner = Thread.currentThread(); + final CountDownLatch entered; + final Map> futures = new ConcurrentHashMap<>(); + int retains, releases; + Backend(int count) { entered = new CountDownLatch(count); } + @Override public void retain(int x, int z) { assertSame(owner, Thread.currentThread()); retains++; } + @Override public CompletableFuture request(int x, int z) { + assertNotSame(owner, Thread.currentThread()); + CompletableFuture result = new CompletableFuture<>(); + futures.put(x, result); + entered.countDown(); + return result; + } + @Override public void release(int x, int z) { assertSame(owner, Thread.currentThread()); releases++; } + } + + private static final class InlineExecutor extends AbstractExecutorService { + boolean stopped; + @Override public void execute(Runnable task) { task.run(); } + @Override public void shutdown() { stopped = true; } + @Override public List shutdownNow() { stopped = true; return Collections.emptyList(); } + @Override public boolean isShutdown() { return stopped; } + @Override public boolean isTerminated() { return stopped; } + @Override public boolean awaitTermination(long timeout, TimeUnit unit) { return stopped; } + } +} 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 new file mode 100644 index 00000000..b09458cf --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/ConnectedProbeObservationTest.java @@ -0,0 +1,96 @@ +package zone.moddev.mc.advantageworks; + +import static org.junit.Assert.*; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.io.ByteArrayInputStream; +import java.lang.reflect.Field; +import java.util.Base64; +import java.util.List; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.junit.BeforeClass; +import org.junit.Test; + +public class ConnectedProbeObservationTest { + private static final BlockPos POS = new BlockPos(4, 80, 7); + + @BeforeClass public static void bootstrap() { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); } + + @Test public void snapshotDoesNotAllocateAbsentPersistentData() throws Exception { + observe(null); + } + + @Test public void snapshotRetainsExistingEmptyPersistentDataWithoutNormalization() throws Exception { + observe(new CompoundTag()); + } + + @Test public void snapshotReadsWitnessWithoutChangingAnyPersistentData() throws Exception { + CompoundTag data = new CompoundTag(); + data.putString("AdvConnectedWitness", "keep witness"); + data.putLong("PrivateLong", 987654321L); + data.putIntArray("PrivateArray", new int[]{3, 5, 8}); + observe(data); + } + + private static void observe(CompoundTag data) throws Exception { + ObservedChest tile = new ObservedChest(); + Field persistent = BlockEntity.class.getDeclaredField("customTileData"); + persistent.setAccessible(true); + persistent.set(tile, data); + CompoundTag before = tile.save(new CompoundTag()).copy(); + ObservationWorld world = allocate(ObservationWorld.class); + world.tile = tile; + ConnectedProbe probe = new ConnectedProbe(); + Field context = ConnectedProbe.class.getDeclaredField("world"); + context.setAccessible(true); + context.set(probe, world); + JsonObject definition = new JsonObject(), block = new JsonObject(); + block.addProperty("x", POS.getX()); + block.addProperty("y", POS.getY()); + block.addProperty("z", POS.getZ()); + JsonArray blocks = new JsonArray(); + blocks.add(block); + definition.add("blocks", blocks); + for (int observation = 0; observation < 2; observation++) { + 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.toString(), row.get("nbt").getAsString()); + assertEquals(before, NbtIo.readCompressed(new ByteArrayInputStream( + Base64.getDecoder().decode(row.get("nbtBase64").getAsString())))); + assertEquals(data == null ? "" : data.getString("AdvConnectedWitness"), row.get("witness").getAsString()); + assertEquals(data != null, before.contains("ForgeData")); + } + } + + private static T allocate(Class type) throws Exception { + Class unsafe = Class.forName("sun.misc.Unsafe"); + Field field = unsafe.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return type.cast(unsafe.getMethod("allocateInstance", Class.class).invoke(field.get(null), type)); + } + + private static final class ObservedChest extends ChestBlockEntity { + int persistentReads; + ObservedChest() { super(POS, Blocks.CHEST.defaultBlockState()); } + @Override public CompoundTag getTileData() { persistentReads++; return super.getTileData(); } + } + + private static final class ObservationWorld extends ServerLevel { + BlockEntity tile; + private ObservationWorld() { super(null, null, null, null, null, null, null, null, false, 0L, List.of(), false); } + @Override public BlockState getBlockState(BlockPos pos) { return Blocks.CHEST.defaultBlockState(); } + @Override public BlockEntity getBlockEntity(BlockPos pos) { return tile; } + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/ConnectedSessionTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/ConnectedSessionTest.java index dc96171d..c2d9f40d 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/ConnectedSessionTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/ConnectedSessionTest.java @@ -15,7 +15,7 @@ private JsonObject definition(String blocks) { } @Test(expected = IllegalArgumentException.class) public void rejectsInfinitePower() { - ConnectedSession.validate(definition("{id:'source',x:1,y:80,z:1,name:'poweradvantage:infinite_steam'}")); + ConnectedSession.validate(definition("{id:'source',x:1,y:80,z:1,name:'basepower:infinite_steam'}")); } @Test(expected = IllegalArgumentException.class) public void rejectsOverlappingPlacements() { diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleNaturalTickObserverTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleNaturalTickObserverTest.java new file mode 100644 index 00000000..e8225470 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleNaturalTickObserverTest.java @@ -0,0 +1,99 @@ +package zone.moddev.mc.advantageworks; + +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.TickingBlockEntity; +import net.minecraft.world.level.chunk.LevelChunk; +import org.junit.Test; +import static org.junit.Assert.*; + +public class LifecycleNaturalTickObserverTest { + private Map state(int progress) { + Map result = new LinkedHashMap<>(); + result.put("progress", progress); result.put("steam", 0); + return result; + } + + @Test public void firstFailedMachineStateIsNeverReplacedByLaterRepair() { + LifecycleNaturalTickObserver.Capture capture = new LifecycleNaturalTickObserver.Capture(); + boolean first = capture.enter(101, true, true, Arrays.asList("native"), state(227)); + Map failed = state(227); + capture.exit(first, failed); + assertTrue(capture.complete()); + boolean later = capture.enter(108, true, true, Arrays.asList("native"), state(0)); + capture.exit(later, state(0)); + assertSame(failed, capture.after); + assertEquals(101, capture.worldTime); + assertFalse(capture.complete()); + } + + @Test public void outerWrapperOrWrongEligibilityCannotClaimInnerDispatch() { + for (boolean[] facts : new boolean[][]{{false, false}, {true, false}, {false, true}}) { + LifecycleNaturalTickObserver.Capture capture = new LifecycleNaturalTickObserver.Capture(); + capture.exit(capture.enter(101, facts[0], facts[1], Arrays.asList("outer"), state(227)), state(227)); + assertFalse(capture.complete()); + } + } + + @Test public void enteringWithoutCompletingDoesNotProveACallback() { + LifecycleNaturalTickObserver.Capture capture = new LifecycleNaturalTickObserver.Capture(); + capture.enter(101, true, true, Arrays.asList("native"), state(227)); + assertFalse(capture.complete()); + assertNull(capture.after); + } + + @Test public void zeroEnergyAndUnchangedProgressAreNotTransportSelectionCriteria() { + LifecycleNaturalTickObserver.Capture capture = new LifecycleNaturalTickObserver.Capture(); + capture.exit(capture.enter(101, true, true, Arrays.asList("native"), state(227)), state(227)); + assertTrue(capture.complete()); + assertEquals(227, capture.after.get("progress")); + assertEquals(0, capture.after.get("steam")); + } + + @Test public void exactNativeInnerBindingLayoutIsPresent() { + boolean wrapper = false, bound = false; + for (Class type : LevelChunk.class.getDeclaredClasses()) { + if (!TickingBlockEntity.class.isAssignableFrom(type)) continue; + int registrations = 0, delegates = 0, tiles = 0; + for (Field field : type.getDeclaredFields()) { + if (field.getType() == TickingBlockEntity.class) registrations++; + if (field.getType() == BlockEntityTicker.class) delegates++; + if (field.getType() == BlockEntity.class) tiles++; + } + wrapper |= registrations == 1; + bound |= delegates == 1 && tiles == 1; + } + assertTrue(wrapper); + assertTrue(bound); + } + + @Test public void sourceConstructorAndOtherLevelFramesDoNotProveNativeTickDispatch() { + String level = "net.minecraft.world.level.Level"; + for (String method : Arrays.asList("", "setBlock", "getBlockEntity", "tick")) { + assertFalse(LifecycleNaturalTickObserver.nativeLoopFrame(new StackTraceElement(level, method, "Level.java", 1))); + } + assertTrue(LifecycleNaturalTickObserver.nativeLoopFrame(new StackTraceElement(level, "tickBlockEntities", "Level.java", 1))); + assertTrue(LifecycleNaturalTickObserver.nativeLoopFrame(new StackTraceElement(level, "m_46463_", "Level.java", 1))); + assertFalse(LifecycleNaturalTickObserver.nativeLoopFrame(new StackTraceElement("Harness", "tickBlockEntities", "Test.java", 1))); + } + + @Test public void observersCannotConstructProductionTilesOrForceADispatch() throws Exception { + String probe = Files.readString(Paths.get("src/main/java/zone/moddev/mc/advantageworks/LifecycleProbe.java")); + String observer = Files.readString(Paths.get("src/main/java/zone/moddev/mc/advantageworks/LifecycleNaturalTickObserver.java")); + assertTrue(probe.contains("queueRoundTrip")); + assertTrue(probe.contains("LevelChunk.EntityCreationType.CHECK")); + assertFalse(probe.contains("tile.tick(")); + assertFalse(probe.contains("new SteamDrill")); + assertFalse(probe.contains("new ElectricDrill")); + assertFalse(observer.contains("new SteamDrill")); + assertEquals(1, observer.split("original\\.tick\\(", -1).length - 1); + assertTrue(observer.contains("frame.getClassName().equals(bound.getClass().getName())")); + assertTrue(observer.contains("frame.getClassName().equals(Level.class.getName())")); + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleProbeTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleProbeTest.java index 660451eb..649f0882 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleProbeTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleProbeTest.java @@ -16,7 +16,7 @@ private LifecycleProbe.Fixture fixture() { fixture.from = new int[] {2063, 80, 2063}; fixture.to = new int[] {2063, 80, 2064}; fixture.hold = new int[] {2063, 81, 2064}; - fixture.block = "steamadvantage:steam_drill"; + fixture.block = "basesteam:steam_drill"; fixture.energyType = "steam"; fixture.initialEnergy = 40; fixture.witnesses = Arrays.asList(fixture.from, fixture.to, fixture.hold); @@ -25,7 +25,7 @@ private LifecycleProbe.Fixture fixture() { @Test public void boundedMoveAccepted() { LifecycleProbe.validate(fixture()); } @Test public void boundedOilBucketAccepted() { - LifecycleProbe.Fixture f = fixture(); f.bucket = "poweradvantage:crude_oil_bucket"; + LifecycleProbe.Fixture f = fixture(); f.bucket = "basepower:crude_oil_bucket"; f.source = new int[] {2060, 80, 2060}; LifecycleProbe.validate(f); } @Test public void fourPowerOilLanesRemainAcceptedWithoutMineralogy() { @@ -33,7 +33,7 @@ private LifecycleProbe.Fixture fixture() { for (String tank : Arrays.asList("fluid_storage_tank", "fluid_metal_tank")) { LifecycleProbe.Fixture f = fixture(); f.id = "OIL-" + oil + "-" + tank; - f.bucket = "poweradvantage:" + oil + "_bucket"; + f.bucket = "basepower:" + oil + "_bucket"; f.source = new int[] {2060, 80, 2060}; LifecycleProbe.validate(f, () -> { throw new AssertionError("Power oil must not require or query Mineralogy"); @@ -57,7 +57,7 @@ private LifecycleProbe.Fixture fixture() { @Test public void waterAndLookalikeOilBucketsRemainRejectedWithMineralogyLoaded() { for (String bucket : Arrays.asList("minecraft:water_bucket", "mineralogy:refined_oil_bucket", "other:crude_oil_bucket", "mineralogy:crude_oil_bucket_extra", - "poweradvantage:crude_oil_bucket_extra", "poweradvantage:other_oil_bucket")) { + "basepower:crude_oil_bucket_extra", "basepower:other_oil_bucket")) { LifecycleProbe.Fixture f = fixture(); f.bucket = bucket; f.source = new int[] {2060, 80, 2060}; IllegalArgumentException error = assertThrows(IllegalArgumentException.class, @@ -89,7 +89,7 @@ private LifecycleProbe.Fixture fixture() { LifecycleProbe.validate(f, () -> true); } @Test(expected = IllegalArgumentException.class) public void oilBucketOutsideBoundsRejected() { - LifecycleProbe.Fixture f = fixture(); f.bucket = "poweradvantage:crude_oil_bucket"; + LifecycleProbe.Fixture f = fixture(); f.bucket = "basepower:crude_oil_bucket"; f.source = new int[] {2080, 80, 2060}; LifecycleProbe.validate(f); } @Test(expected = IllegalArgumentException.class) public void arbitraryBucketRejected() { diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleRoundTripTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleRoundTripTest.java new file mode 100644 index 00000000..0394dd74 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleRoundTripTest.java @@ -0,0 +1,59 @@ +package zone.moddev.mc.advantageworks; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class LifecycleRoundTripTest { + @Test public void oneSimultaneousStartAndMatchingEndComplete() { + LifecycleRoundTrip window = new LifecycleRoundTrip(0); + assertFalse(window.start(100, 1, false)); + assertEquals(0, window.mutations); + assertTrue(window.start(101, 2, true)); + window.mutated(); + window.end(102, true); + assertTrue(window.finished); + assertNull(window.failure); + } + + @Test public void noCallbackCannotBeReplacedWithClockAdvanceOrALaterTick() { + for (long end : new long[]{100, 101, 108}) { + LifecycleRoundTrip window = new LifecycleRoundTrip(0); + window.start(100, 1, true); window.mutated(); + window.end(end, false); + assertNotNull(window.failure); + assertThrows(IllegalStateException.class, () -> window.end(109, true)); + } + } + + @Test public void aLateSuccessfulCallbackCannotPassEvenWithValidDispatch() { + LifecycleRoundTrip window = new LifecycleRoundTrip(0); + window.start(100, 1, true); window.mutated(); window.end(108, true); + assertNotNull(window.failure); + } + + @Test public void duplicateStartMutationAndUnmatchedEndFail() { + LifecycleRoundTrip window = new LifecycleRoundTrip(0); + assertThrows(IllegalStateException.class, window::mutated); + assertThrows(IllegalStateException.class, () -> window.end(101, true)); + window.start(100, 1, true); window.mutated(); + assertThrows(IllegalStateException.class, () -> window.start(101, 2, true)); + assertThrows(IllegalStateException.class, window::mutated); + } + + @Test public void failedPrerequisitesAreBoundedWithoutAnyMutation() { + LifecycleRoundTrip window = new LifecycleRoundTrip(0); + for (int i = 0; i < LifecycleRoundTrip.MAX_STARTS; i++) assertFalse(window.start(i, i, false)); + assertFalse(window.start(200, 200, true)); + assertTrue(window.finished); + assertNotNull(window.failure); + assertEquals(0, window.mutations); + } + + @Test public void elapsedLimitCannotBePassedByLateReadiness() { + LifecycleRoundTrip window = new LifecycleRoundTrip(0); + assertFalse(window.start(100, LifecycleRoundTrip.MAX_NANOS, true)); + assertTrue(window.finished); + assertFalse(window.started); + assertEquals(0, window.mutations); + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/MeasurementDefinitionTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/MeasurementDefinitionTest.java index 354665a9..3e9fc846 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/MeasurementDefinitionTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/MeasurementDefinitionTest.java @@ -24,7 +24,7 @@ public class MeasurementDefinitionTest { } private void rejects(Consumer corrupt) throws Exception { - WorksDefinition.Station station = WorksDefinitionLoader.loadNative116().stations.get(0); + WorksDefinition.Station station = WorksDefinitionLoader.loadNative117().stations.get(0); corrupt.accept(station); Method validate = WorksDefinitionLoader.class.getDeclaredMethod("validatePlacements", WorksDefinition.Station.class); validate.setAccessible(true); diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116EntityContextTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116EntityContextTest.java deleted file mode 100644 index 6f391771..00000000 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116EntityContextTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package zone.moddev.mc.advantageworks; - -import static org.junit.Assert.*; - -import com.google.gson.JsonObject; -import java.lang.reflect.Field; -import java.util.UUID; -import java.util.function.Supplier; -import net.minecraft.entity.EntityType; -import net.minecraft.entity.item.BoatEntity; -import net.minecraft.profiler.EmptyProfiler; -import net.minecraft.profiler.IProfiler; -import net.minecraft.util.RegistryKey; -import net.minecraft.util.registry.Bootstrap; -import net.minecraft.world.World; -import net.minecraft.world.server.ServerWorld; -import org.junit.BeforeClass; -import org.junit.Test; -import sun.misc.Unsafe; - -public class Native116EntityContextTest { - @BeforeClass public static void bootstrap() { Bootstrap.bootStrap(); } - - @Test public void observesActualRegistryKeyAndWorldIdentity() throws Exception { - ServerWorld world = world(World.NETHER); - BoatEntity boat = new BoatEntity(EntityType.BOAT, world); - JsonObject actual = Native116EntityContext.observe(boat, world); - assertEquals("minecraft:the_nether", actual.get("worldDimensionKey").getAsString()); - assertTrue(actual.get("sameWorld").getAsBoolean()); - assertEquals("minecraft:the_nether", actual.getAsJsonObject("entityWorlds").get(boat.getUUID().toString()).getAsString()); - } - - @Test public void observesEndWithoutAnyExpectedFixtureDimension() throws Exception { - ServerWorld world = world(World.END); - BoatEntity boat = new BoatEntity(EntityType.BOAT, world); - assertEquals("minecraft:the_end", Native116EntityContext.observe(boat, world).get("worldDimensionKey").getAsString()); - } - - @Test public void recordsActualPassengerWorldAndUuid() throws Exception { - ServerWorld world = world(World.NETHER); - BoatEntity vehicle = new BoatEntity(EntityType.BOAT, world), passenger = new BoatEntity(EntityType.BOAT, world); - assertTrue(passenger.startRiding(vehicle, true)); - JsonObject actual = Native116EntityContext.observe(vehicle, world).getAsJsonObject("entityWorlds"); - assertEquals(2, actual.entrySet().size()); - assertEquals("minecraft:the_nether", actual.get(passenger.getUUID().toString()).getAsString()); - } - - @Test public void rejectsDifferentWorldInstanceEvenWithSameRegistryKey() throws Exception { - ServerWorld actual = world(World.NETHER), other = world(World.NETHER); - reject(new BoatEntity(EntityType.BOAT, actual), other); - } - - @Test public void rejectsPassengerWithDifferentActualWorld() throws Exception { - ServerWorld world = world(World.NETHER); - BoatEntity vehicle = new BoatEntity(EntityType.BOAT, world), passenger = new BoatEntity(EntityType.BOAT, world); - assertTrue(passenger.startRiding(vehicle, true)); - passenger.level = world(World.END); - reject(vehicle, world); - } - - @Test public void rejectsDuplicatePassengerUuid() throws Exception { - ServerWorld world = world(World.NETHER); - BoatEntity vehicle = new BoatEntity(EntityType.BOAT, world), passenger = new BoatEntity(EntityType.BOAT, world); - passenger.setUUID(vehicle.getUUID()); - assertTrue(passenger.startRiding(vehicle, true)); - reject(vehicle, world); - } - - @Test public void observationDoesNotChangeEntityIdentityOrPassengers() throws Exception { - ServerWorld world = world(World.NETHER); - BoatEntity boat = new BoatEntity(EntityType.BOAT, world); - UUID before = boat.getUUID(); - Native116EntityContext.observe(boat, world); - assertEquals(before, boat.getUUID()); - assertTrue(boat.getPassengers().isEmpty()); - } - - @Test public void rejectsMissingContext() throws Exception { - reject(null, world(World.NETHER)); - } - - private static void reject(BoatEntity entity, ServerWorld world) { - try { Native116EntityContext.observe(entity, world); fail("Invalid native context accepted"); } - catch (IllegalStateException expected) { } - } - - private static ServerWorld world(RegistryKey key) throws Exception { - Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); - ServerWorld world = (ServerWorld) ((Unsafe) field.get(null)).allocateInstance(ServerWorld.class); - set(world, "dimension", key); - set(world, "profiler", (Supplier) () -> EmptyProfiler.INSTANCE); - return world; - } - - private static void set(ServerWorld world, String name, Object value) throws Exception { - Field field = World.class.getDeclaredField(name); field.setAccessible(true); field.set(world, value); - } -} 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 new file mode 100644 index 00000000..6a6a253e --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117AsyncWorldgenSourceTest.java @@ -0,0 +1,88 @@ +package zone.moddev.mc.advantageworks; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.ChunkPos; +import org.junit.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import static org.junit.Assert.*; + +public class Native117AsyncWorldgenSourceTest { + @Test public void actualNativeUnknownTicketExpiresButDedicatedRegionTicketDoesNot() { + assertEquals(1L, TicketType.UNKNOWN.timeout()); + TicketType retained = TicketType.create("async-worldgen-unit", Comparator.comparingLong(ChunkPos::toLong)); + assertEquals(0L, retained.timeout()); + } + + @Test public void actualMappedNativeFutureApiHasBothBlockingAndOffThreadBranches() throws Exception { + List calls = new ArrayList<>(); + try (InputStream input = ServerChunkCache.class.getResourceAsStream("ServerChunkCache.class")) { + assertNotNull("Actual mapped Forge class is required", input); + new ClassReader(input).accept(new ClassVisitor(Opcodes.ASM9) { + @Override public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + if (!"getChunkFuture".equals(name)) return null; + return new MethodVisitor(Opcodes.ASM9) { + @Override public void visitMethodInsn(int opcode, String owner, String method, + String desc, boolean isInterface) { + calls.add(method); + } + }; + } + }, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + } + assertTrue(calls.contains("getChunkFutureMainThread")); + assertTrue("Calling this API on a server tick still blocks", calls.contains("managedBlock")); + assertTrue("Use the native off-thread scheduling branch", calls.contains("supplyAsync")); + assertTrue(calls.contains("thenCompose")); + } + + @Test public void nativeWorldgenAdapterNeverSynchronouslyRequestsFullChunks() throws Exception { + String controller = source("WorksController.java"); + int start = controller.indexOf("synchronized String sampleWorldgen("); + int end = controller.indexOf("List stationIdsWithAll()"); + assertFalse("Population and count must not block the native server tick", + controller.substring(start, end).contains("world.getChunk(")); + Path adapter = Path.of("src/main/java/zone/moddev/mc/advantageworks/Native117WorldgenAccess.java"); + assertTrue("A native async adapter must own the FULL futures", Files.isRegularFile(adapter)); + String nativeSource = Files.readString(adapter, StandardCharsets.UTF_8); + assertTrue(nativeSource.matches("(?s).*getChunkFuture\\(.*ChunkStatus\\.FULL,\\s*true\\).*")); + assertTrue(nativeSource.contains("isLightCorrect()")); + assertTrue(nativeSource.contains("addRegionTicket(TICKET, position, 0, position)")); + assertTrue(nativeSource.contains("removeRegionTicket(TICKET, position, 0, position)")); + assertFalse(nativeSource.contains(".join()")); + assertFalse(nativeSource.contains("world.getChunk(")); + } + + @Test public void samplerUsesPolledFuturesRatherThanSynchronousPopulation() throws Exception { + String source = source("WorldgenSamplingJob.java"); + assertTrue("Pending native futures must be representable", source.contains("CompletableFuture")); + assertTrue(source.contains(".isDone()")); + assertTrue(source.contains(".getNow(null)")); + assertFalse(source.contains(".join()")); + } + + @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 end = controller.indexOf("static boolean matchesExpectedBiome", start); + assertTrue(start >= 0 && end > start); + assertTrue(controller.substring(start, end).contains("worldgenJob.release()")); + assertTrue(source("AsyncWorldgenChunks.java").contains("executor.shutdownNow()")); + } + + private static String source(String name) throws Exception { + return Files.readString(Path.of("src/main/java/zone/moddev/mc/advantageworks", name), StandardCharsets.UTF_8); + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116CommandRegistrationTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117CommandRegistrationTest.java similarity index 95% rename from test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116CommandRegistrationTest.java rename to test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117CommandRegistrationTest.java index 5cd3aa0b..9ba8bdc8 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116CommandRegistrationTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117CommandRegistrationTest.java @@ -6,7 +6,7 @@ import org.junit.Test; import static org.junit.Assert.*; -public class Native116CommandRegistrationTest { +public class Native117CommandRegistrationTest { @Test public void everyProbeRegistersOnTheNativeDispatcherRebuildEvent() throws Exception { for (String name : new String[] {"ActorProbe", "AdvantageWorksMod", "DimensionEntityProbe", "ConnectedProbe", "NativeHarvestProbe", "LifecycleProbe", "MigrationProbe", diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116DefinitionTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117DefinitionTest.java similarity index 90% rename from test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116DefinitionTest.java rename to test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117DefinitionTest.java index a8aef60c..be2e3ffe 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116DefinitionTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117DefinitionTest.java @@ -6,16 +6,16 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; -public class Native116DefinitionTest { +public class Native117DefinitionTest { @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.loadNative116(); + WorksDefinition target = WorksDefinitionLoader.loadNative117(); assertEquals("1.13.2", source.minecraft); - assertEquals("1.16.5", target.minecraft); + assertEquals("1.17.1", 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/Native116EngineSignaturesTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117EngineSignaturesTest.java similarity index 52% rename from test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116EngineSignaturesTest.java rename to test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117EngineSignaturesTest.java index 7254be07..96b14068 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116EngineSignaturesTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117EngineSignaturesTest.java @@ -2,37 +2,37 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import net.minecraft.block.BlockState; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.nbt.CompoundTag; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.management.PlayerList; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.RegistryKey; -import net.minecraft.world.chunk.Chunk; -import net.minecraft.world.server.ServerChunkProvider; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; import org.junit.Test; import static org.junit.Assert.*; -public class Native116EngineSignaturesTest { +public class Native117EngineSignaturesTest { @Test public void nativeChunkRegistryAndTileSignaturesAreExact() throws Exception { - assertEquals(Chunk.class, ServerChunkProvider.class.getMethod("getChunkNow", int.class, int.class).getReturnType()); - assertEquals(ServerWorld.class, MinecraftServer.class.getMethod("getLevel", RegistryKey.class).getReturnType()); - assertEquals(void.class, TileEntity.class.getMethod("load", BlockState.class, CompoundNBT.class).getReturnType()); - assertEquals(CompoundNBT.class, TileEntity.class.getMethod("save", CompoundNBT.class).getReturnType()); + 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()); } @Test public void nativePlayerSaveRemainsProtectedAndRespawnUsesRegistryKeys() throws Exception { - Method save = PlayerList.class.getDeclaredMethod("save", ServerPlayerEntity.class); + Method save = PlayerList.class.getDeclaredMethod("save", ServerPlayer.class); assertTrue(Modifier.isProtected(save.getModifiers())); - assertEquals(RegistryKey.class, ServerPlayerEntity.class.getMethod("getRespawnDimension").getReturnType()); - assertEquals(void.class, ServerPlayerEntity.class.getMethod("setRespawnPosition", RegistryKey.class, - net.minecraft.util.math.BlockPos.class, float.class, boolean.class, boolean.class).getReturnType()); + assertEquals(ResourceKey.class, ServerPlayer.class.getMethod("getRespawnDimension").getReturnType()); + assertEquals(void.class, ServerPlayer.class.getMethod("setRespawnPosition", ResourceKey.class, + net.minecraft.core.BlockPos.class, float.class, boolean.class, boolean.class).getReturnType()); } @Test public void realNativeBitStoragePadsFiveSevenAndThirteenBitValues() throws Exception { - Class storage = Class.forName("net.minecraft.util.BitArray"); + 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); int perLong = 64 / bits; diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117EntityContextTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117EntityContextTest.java new file mode 100644 index 00000000..ec60aece --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117EntityContextTest.java @@ -0,0 +1,102 @@ +package zone.moddev.mc.advantageworks; + +import static org.junit.Assert.*; + +import com.google.gson.JsonObject; +import java.lang.reflect.Field; +import java.util.UUID; +import java.util.function.Supplier; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.vehicle.Boat; +import net.minecraft.util.profiling.InactiveProfiler; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.Level; +import net.minecraft.server.level.ServerLevel; +import org.junit.BeforeClass; +import org.junit.Test; + +public class Native117EntityContextTest { + @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); } + + @Test public void observesActualRegistryKeyAndWorldIdentity() throws Exception { + ServerLevel world = world(Level.NETHER); + Boat boat = new Boat(EntityType.BOAT, world); + JsonObject actual = Native117EntityContext.observe(boat, world); + assertEquals("minecraft:the_nether", actual.get("worldDimensionKey").getAsString()); + assertTrue(actual.get("sameWorld").getAsBoolean()); + assertEquals("minecraft:the_nether", actual.getAsJsonObject("entityWorlds").get(boat.getUUID().toString()).getAsString()); + } + + @Test public void observesEndWithoutAnyExpectedFixtureDimension() throws Exception { + ServerLevel world = world(Level.END); + Boat boat = new Boat(EntityType.BOAT, world); + assertEquals("minecraft:the_end", Native117EntityContext.observe(boat, world).get("worldDimensionKey").getAsString()); + } + + @Test public void recordsActualPassengerWorldAndUuid() throws Exception { + ServerLevel world = world(Level.NETHER); + Boat vehicle = new Boat(EntityType.BOAT, world), passenger = new Boat(EntityType.BOAT, world); + assertTrue(passenger.startRiding(vehicle, true)); + JsonObject actual = Native117EntityContext.observe(vehicle, world).getAsJsonObject("entityWorlds"); + assertEquals(2, actual.entrySet().size()); + assertEquals("minecraft:the_nether", actual.get(passenger.getUUID().toString()).getAsString()); + } + + @Test public void rejectsDifferentWorldInstanceEvenWithSameRegistryKey() throws Exception { + ServerLevel actual = world(Level.NETHER), other = world(Level.NETHER); + reject(new Boat(EntityType.BOAT, actual), other); + } + + @Test public void rejectsPassengerWithDifferentActualWorld() throws Exception { + ServerLevel world = world(Level.NETHER); + Boat vehicle = new Boat(EntityType.BOAT, world), passenger = new Boat(EntityType.BOAT, world); + assertTrue(passenger.startRiding(vehicle, true)); + passenger.level = world(Level.END); + reject(vehicle, world); + } + + @Test public void rejectsDuplicatePassengerUuid() throws Exception { + ServerLevel world = world(Level.NETHER); + Boat vehicle = new Boat(EntityType.BOAT, world), passenger = new Boat(EntityType.BOAT, world); + passenger.setUUID(vehicle.getUUID()); + assertTrue(passenger.startRiding(vehicle, true)); + reject(vehicle, world); + } + + @Test public void observationDoesNotChangeEntityIdentityOrPassengers() throws Exception { + ServerLevel world = world(Level.NETHER); + Boat boat = new Boat(EntityType.BOAT, world); + UUID before = boat.getUUID(); + Native117EntityContext.observe(boat, world); + assertEquals(before, boat.getUUID()); + assertTrue(boat.getPassengers().isEmpty()); + } + + @Test public void rejectsMissingContext() throws Exception { + reject(null, world(Level.NETHER)); + } + + private static void reject(Boat entity, ServerLevel world) { + try { Native117EntityContext.observe(entity, world); fail("Invalid native context accepted"); } + catch (IllegalStateException expected) { } + } + + static ServerLevel world(ResourceKey key) throws Exception { + Class unsafe = Class.forName("sun.misc.Unsafe"); + Field field = unsafe.getDeclaredField("theUnsafe"); field.setAccessible(true); + ServerLevel world = (ServerLevel) unsafe.getMethod("allocateInstance", Class.class) + .invoke(field.get(null), ServerLevel.class); + set(world, "dimension", key); + set(world, "random", new java.util.Random(0)); + set(world, "profiler", (Supplier) () -> InactiveProfiler.INSTANCE); + Field ticks = ServerLevel.class.getDeclaredField("entityTickList"); ticks.setAccessible(true); + ticks.set(world, new net.minecraft.world.level.entity.EntityTickList()); + return world; + } + + private static void set(ServerLevel world, String name, Object value) throws Exception { + Field field = Level.class.getDeclaredField(name); field.setAccessible(true); field.set(world, value); + } +} 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 new file mode 100644 index 00000000..edb78088 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117HammerQueryGateTest.java @@ -0,0 +1,101 @@ +package zone.moddev.mc.advantageworks; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import static org.junit.Assert.*; + +public class Native117HammerQueryGateTest { + private static JsonObject visible() { + JsonObject before = new JsonObject(); + before.addProperty("sourceInAabb", true); + before.addProperty("uuidLookupIsSource", true); + before.addProperty("entityTicking", false); + before.addProperty("entitiesLoaded", false); + JsonObject source = new JsonObject(); + source.addProperty("isSource", true); + JsonArray items = new JsonArray(); + items.add(source); + before.add("items", items); + return before; + } + + @Test public void trackedSourceNeedNotHaveTicked() { + assertTrue(HammerQueryGate.ready(true, false, true, visible())); + } + @Test public void refusedAdditionCannotInvoke() { + assertFalse(HammerQueryGate.ready(false, false, true, visible())); + } + @Test public void removedSourceCannotInvoke() { + assertFalse(HammerQueryGate.ready(true, true, true, visible())); + } + @Test public void alteredInputCannotInvoke() { + assertFalse(HammerQueryGate.ready(true, false, false, visible())); + } + @Test public void tickingFlagsCannotReplaceQueryIdentity() { + JsonObject before = visible(); + before.addProperty("sourceInAabb", false); + before.addProperty("entityTicking", true); + before.addProperty("entitiesLoaded", true); + assertFalse(HammerQueryGate.ready(true, false, true, before)); + } + @Test public void uuidLookupDoesNotReplaceAabbIdentity() { + JsonObject before = visible(); + before.addProperty("uuidLookupIsSource", false); + assertFalse(HammerQueryGate.ready(true, false, true, before)); + } + @Test public void sameContentsDecoyDoesNotCount() { + JsonObject before = visible(); + before.getAsJsonArray("items").get(0).getAsJsonObject().addProperty("isSource", false); + assertFalse(HammerQueryGate.ready(true, false, true, before)); + } + @Test public void contaminatedQueryCannotInvoke() { + JsonObject before = visible(); + before.getAsJsonArray("items").add(new JsonObject()); + assertFalse(HammerQueryGate.ready(true, false, true, before)); + before.getAsJsonArray("items").remove(1); + before.getAsJsonArray("items").remove(0); + assertFalse(HammerQueryGate.ready(true, false, true, before)); + } + @Test public void malformedSnapshotsFailClosed() { + assertFalse(HammerQueryGate.ready(true, false, true, null)); + JsonObject before = visible(); + before.addProperty("sourceInAabb", "true"); + assertFalse(HammerQueryGate.ready(true, false, true, before)); + before = visible(); + before.remove("items"); + assertFalse(HammerQueryGate.ready(true, false, true, before)); + } + @Test public void noMatchIsObservedAndMustReachOriginalActionOracle() { + for (String status : new String[] {"MATCH", "NO_MATCH"}) { + JsonObject selection = new JsonObject(); + selection.addProperty("status", status); + assertTrue(HammerQueryGate.selectorObserved(selection)); + } + } + @Test public void observerErrorsAreNotMissingRecipes() { + assertFalse(HammerQueryGate.selectorObserved(null)); + assertFalse(HammerQueryGate.selectorObserved(new JsonObject())); + for (String status : new String[] {"OBSERVER_ERROR", "unknown", ""}) { + JsonObject selection = new JsonObject(); + selection.addProperty("status", status); + assertFalse(HammerQueryGate.selectorObserved(selection)); + } + } + @Test public void singleCallStillUsesOriginalActionAndOutputAssertions() throws Exception { + String source = Files.readString(Path.of("src/main/java/zone/moddev/mc/advantageworks/ProcessProgression.java")); + String body = source.substring(source.indexOf("static void hammer("), source.indexOf("private static JsonObject hammerRecipeSelection(")); + assertEquals(1, body.split("\\.useOn\\(", -1).length - 1); + 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("player.getMainHandItem().getDamageValue() == 1, \"Live hammer failed \"")); + assertTrue(body.contains("id(entity.getItem()).equals(id(expected))")); + assertTrue(body.contains("require(produced == expected.getCount()")); + assertFalse(body.contains("Thread.sleep")); + assertFalse(body.contains("while (")); + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116HarvestIsolationTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117HarvestIsolationTest.java similarity index 66% rename from test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116HarvestIsolationTest.java rename to test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117HarvestIsolationTest.java index 4d06582a..d984dea0 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116HarvestIsolationTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117HarvestIsolationTest.java @@ -4,27 +4,31 @@ import java.util.Collections; import java.util.List; import java.util.function.Predicate; -import net.minecraft.entity.Entity; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; -import net.minecraft.util.EntityPredicates; -import net.minecraft.util.math.AxisAlignedBB; -import net.minecraft.util.registry.Bootstrap; -import net.minecraft.world.IEntityReader; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.entity.EntitySelector; +import net.minecraft.world.phys.AABB; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.EntityGetter; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.*; -public class Native116HarvestIsolationTest { - private static final AxisAlignedBB BOUNDS = new AxisAlignedBB(0, 0, 0, 1, 1, 1).inflate(2); +public class Native117HarvestIsolationTest { + private static final AABB BOUNDS = new AABB(0, 0, 0, 1, 1, 1).inflate(2); + private static net.minecraft.world.level.Level world; - @BeforeClass public static void initializeNativeRegistries() { Bootstrap.bootStrap(); } + @BeforeClass public static void initializeNativeRegistries() throws Exception { + net.minecraft.SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); + world = Native117EntityContextTest.world(net.minecraft.world.level.Level.OVERWORLD); + } private static ItemEntity item(Item type, int count) { - return new ItemEntity(null, 0.5, 0.5, 0.5, new ItemStack(type, count)); + return new ItemEntity(world, 0.5, 0.5, 0.5, new ItemStack(type, count)); } @Test public void nativeDefaultQueryReproducesFourThenSixThenEightBeforeTickCleanup() { @@ -32,13 +36,13 @@ private static ItemEntity item(Item type, int count) { ItemEntity first = item(Items.GUNPOWDER, 4); reader.index.add(first); assertEquals(4, count(reader.getEntitiesOfClass(ItemEntity.class, BOUNDS))); - reader.getEntitiesOfClass(ItemEntity.class, BOUNDS).forEach(ItemEntity::remove); - assertTrue(first.removed); - assertTrue(EntityPredicates.NO_SPECTATORS.test(first)); + reader.getEntitiesOfClass(ItemEntity.class, BOUNDS).forEach(ItemEntity::discard); + assertTrue(first.isRemoved()); + assertTrue(EntitySelector.NO_SPECTATORS.test(first)); assertSame(first, reader.getEntitiesOfClass(ItemEntity.class, BOUNDS).get(0)); reader.index.add(item(Items.GUNPOWDER, 2)); assertEquals(6, count(reader.getEntitiesOfClass(ItemEntity.class, BOUNDS))); - reader.getEntitiesOfClass(ItemEntity.class, BOUNDS).forEach(ItemEntity::remove); + reader.getEntitiesOfClass(ItemEntity.class, BOUNDS).forEach(ItemEntity::discard); reader.index.add(item(Items.GUNPOWDER, 2)); assertEquals(8, count(reader.getEntitiesOfClass(ItemEntity.class, BOUNDS))); } @@ -46,7 +50,7 @@ private static ItemEntity item(Item type, int count) { @Test public void removedEarlierDropCannotInflateTheNextTrial() { QueryReader reader = new QueryReader(); reader.index.add(item(Items.GUNPOWDER, 4)); - NativeHarvestProbe.harvestDrops(reader, BOUNDS).forEach(ItemEntity::remove); + NativeHarvestProbe.harvestDrops(reader, BOUNDS).forEach(ItemEntity::discard); assertTrue(NativeHarvestProbe.harvestDrops(reader, BOUNDS).isEmpty()); reader.index.add(item(Items.GUNPOWDER, 2)); assertEquals(2, count(NativeHarvestProbe.harvestDrops(reader, BOUNDS))); @@ -56,7 +60,7 @@ private static ItemEntity item(Item type, int count) { QueryReader reader = new QueryReader(); ItemEntity powder = item(Items.GUNPOWDER, 4); reader.index.add(powder); - powder.remove(); + powder.discard(); assertTrue(NativeHarvestProbe.harvestDrops(reader, BOUNDS).isEmpty()); ItemEntity ore = item(Items.IRON_ORE, 1); reader.index.add(ore); @@ -85,23 +89,23 @@ private static int count(List entities) { } /** Models only Chunk's index and predicate dispatch; native entities/default queries are real. */ - private static final class QueryReader implements IEntityReader { + private static final class QueryReader implements EntityGetter { final List index = new ArrayList<>(); - @Override public List getEntitiesOfClass(Class type, - AxisAlignedBB bounds, Predicate filter) { + @Override public List getEntities(net.minecraft.world.level.entity.EntityTypeTest type, + AABB bounds, Predicate filter) { List found = new ArrayList<>(); - for (ItemEntity entity : index) if (type.isInstance(entity) && entity.getBoundingBox().intersects(bounds)) { - T value = type.cast(entity); - if (filter == null || filter.test(value)) found.add(value); + for (ItemEntity entity : index) if (entity.getBoundingBox().intersects(bounds)) { + T value = type.tryCast(entity); + if (value != null && (filter == null || filter.test(value))) found.add(value); } return found; } - @Override public List getEntities(Entity except, AxisAlignedBB bounds, Predicate filter) { + @Override public List getEntities(Entity except, AABB bounds, Predicate filter) { throw new UnsupportedOperationException("Not used by harvest queries"); } - @Override public List players() { return Collections.emptyList(); } + @Override public List players() { return Collections.emptyList(); } } } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117JoiningHoldTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117JoiningHoldTest.java new file mode 100644 index 00000000..330a5636 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117JoiningHoldTest.java @@ -0,0 +1,72 @@ +package zone.moddev.mc.advantageworks; + +import static org.junit.Assert.*; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.UUID; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.vehicle.Boat; +import net.minecraft.world.level.Level; +import net.minecraftforge.event.entity.EntityJoinWorldEvent; +import org.junit.BeforeClass; +import org.junit.Test; + +public class Native117JoiningHoldTest { + @BeforeClass public static void bootstrap() { + net.minecraft.SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); + } + + @Test public void joiningActorIsHeldBeforeItCanEnterTheNativeTickList() throws Exception { + ServerLevel world = Native117EntityContextTest.world(Level.OVERWORLD); + Boat boat = new Boat(EntityType.BOAT, world); + boat.tickCount = 17; + UUID identity = boat.getUUID(); + MachineOperationalProbe probe = new MachineOperationalProbe(); + Method joining = MachineOperationalProbe.class.getDeclaredMethod("entityJoining", EntityJoinWorldEvent.class); + joining.setAccessible(true); + joining.invoke(probe, new EntityJoinWorldEvent(boat, world)); + assertFalse(boat.canUpdate()); + // Repeated START enumeration must not replace the pre-join state. + Field field = MachineOperationalProbe.class.getDeclaredField("heldEntities"); field.setAccessible(true); + NativeEntityHold hold = (NativeEntityHold) field.get(probe); + hold.hold(boat); + hold.restore(); + assertTrue(boat.canUpdate()); + assertEquals(17, boat.tickCount); + assertEquals(identity, boat.getUUID()); + assertSame(world, boat.level); + assertFalse(boat.isRemoved()); + } + + @Test public void nativeJoinWhileMeasuringIsNotFrozen() throws Exception { + ServerLevel world = Native117EntityContextTest.world(Level.OVERWORLD); + Boat boat = new Boat(EntityType.BOAT, world); + MachineOperationalProbe probe = new MachineOperationalProbe(); + Field paused = MachineOperationalProbe.class.getDeclaredField("paused"); paused.setAccessible(true); + paused.setBoolean(probe, false); + Method joining = MachineOperationalProbe.class.getDeclaredMethod("entityJoining", EntityJoinWorldEvent.class); + joining.setAccessible(true); + joining.invoke(probe, new EntityJoinWorldEvent(boat, world)); + assertTrue(boat.canUpdate()); + } + + @Test public void previouslyDisabledActorRemainsDisabledAfterHold() throws Exception { + Boat boat = new Boat(EntityType.BOAT, Native117EntityContextTest.world(Level.OVERWORLD)); + boat.canUpdate(false); + NativeEntityHold hold = new NativeEntityHold(); + hold.hold(boat); hold.hold(boat); hold.restore(); + assertFalse(boat.canUpdate()); + } + + @Test public void nativeTickDriftCannotBeHiddenByRestoringTheCounter() throws Exception { + Boat boat = new Boat(EntityType.BOAT, Native117EntityContextTest.world(Level.OVERWORLD)); + NativeEntityHold hold = new NativeEntityHold(); + hold.hold(boat); + boat.tickCount++; + try { hold.restore(); fail("Native tick drift was erased"); } + catch (AssertionError expected) { assertTrue(expected.getMessage().contains("ticked while held")); } + assertEquals(1, boat.tickCount); + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116PackMetadataTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117PackMetadataTest.java similarity index 80% rename from test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116PackMetadataTest.java rename to test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117PackMetadataTest.java index 2d5449ec..9df3b685 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116PackMetadataTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117PackMetadataTest.java @@ -8,14 +8,14 @@ import org.junit.Test; import static org.junit.Assert.*; -public class Native116PackMetadataTest { - @Test public void harnessResourcesDeclareTheNative116PackFormat() throws Exception { +public class Native117PackMetadataTest { + @Test public void harnessResourcesDeclareTheNative117PackFormat() 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(6, pack.get("pack_format").getAsInt()); + assertEquals(7, pack.get("pack_format").getAsInt()); } } } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116PlayerIoTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117PlayerIoTest.java similarity index 61% rename from test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116PlayerIoTest.java rename to test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117PlayerIoTest.java index 3e6487d9..f72b9efc 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116PlayerIoTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117PlayerIoTest.java @@ -8,48 +8,47 @@ import java.lang.reflect.Method; import java.nio.file.Files; import java.util.UUID; -import net.minecraft.entity.player.PlayerInventory; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.JsonToNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.network.play.ServerPlayNetHandler; -import net.minecraft.server.management.PlayerList; -import net.minecraft.util.SharedConstants; -import net.minecraft.util.datafix.DataFixesManager; -import net.minecraft.util.registry.Bootstrap; -import net.minecraft.world.storage.PlayerData; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.TagParser; +import net.minecraft.nbt.ListTag; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.players.PlayerList; +import net.minecraft.SharedConstants; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.storage.PlayerDataStorage; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import sun.misc.Unsafe; -public class Native116PlayerIoTest { +public class Native117PlayerIoTest { private static final UUID ID = UUID.fromString("a614e658-6270-33c3-9297-7ce80908964f"); @Rule public TemporaryFolder temporary = new TemporaryFolder() { @Override protected void after() { - if (!Boolean.getBoolean("native116.playerIo.keepEvidence")) super.after(); + if (!Boolean.getBoolean("native117.playerIo.keepEvidence")) super.after(); } }; - @BeforeClass public static void bootstrap() { Bootstrap.bootStrap(); } + @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); } @Test public void realForgePlayerListSaveWithoutConnectionLeavesLegacyBytesUnchanged() throws Exception { for (int version : new int[] {512, 1343}) { File directory = temporary.newFolder("guard-" + version); File file = new File(directory, ID + ".dat"); - CompoundNBT legacy = legacy(version); - CompressedStreamTools.writeCompressed(legacy, file); + CompoundTag legacy = legacy(version); + NbtIo.writeCompressed(legacy, file); byte[] before = Files.readAllBytes(file.toPath()); PlayerList players = players(data(directory), false); - Method save = PlayerList.class.getDeclaredMethod("save", ServerPlayerEntity.class); + Method save = PlayerList.class.getDeclaredMethod("save", ServerPlayer.class); save.setAccessible(true); save.invoke(players, player(1, 0)); assertArrayEquals(before, Files.readAllBytes(file.toPath())); - assertEquals(legacy, CompressedStreamTools.readCompressed(file)); + assertEquals(legacy, NbtIo.readCompressed(file)); } } @@ -57,64 +56,64 @@ public class Native116PlayerIoTest { for (int version : new int[] {512, 1343}) { File directory = temporary.newFolder("save-" + version); File file = new File(directory, ID + ".dat"); - CompoundNBT legacy = legacy(version); - CompressedStreamTools.writeCompressed(legacy, file); + CompoundTag legacy = legacy(version); + NbtIo.writeCompressed(legacy, file); byte[] before = Files.readAllBytes(file.toPath()); HeadlessPlayer player = player(1, 0); - Native116PlayerIo.save(players(data(directory), false), player); + Native117PlayerIo.save(players(data(directory), false), player); assertFalse(java.util.Arrays.equals(before, Files.readAllBytes(file.toPath()))); - assertSaved(CompressedStreamTools.readCompressed(file), player, 1, 0); - assertEquals(legacy, CompressedStreamTools.readCompressed(new File(directory, ID + ".dat_old"))); + assertSaved(NbtIo.readCompressed(file), player, 1, 0); + assertEquals(legacy, NbtIo.readCompressed(new File(directory, ID + ".dat_old"))); } } @Test public void freshNativePlayerDataAndPlayerReloadTheSavedInventoryWithoutDrift() throws Exception { File directory = temporary.newFolder("reload"); HeadlessPlayer first = player(1, 0); - Native116PlayerIo.save(players(data(directory), false), first); + Native117PlayerIo.save(players(data(directory), false), first); HeadlessPlayer second = player(9, 8); - PlayerData reopened = data(directory); - CompoundNBT saved = reopened.load(second); + PlayerDataStorage reopened = data(directory); + CompoundTag saved = reopened.load(second); assertNotNull(saved); assertSaved(saved, second, 1, 0); - Native116PlayerIo.save(players(reopened, false), second); - assertEquals(saved, CompressedStreamTools.readCompressed(new File(directory, ID + ".dat"))); + Native117PlayerIo.save(players(reopened, false), second); + assertEquals(saved, NbtIo.readCompressed(new File(directory, ID + ".dat"))); } @Test public void nativeSavePreservesExactCountSlotAndAllNamedStoredEnchantments() throws Exception { File directory = temporary.newFolder("count-slot"); HeadlessPlayer player = player(3, 8); - Native116PlayerIo.save(players(data(directory), false), player); - assertSaved(CompressedStreamTools.readCompressed(new File(directory, ID + ".dat")), player, 3, 8); + Native117PlayerIo.save(players(data(directory), false), player); + assertSaved(NbtIo.readCompressed(new File(directory, ID + ".dat")), player, 3, 8); } @Test public void connectedPlayersKeepThePlayerListSavePath() throws Exception { File directory = temporary.newFolder("connected"); HeadlessPlayer player = player(1, 0); - player.connection = allocate(ServerPlayNetHandler.class); + player.connection = allocate(ServerGamePacketListenerImpl.class); CountingPlayerList players = (CountingPlayerList) players(data(directory), true); - Native116PlayerIo.save(players, player); + Native117PlayerIo.save(players, player); assertEquals(1, players.calls); assertFalse(new File(directory, ID + ".dat").exists()); } @Test public void missingNativeStorageFailsInsteadOfPretendingToSave() throws Exception { try { - Native116PlayerIo.save(players(null, false), player(1, 0)); + Native117PlayerIo.save(players(null, false), player(1, 0)); fail("missing PlayerData accepted"); } catch (IllegalStateException expected) { assertEquals("Native PlayerData is missing", expected.getMessage()); } } - private static void assertSaved(CompoundNBT saved, HeadlessPlayer player, int count, int slot) throws Exception { + private static void assertSaved(CompoundTag saved, HeadlessPlayer player, int count, int slot) throws Exception { assertEquals(SharedConstants.getCurrentVersion().getWorldVersion(), saved.getInt("DataVersion")); assertEquals(ID, saved.getUUID("UUID")); assertFalse(saved.contains("UUIDMost")); assertFalse(saved.contains("UUIDLeast")); - ListNBT inventory = saved.getList("Inventory", 10); + ListTag inventory = saved.getList("Inventory", 10); assertEquals(1, inventory.size()); - CompoundNBT stack = inventory.getCompound(0); + CompoundTag stack = inventory.getCompound(0); assertEquals("minecraft:enchanted_book", stack.getString("id")); assertEquals(count, stack.getByte("Count")); assertEquals(slot, stack.getByte("Slot")); @@ -122,28 +121,28 @@ private static void assertSaved(CompoundNBT saved, HeadlessPlayer player, int co assertEquals(book(count).getCompound("tag"), stack.getCompound("tag")); assertEquals(5, stack.getCompound("tag").getList("StoredEnchantments", 10).size()); assertTrue(ItemStack.matches(ItemStack.of(stack), player.items.getItem(slot))); - assertEquals(inventory, player.items.save(new ListNBT())); + assertEquals(inventory, player.items.save(new ListTag())); } - private static CompoundNBT legacy(int version) throws Exception { - CompoundNBT root = new CompoundNBT(); + private static CompoundTag legacy(int version) throws Exception { + CompoundTag root = new CompoundTag(); root.putInt("DataVersion", version); root.putLong("UUIDMost", ID.getMostSignificantBits()); root.putLong("UUIDLeast", ID.getLeastSignificantBits()); - CompoundNBT stack = book(1); + CompoundTag stack = book(1); stack.putShort("Damage", (short) 0); stack.putByte("Slot", (byte) 0); - ListNBT inventory = new ListNBT(); + ListTag inventory = new ListTag(); inventory.add(stack); root.put("Inventory", inventory); return root; } - private static CompoundNBT book(int count) throws Exception { - CompoundNBT stack = JsonToNBT.parseTag("{id:\"minecraft:enchanted_book\",Count:1b,tag:{StoredEnchantments:[" - + "{id:\"minecraft:efficiency\",lvl:3s},{id:\"steamadvantage:high_explosive\",lvl:1s}," - + "{id:\"steamadvantage:powderless\",lvl:1s},{id:\"steamadvantage:rapid_reload\",lvl:1s}," - + "{id:\"steamadvantage:recoil\",lvl:1s}]}}"); + private static CompoundTag book(int count) throws Exception { + CompoundTag stack = TagParser.parseTag("{id:\"minecraft:enchanted_book\",Count:1b,tag:{StoredEnchantments:[" + + "{id:\"minecraft:efficiency\",lvl:3s},{id:\"basesteam:high_explosive\",lvl:1s}," + + "{id:\"basesteam:powderless\",lvl:1s},{id:\"basesteam:rapid_reload\",lvl:1s}," + + "{id:\"basesteam:recoil\",lvl:1s}]}}"); stack.putByte("Count", (byte) count); return stack; } @@ -151,19 +150,19 @@ private static CompoundNBT book(int count) throws Exception { private static HeadlessPlayer player(int count, int slot) throws Exception { HeadlessPlayer player = allocate(HeadlessPlayer.class); player.setUUID(ID); - player.items = new PlayerInventory(player); + player.items = new Inventory(player); player.items.setItem(slot, ItemStack.of(book(count))); return player; } - private static PlayerData data(File directory) throws Exception { - PlayerData data = allocate(PlayerData.class); - set(PlayerData.class, data, "playerDir", directory); - set(PlayerData.class, data, "fixerUpper", DataFixesManager.getDataFixer()); + private static PlayerDataStorage data(File directory) throws Exception { + PlayerDataStorage data = allocate(PlayerDataStorage.class); + set(PlayerDataStorage.class, data, "playerDir", directory); + set(PlayerDataStorage.class, data, "fixerUpper", DataFixers.getDataFixer()); return data; } - private static PlayerList players(PlayerData data, boolean counting) throws Exception { + private static PlayerList players(PlayerDataStorage data, boolean counting) throws Exception { PlayerList players = counting ? allocate(CountingPlayerList.class) : allocate(HeadlessPlayerList.class); set(PlayerList.class, players, "playerIo", data); return players; @@ -176,23 +175,24 @@ private static void set(Class type, Object owner, String name, Object value) } private static T allocate(Class type) throws Exception { - Field field = Unsafe.class.getDeclaredField("theUnsafe"); + Class unsafe = Class.forName("sun.misc.Unsafe"); + Field field = unsafe.getDeclaredField("theUnsafe"); field.setAccessible(true); - return type.cast(((Unsafe) field.get(null)).allocateInstance(type)); + return type.cast(unsafe.getMethod("allocateInstance", Class.class).invoke(field.get(null), type)); } // Only entity construction/serialization is a fixture. PlayerList's guard, // PlayerData save/load, compressed files, DFU and ItemStacks are native. - private static final class HeadlessPlayer extends ServerPlayerEntity { - PlayerInventory items; - private HeadlessPlayer() { super(null, null, new GameProfile(ID, "Headless"), null); } - @Override public CompoundNBT saveWithoutId(CompoundNBT root) { + private static final class HeadlessPlayer extends ServerPlayer { + Inventory items; + private HeadlessPlayer() { super(null, null, new GameProfile(ID, "Headless")); } + @Override public CompoundTag saveWithoutId(CompoundTag root) { root.putInt("DataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); root.putUUID("UUID", getUUID()); - root.put("Inventory", items.save(new ListNBT())); + root.put("Inventory", items.save(new ListTag())); return root; } - @Override public void load(CompoundNBT root) { + @Override public void load(CompoundTag root) { setUUID(root.getUUID("UUID")); items.load(root.getList("Inventory", 10)); } @@ -204,6 +204,6 @@ private static class HeadlessPlayerList extends PlayerList { private static final class CountingPlayerList extends HeadlessPlayerList { int calls; - @Override protected void save(ServerPlayerEntity player) { calls++; } + @Override protected void save(ServerPlayer player) { calls++; } } } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117ProgressionJsonTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117ProgressionJsonTest.java new file mode 100644 index 00000000..e9e5b6f8 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117ProgressionJsonTest.java @@ -0,0 +1,17 @@ +package zone.moddev.mc.advantageworks; + +import static org.junit.Assert.*; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Test; + +public class Native117ProgressionJsonTest { + @Test public void emptyAndSingletonInputsDoNotReflectIntoJdkPrivateCollections() { + assertEquals("[]", ProgressionProbe.inputJson(Collections.emptyList()).toString()); + assertEquals("[\"minecraft:potato\"]", ProgressionProbe.inputJson(Collections.singletonList("minecraft:potato")).toString()); + } + + @Test public void ingredientOrderAndRepeatedInputsAreRetained() { + assertEquals("[\"a\",\"b\",\"b\"]", ProgressionProbe.inputJson(Arrays.asList("a", "b", "b")).toString()); + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117ReadinessTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117ReadinessTest.java new file mode 100644 index 00000000..476b3312 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117ReadinessTest.java @@ -0,0 +1,98 @@ +package zone.moddev.mc.advantageworks; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; + +public class Native117ReadinessTest { + private Native117Readiness.Observation observation() { + return new Native117Readiness.Observation(new Native117Readiness.Bounds(74, 63, 75, 64), 10, 0); + } + + private List states(boolean loaded, boolean ticking) { + return Arrays.asList(new Native117Readiness.State(74, 63, loaded, ticking), + new Native117Readiness.State(74, 64, loaded, ticking), + new Native117Readiness.State(75, 63, loaded, ticking), + new Native117Readiness.State(75, 64, loaded, ticking)); + } + + @Test public void bothNativeReadinessFactsAreRequiredBeforeCompletion() { + Native117Readiness.Observation check = observation(); + assertFalse(check.observe(11, 1, states(true, false))); + assertFalse(check.observe(12, 2, states(false, true))); + assertThrows(IllegalStateException.class, check::report); + assertTrue(check.observe(13, 3, states(true, true))); + assertTrue(check.passed); + assertEquals("PASS", check.report().get("status").getAsString()); + assertEquals(3, check.report().get("checks").getAsInt()); + } + + @Test public void oneUnreadyBoundaryChunkPreventsAReadyRectangle() { + Native117Readiness.Observation check = observation(); + List rows = states(true, true); + rows.set(3, new Native117Readiness.State(75, 64, true, false)); + assertFalse(check.observe(11, 1, rows)); + assertTrue(check.observe(12, 2, states(true, true))); + assertEquals(2, check.report().getAsJsonArray("transitions").size()); + assertEquals(1, check.report().getAsJsonArray("transitions").get(1).getAsJsonObject() + .getAsJsonArray("changes").size()); + } + + @Test public void unchangedObservationsRetainOnlyActualTransitions() { + Native117Readiness.Observation check = observation(); + assertFalse(check.observe(11, 1, states(false, false))); + assertFalse(check.observe(12, 2, states(false, false))); + assertTrue(check.observe(13, 3, states(true, true))); + assertEquals(2, check.report().getAsJsonArray("transitions").size()); + assertEquals(3, check.report().getAsJsonArray("transitions").get(1).getAsJsonObject().get("check").getAsInt()); + } + + @Test public void alreadyReadyNativeObservationCanFinishOnTheFirstEndCheck() { + Native117Readiness.Observation check = observation(); + assertTrue(check.observe(11, 1, states(true, true))); + assertEquals(1, check.report().get("checks").getAsInt()); + assertEquals(4, check.report().getAsJsonArray("chunks").size()); + assertThrows(IllegalStateException.class, () -> check.observe(12, 2, states(true, true))); + } + + @Test public void naturalCheckLimitReportsFailureNotFalseCompletion() { + Native117Readiness.Observation check = observation(); + for (int tick = 1; tick < Native117Readiness.MAX_CHECKS; tick++) { + assertFalse(check.observe(10 + tick, tick, states(false, false))); + } + assertTrue(check.observe(1210, 1200, states(false, false))); + assertFalse(check.passed); + assertEquals("FAIL", check.report().get("status").getAsString()); + assertEquals(1200, check.report().get("checks").getAsInt()); + } + + @Test public void wallClockLimitRefusesLateReadySamplesAsWellAsUnreadySamples() { + for (boolean ready : new boolean[]{false, true}) { + Native117Readiness.Observation check = observation(); + assertTrue(check.observe(11, Native117Readiness.MAX_WAIT_NANOS + 1, states(ready, ready))); + assertFalse(check.passed); + assertEquals("FAIL", check.report().get("status").getAsString()); + } + } + + @Test public void missingDuplicateAndOutOfOrderChunksCannotSatisfyTheGate() { + Native117Readiness.Observation check = observation(); + assertThrows(IllegalArgumentException.class, () -> check.observe(11, 1, Collections.emptyList())); + List rows = states(true, true); + rows.set(3, rows.get(0)); + assertThrows(IllegalArgumentException.class, () -> check.observe(11, 1, rows)); + assertEquals(0, check.checks); + } + + @Test public void boundsEnforceWorldCoordinatesSizeAndOrder() { + assertEquals(256, new Native117Readiness.Bounds(0, 0, 15, 15).size()); + assertEquals(4, new Native117Readiness.Bounds(74, 63, 75, 64).size()); + for (int[] bounds : new int[][]{{0, 0, 16, 15}, {0, 0, 64, 0}, {1, 0, 0, 0}, + {-1875001, 0, -1875001, 0}, {1875000, 0, 1875000, 0}}) { + assertThrows(IllegalArgumentException.class, + () -> new Native117Readiness.Bounds(bounds[0], bounds[1], bounds[2], bounds[3])); + } + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117RecipeTransportTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117RecipeTransportTest.java new file mode 100644 index 00000000..85704ccc --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117RecipeTransportTest.java @@ -0,0 +1,27 @@ +package zone.moddev.mc.advantageworks; + +import static org.junit.Assert.*; +import io.netty.channel.embedded.EmbeddedChannel; +import net.minecraft.network.Connection; +import io.netty.util.AttributeKey; +import org.junit.BeforeClass; +import org.junit.Test; + +public class Native117RecipeTransportTest { + @BeforeClass public static void bootstrap() { + net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); + } + @Test public void nativeChannelActivationSuppliesRealConnectionAttributesWithoutASocket() { + Connection connection = RecipeUnlockProbe.nativeTransport(); + EmbeddedChannel channel = (EmbeddedChannel) connection.channel(); + try { + assertTrue(channel.isActive()); + AttributeKey key = AttributeKey.valueOf("advantageworks_unit_transport"); + assertNull(connection.channel().attr(key).get()); + connection.channel().attr(key).set("native"); + assertEquals("native", channel.attr(key).get()); + assertFalse(connection.isMemoryConnection()); + } finally { channel.finishAndReleaseAll(); } + assertFalse(channel.isActive()); + } +} 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 new file mode 100644 index 00000000..16a325c7 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117RedstoneReadinessTest.java @@ -0,0 +1,47 @@ +package zone.moddev.mc.advantageworks; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import org.junit.Test; +import static org.junit.Assert.*; + +/** Native scheduling preconditions, independent of the drill's expected outcome. */ +public class Native117RedstoneReadinessTest { + @Test public void blockTickingAndRegistrationCannotReplaceLoadedEntitySections() { + assertFalse(RedstoneReloadProbe.nativeReady(true, true, false, true)); + } + + @Test public void loadedEntitySectionsCannotReplaceEntityTickingReadiness() { + assertFalse(RedstoneReloadProbe.nativeReady(true, true, true, false)); + } + + @Test public void onlyAllFourNativePreconditionsReleaseAcrossEveryCombination() { + for (int mask = 0; mask < 16; mask++) { + assertEquals("native readiness mask " + mask, mask == 15, + RedstoneReloadProbe.nativeReady((mask & 1) != 0, (mask & 2) != 0, + (mask & 4) != 0, (mask & 8) != 0)); + } + } + + @Test public void runtimeRecordsActualQueriesAndNeverWaitsForTheExpectedResult() throws Exception { + String source = Files.readString(Paths.get("src/main/java/zone/moddev/mc/advantageworks/RedstoneReloadProbe.java"), + StandardCharsets.UTF_8); + int start = source.indexOf("private boolean ready(String stage)"); + 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("NativeBlockEntityTicks.contains(world, tile)")); + assertTrue(readiness.contains("all &= nativeReady(ticking, listed, blockAndEntitiesLoaded, entityTicking);")); + for (String field : new String[]{"blockTicking", "tickableListed", "blockAndEntitiesLoaded", "entityTicking"}) { + assertTrue(field, readiness.contains("row.put(\"" + field + "\",")); + } + for (String forbidden : new String[]{"getEnergy(", "progress", "ACTIVE", "frame(", "snapshots(", ".tickUpdate(", ".powerUpdate("}) { + assertFalse(forbidden, readiness.contains(forbidden)); + } + assertTrue(source.contains("private static final int OBSERVATION_TICKS = 16;")); + assertTrue(source.contains("private static final int READINESS_TICKS = 80;")); + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117RespawnRequestTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117RespawnRequestTest.java new file mode 100644 index 00000000..32a6af64 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117RespawnRequestTest.java @@ -0,0 +1,164 @@ +package zone.moddev.mc.advantageworks; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import static org.junit.Assert.*; + +public class Native117RespawnRequestTest { + @Rule public TemporaryFolder temporary = new TemporaryFolder(); + + private String hash(byte[] bytes) throws Exception { + StringBuilder result = new StringBuilder(); + for (byte value : MessageDigest.getInstance("SHA-256").digest(bytes)) result.append(String.format("%02X", value & 255)); + return result.toString(); + } + + private Path fixture() throws Exception { + Path root = temporary.newFolder().toPath(); + Files.writeString(root.resolve("dimension-fixture.json"), + "{\"dimension\":1,\"playerName\":\"Witness\",\"playerUuid\":\"e036830e-303d-3ac3-bd23-062b72eed895\"," + + "\"geometry\":{\"x\":176},\"respawn\":{\"retained\":\"base-only\"}}\n"); + Files.createDirectory(root.resolve("respawn-requests")); + return root.resolve("dimension-fixture.json"); + } + + private JsonObject request(Path fixture, String label) throws Exception { + JsonObject value = new JsonObject(); + value.addProperty("protocol", "native117-respawn-request-v1"); + value.addProperty("id", label); + value.addProperty("baseFixtureSha256", hash(Files.readAllBytes(fixture))); + value.addProperty("keepInventory", true); + value.addProperty("bedMode", "bed"); + return value; + } + + private Path write(Path fixture, String label, JsonObject request) throws Exception { + Path path = fixture.getParent().resolve("respawn-requests").resolve(label + ".json"); + Files.writeString(path, request.toString(), StandardCharsets.UTF_8); + return path; + } + + private JsonObject read(Path fixture, String label) throws Exception { + Method method = DimensionEntityProbe.class.getDeclaredMethod("respawnRequest", Path.class, String.class); + method.setAccessible(true); + try { return (JsonObject) method.invoke(null, fixture, label); } + catch (InvocationTargetException error) { + if (error.getCause() instanceof Exception) throw (Exception) error.getCause(); + throw error; + } + } + + @Test public void immutableRequestMergesOnlyOptionsAndReportsActualRawHashes() throws Exception { + Path fixture = fixture(); String label = "respawn-keep-bed"; + byte[] before = Files.readAllBytes(fixture); + Path path = write(fixture, label, request(fixture, label)); + JsonObject result = read(fixture, label); + JsonObject definition = result.getAsJsonObject("definition"); + JsonObject expected = new JsonParser().parse(new String(before, StandardCharsets.UTF_8)).getAsJsonObject(); + expected.getAsJsonObject("respawn").addProperty("keepInventory", true); + expected.getAsJsonObject("respawn").addProperty("bedMode", "bed"); + assertEquals(expected, definition); + JsonObject evidence = result.getAsJsonObject("evidence"); + assertEquals(label, evidence.get("id").getAsString()); + assertEquals("native117-respawn-request-v1", evidence.get("protocol").getAsString()); + assertEquals("respawn-requests/" + label + ".json", evidence.get("path").getAsString()); + assertEquals(hash(before), evidence.get("baseFixtureSha256").getAsString()); + assertEquals(hash(Files.readAllBytes(path)), evidence.get("sha256").getAsString()); + assertArrayEquals(before, Files.readAllBytes(fixture)); + } + + @Test public void wrongProtocolIdBaseHashAndUnknownFieldsAreRejected() throws Exception { + for (String field : new String[]{"protocol", "id", "baseFixtureSha256", "dimension"}) { + Path fixture = fixture(); String label = "respawn-keep-bed"; + JsonObject value = request(fixture, label); value.addProperty(field, "wrong"); + write(fixture, label, value); + assertThrows(field, IllegalArgumentException.class, () -> read(fixture, label)); + } + } + + @Test public void booleansModesAndLabelOptionsUseExactTypesAndValues() throws Exception { + for (String invalid : new String[]{"1", "\"true\"", "null", "false"}) { + Path fixture = fixture(); String label = "respawn-keep-bed"; + JsonObject value = request(fixture, label); + value.add("keepInventory", new JsonParser().parse(invalid)); + write(fixture, label, value); + assertThrows(IllegalArgumentException.class, () -> read(fixture, label)); + } + Path fixture = fixture(); String label = "respawn-keep-bed"; + JsonObject value = request(fixture, label); value.addProperty("bedMode", "forced"); + write(fixture, label, value); + assertThrows(IllegalArgumentException.class, () -> read(fixture, label)); + } + + @Test public void traversalOtherLabelsMissingFilesAndDuplicateKeysCannotBeConsumed() throws Exception { + Path fixture = fixture(); String label = "respawn-keep-bed"; + for (String invalid : new String[]{"../outside", "respawn", "respawn-keep-bed.json", "respawn-keep-other"}) + assertThrows(IllegalArgumentException.class, () -> read(fixture, invalid)); + assertThrows(Exception.class, () -> read(fixture, label)); + Path path = write(fixture, label, request(fixture, label)); + String text = Files.readString(path); + Files.writeString(path, text.substring(0, text.length() - 1) + ",\"keepInventory\":true}"); + assertThrows(IllegalArgumentException.class, () -> read(fixture, label)); + } + + @Test public void consumedRequestDoesNotHoldOpenTheRootOrItsOwnFile() throws Exception { + Path fixture = fixture(); String label = "respawn-keep-bed"; + Path path = write(fixture, label, request(fixture, label)); + try (java.io.InputStream rootReader = Files.newInputStream(fixture)) { + read(fixture, label); + assertTrue(rootReader.read() >= 0); + } + Files.move(path, path.resolveSibling("closed-request.json")); + Files.move(fixture, fixture.resolveSibling("closed-fixture.json")); + } + + @Test public void everyExistingCaseLabelRetainsItsDimensionAndExactOptions() throws Exception { + for (int dimension : new int[]{-1, 1}) for (String boot : new String[]{"respawn", "respawn-restart"}) + for (boolean keep : new boolean[]{true, false}) for (String mode : new String[]{"forced", "bed", "missing"}) { + Path fixture = fixture(); + JsonObject base = new JsonParser().parse(Files.readString(fixture)).getAsJsonObject(); + base.addProperty("dimension", dimension); Files.writeString(fixture, base.toString()); + byte[] bytes = Files.readAllBytes(fixture); + String label = boot + "-" + (keep ? "keep" : "drop") + "-" + mode; + JsonObject value = request(fixture, label); + value.addProperty("keepInventory", keep); value.addProperty("bedMode", mode); + write(fixture, label, value); + JsonObject result = read(fixture, label), merged = result.getAsJsonObject("definition"); + assertEquals(dimension, merged.get("dimension").getAsInt()); + assertEquals(keep, merged.getAsJsonObject("respawn").get("keepInventory").getAsBoolean()); + assertEquals(mode, merged.getAsJsonObject("respawn").get("bedMode").getAsString()); + assertArrayEquals(bytes, Files.readAllBytes(fixture)); + } + } + + @Test public void malformedOversizedMissingAndTrailingRequestFieldsAreRejected() throws Exception { + Path fixture = fixture(); String label = "respawn-keep-bed"; + JsonObject value = request(fixture, label); + Path path = write(fixture, label, value); + for (String invalid : new String[]{"{", value + " {}", value + " ".repeat(4097)}) { + Files.writeString(path, invalid); + assertThrows(IllegalArgumentException.class, () -> read(fixture, label)); + } + value.remove("bedMode"); Files.writeString(path, value.toString()); + assertThrows(IllegalArgumentException.class, () -> read(fixture, label)); + } + + @Test public void commandDispatchConsumesTheValidatedDefinitionAndAcknowledgesBeforeWriting() throws Exception { + String source = Files.readString(Path.of("src/main/java/zone/moddev/mc/advantageworks/DimensionEntityProbe.java")); + String execute = source.substring(source.indexOf("private void execute("), source.indexOf("private static JsonObject definition()")); + assertTrue(execute.contains("operation.equals(\"respawn\") ? respawnRequest(DEFINITION.toPath(), label) : null")); + assertTrue(execute.contains("request == null ? definition() : request.getAsJsonObject(\"definition\")")); + assertTrue(execute.indexOf("respawnRequest(DEFINITION") < execute.indexOf("snapshot(server, world, definition, operation)")); + int acknowledgement = execute.indexOf("result.add(\"respawnRequest\", request.getAsJsonObject(\"evidence\"))"); + assertTrue(acknowledgement >= 0 && acknowledgement < execute.indexOf("Files.write(")); + } +} 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 new file mode 100644 index 00000000..e726586e --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotTest.java @@ -0,0 +1,156 @@ +package zone.moddev.mc.advantageworks; + +import static org.junit.Assert.*; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.io.ByteArrayInputStream; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.HashSet; +import java.util.List; +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LightningBolt; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.vehicle.Boat; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraftforge.fmlserverevents.FMLServerStoppingEvent; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Native constructors/writers/readers, without starting Minecraft or claiming stop-order runtime proof. */ +public class Native117StoppedEntitiesSnapshotTest { + @Rule public TemporaryFolder temp = new TemporaryFolder(); + @BeforeClass public static void bootstrap() { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); } + + @Test public void requiresExactWorldAndAnActualAllowedMarker() throws Exception { + Path runtime = temp.newFolder().toPath(); + assertFalse(Native117StoppedEntitiesSnapshot.marked(runtime, "AdvantageWorksMigration")); + Files.createDirectory(runtime.resolve("advantage-migration.allow")); + assertFalse(Native117StoppedEntitiesSnapshot.marked(runtime, "AdvantageWorksMigration")); + Files.delete(runtime.resolve("advantage-migration.allow")); + Files.createFile(runtime.resolve("advantage-migration.allow")); + assertTrue(Native117StoppedEntitiesSnapshot.marked(runtime, "AdvantageWorksMigration")); + assertFalse(Native117StoppedEntitiesSnapshot.marked(runtime, "New World")); + assertFalse(Native117StoppedEntitiesSnapshot.marked(runtime, "advantageworksmigration")); + Files.delete(runtime.resolve("advantage-migration.allow")); + Files.createFile(runtime.resolve("advantage-machine-operation.allow")); + assertTrue(Native117StoppedEntitiesSnapshot.marked(runtime, "AdvantageWorksMigration")); + } + + @Test public void retainsNativeItemInventoryNbtAndDoesNotTickOrMutateEntity() throws Exception { + ServerLevel world = Native117EntityContextTest.world(Level.NETHER); + ItemEntity entity = new ItemEntity(world, 8.5, 80, 8.5, new ItemStack(Items.DIRT, 7)); + entity.getPersistentData().putString("Private", "keep exactly"); + CompoundTag before = Native117StoppedEntitiesSnapshot.serialized(entity).copy(); + int ticks = entity.tickCount; + JsonObject row = Native117StoppedEntitiesSnapshot.entities(world, List.of(entity), new HashSet<>()).get(0).getAsJsonObject(); + CompoundTag captured = decode(row); + assertEquals(before, captured); + assertEquals(7, captured.getCompound("Item").getByte("Count")); + assertEquals("keep exactly", captured.getCompound("ForgeData").getString("Private")); + assertEquals("minecraft:the_nether", row.get("worldDimensionKey").getAsString()); + assertEquals(entity.getUUID().toString(), row.get("uuid").getAsString()); + assertEquals(ticks, entity.tickCount); + assertEquals(before, Native117StoppedEntitiesSnapshot.serialized(entity)); + assertFalse(entity.isRemoved()); + } + + @Test public void capturesRootAndPassengerNativeNbtWithoutDismounting() throws Exception { + ServerLevel world = Native117EntityContextTest.world(Level.END); + Boat root = new Boat(EntityType.BOAT, world), passenger = new Boat(EntityType.BOAT, world); + root.setPos(8.5, 80, 8.5); + passenger.setPos(8.5, 81, 8.5); + assertTrue(passenger.startRiding(root, true)); + CompoundTag before = Native117StoppedEntitiesSnapshot.serialized(root).copy(); + JsonArray rows = Native117StoppedEntitiesSnapshot.entities(world, List.of(root, passenger), new HashSet<>()); + assertEquals(2, rows.size()); + assertEquals(before, decode(rows.get(0).getAsJsonObject())); + assertEquals(passenger.getUUID(), decode(rows.get(0).getAsJsonObject()).getList("Passengers", 10).getCompound(0).getUUID("UUID")); + assertEquals(passenger.getUUID(), decode(rows.get(1).getAsJsonObject()).getUUID("UUID")); + assertEquals(decode(rows.get(0).getAsJsonObject()).getList("Passengers", 10).getCompound(0), + decode(rows.get(1).getAsJsonObject())); + assertEquals(root.getUUID().toString(), rows.get(1).getAsJsonObject().get("vehicleUuid").getAsString()); + assertSame(root, passenger.getVehicle()); + assertEquals(before, Native117StoppedEntitiesSnapshot.serialized(root)); + } + + @Test public void nativeLoadAndSavePreserveCapturedItemNbt() throws Exception { + ServerLevel world = Native117EntityContextTest.world(Level.OVERWORLD); + ItemEntity entity = new ItemEntity(world, -0.25, 80, 8.5, new ItemStack(Items.DIRT, 7)); + entity.getPersistentData().putInt("Private", 42); + CompoundTag saved = Native117StoppedEntitiesSnapshot.serialized(entity); + Entity loaded = EntityType.loadEntityRecursive(saved.copy(), world, value -> value); + assertNotNull(loaded); + assertEquals(saved, Native117StoppedEntitiesSnapshot.serialized(loaded)); + } + + @Test public void skipsNativePlayerTypesBeforeTouchingTheirFields() throws Exception { + ServerLevel world = Native117EntityContextTest.world(Level.OVERWORLD); + Class unsafe = Class.forName("sun.misc.Unsafe"); + Field field = unsafe.getDeclaredField("theUnsafe"); field.setAccessible(true); + ServerPlayer player = (ServerPlayer) unsafe.getMethod("allocateInstance", Class.class).invoke(field.get(null), ServerPlayer.class); + assertEquals(0, Native117StoppedEntitiesSnapshot.entities(world, List.of(player), new HashSet<>()).size()); + } + + @Test public void rejectsWrongWorldAndDuplicateEnumeratedUuid() throws Exception { + ServerLevel world = Native117EntityContextTest.world(Level.OVERWORLD); + Boat root = new Boat(EntityType.BOAT, world), other = new Boat(EntityType.BOAT, world); + other.setUUID(root.getUUID()); + try { + Native117StoppedEntitiesSnapshot.entities(world, List.of(root, other), new HashSet<>()); + fail("Duplicate enumerated UUID accepted"); + } catch (IllegalStateException expected) { assertTrue(expected.getMessage().contains("Duplicate")); } + try { + Native117StoppedEntitiesSnapshot.entities(Native117EntityContextTest.world(Level.NETHER), List.of(root), new HashSet<>()); + fail("Wrong dimension instance accepted"); + } catch (IllegalStateException expected) { assertTrue(expected.getMessage().contains("another world")); } + } + + @Test public void nativeWriterRefusalIsAFailureNotASilentSkippedEntity() throws Exception { + ServerLevel world = Native117EntityContextTest.world(Level.OVERWORLD); + LightningBolt transientEntity = new LightningBolt(EntityType.LIGHTNING_BOLT, world); + try { + Native117StoppedEntitiesSnapshot.entities(world, List.of(transientEntity), new HashSet<>()); + fail("Native writer refusal was hidden"); + } catch (IllegalStateException expected) { assertTrue(expected.getMessage().contains("writer refused")); } + } + + @Test public void replacesOnlyRuntimeSnapshotAndLeavesNoTemporaryFile() throws Exception { + Path runtime = temp.newFolder().toPath(); + JsonObject first = new JsonObject(); first.addProperty("status", "CAPTURED"); + JsonObject next = new JsonObject(); next.addProperty("status", "PENDING_STOP"); + Native117StoppedEntitiesSnapshot.write(runtime, first); + Native117StoppedEntitiesSnapshot.write(runtime, next); + assertTrue(Files.readString(runtime.resolve(Native117StoppedEntitiesSnapshot.FILE)).contains("PENDING_STOP")); + try (java.util.stream.Stream files = Files.list(runtime)) { assertEquals(1, files.count()); } + } + + @Test public void entrypointRegistersNativeStoppingHandlerAndRealEnumeration() throws Exception { + assertNotNull(Native117StoppedEntitiesSnapshot.class.getDeclaredMethod("stopping", FMLServerStoppingEvent.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")); + assertTrue(source.contains("world.getAllEntities()")); + assertFalse(source.contains("canUpdate(")); + assertFalse(source.contains(".tick(")); + assertFalse(source.contains(".load(")); + } + + private static CompoundTag decode(JsonObject row) throws Exception { + return NbtIo.readCompressed(new ByteArrayInputStream(Base64.getDecoder().decode(row.get("nbtBase64").getAsString()))); + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2Test.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2Test.java new file mode 100644 index 00000000..e1388db8 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2Test.java @@ -0,0 +1,81 @@ +package zone.moddev.mc.advantageworks; + +import static org.junit.Assert.*; +import com.google.gson.JsonObject; +import java.util.HashSet; +import java.util.List; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LightningBolt; +import net.minecraft.world.entity.decoration.LeashFenceKnotEntity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import org.junit.BeforeClass; +import org.junit.Test; + +public class Native117StoppedEntitiesSnapshotV2Test { + @BeforeClass public static void bootstrap() { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); } + + @Test public void partitionsExactNativeNoSaveHelperWithoutMutation() throws Exception { + ServerLevel world = Native117EntityContextTest.world(Level.NETHER); + LeashFenceKnotEntity knot = new LeashFenceKnotEntity(world, new BlockPos(154, 100, 8)); + ItemEntity item = new ItemEntity(world, 154, 100, 8, new ItemStack(Items.DIRT, 7)); + CompoundTag before = knot.saveWithoutId(new CompoundTag()).copy(); + assertFalse(before.contains("TileX")); + assertFalse(before.contains("TileY")); + assertFalse(before.contains("TileZ")); + assertEquals(154.5, before.getList("Pos", 6).getDouble(0), 0.0); + assertEquals(100.375, before.getList("Pos", 6).getDouble(1), 0.0); + assertEquals(8.5, before.getList("Pos", 6).getDouble(2), 0.0); + CompoundTag persistent = Native117StoppedEntitiesSnapshot.serialized(item).copy(); + JsonObject record = Native117StoppedEntitiesSnapshotV2.partition(world, List.of(knot, item), new HashSet<>()); + assertEquals(1, record.get("entityCount").getAsInt()); + assertEquals(1, record.get("nonpersistentHelperCount").getAsInt()); + assertEquals(2, record.get("observedEntityCount").getAsInt()); + assertEquals("minecraft:leash_knot", record.getAsJsonArray("nonpersistentHelpers").get(0).getAsJsonObject().get("id").getAsString()); + assertEquals(before, knot.saveWithoutId(new CompoundTag())); + assertEquals(persistent, Native117StoppedEntitiesSnapshot.serialized(item)); + assertEquals(0, knot.tickCount); + assertFalse(knot.isRemoved()); + } + + @Test public void unexpectedNoSaveTypeFailsWithActualNativeIdentity() throws Exception { + ServerLevel world = Native117EntityContextTest.world(Level.END); + LightningBolt bolt = new LightningBolt(EntityType.LIGHTNING_BOLT, world); + try { + Native117StoppedEntitiesSnapshotV2.partition(world, List.of(bolt), new HashSet<>()); + fail("Unproven no-save type was skipped"); + } catch (IllegalStateException error) { + assertTrue(error.getMessage().contains(bolt.getUUID().toString())); + assertTrue(error.getMessage().contains("class=net.minecraft.world.entity.LightningBolt")); + assertTrue(error.getMessage().contains("id=minecraft:lightning_bolt")); + assertTrue(error.getMessage().contains("canSerialize=false")); + } + } + + @Test public void helperCannotReplacePersistentUuidOrAnotherWorld() throws Exception { + ServerLevel world = Native117EntityContextTest.world(Level.NETHER); + LeashFenceKnotEntity knot = new LeashFenceKnotEntity(world, new BlockPos(154, 100, 8)); + ItemEntity item = new ItemEntity(world, 154, 100, 8, new ItemStack(Items.DIRT, 7)); + item.setUUID(knot.getUUID()); + try { + Native117StoppedEntitiesSnapshotV2.partition(world, List.of(knot, item), new HashSet<>()); + fail("Duplicate classification UUID accepted"); + } catch (IllegalStateException expected) { assertTrue(expected.toString().contains("Persistent writer")); } + try { + Native117StoppedEntitiesSnapshotV2.partition(Native117EntityContextTest.world(Level.END), List.of(knot), new HashSet<>()); + fail("Wrong-world helper accepted"); + } catch (IllegalStateException expected) { assertTrue(expected.toString().contains("another world")); } + } + + @Test public void resourceIsExactIndependentNativeControl() throws Exception { + assertEquals("59B45FFB7D3CA1F726EFDE111D1716FB0583B092D4F7EAA1A2AE09FA71C7F73F", + Native117StoppedEntitiesSnapshotV2.sha(Native117StoppedEntitiesSnapshotV2.contract())); + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116TestFlatTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117TestFlatTest.java similarity index 58% rename from test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116TestFlatTest.java rename to test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117TestFlatTest.java index addc53d7..1a0c66bc 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native116TestFlatTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117TestFlatTest.java @@ -2,43 +2,43 @@ import com.google.gson.JsonObject; import com.mojang.serialization.JsonOps; -import net.minecraft.block.BlockState; -import net.minecraft.block.Blocks; -import net.minecraft.util.registry.Bootstrap; -import net.minecraft.util.registry.DynamicRegistries; -import net.minecraft.util.registry.Registry; -import net.minecraft.util.registry.WorldGenSettingsExport; -import net.minecraft.world.gen.FlatChunkGenerator; -import net.minecraft.world.gen.FlatGenerationSettings; -import net.minecraft.world.gen.settings.DimensionGeneratorSettings; +import net.minecraft.world.level.block.state.BlockState; +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.world.level.levelgen.FlatLevelSource; +import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; +import net.minecraft.world.level.levelgen.WorldGenSettings; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.*; -public class Native116TestFlatTest { - @BeforeClass public static void bootstrap() { Bootstrap.bootStrap(); } +public class Native117TestFlatTest { + @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); } @Test public void constructorHasExactLayersAndNoStructures() { - DynamicRegistries registries = DynamicRegistries.builtin(); - FlatChunkGenerator generator = (FlatChunkGenerator) Native116TestFlat.createType().createChunkGenerator( + RegistryAccess registries = RegistryAccess.builtin(); + FlatLevelSource generator = (FlatLevelSource) Native117TestFlat.createType().createChunkGenerator( registries.registryOrThrow(Registry.BIOME_REGISTRY), registries.registryOrThrow(Registry.NOISE_GENERATOR_SETTINGS_REGISTRY), 42L, ""); - FlatGenerationSettings flat = generator.settings(); + FlatLevelGeneratorSettings flat = generator.settings(); assertEquals(3, flat.getLayersInfo().size()); - BlockState[] layers = flat.getLayers(); - assertEquals(Blocks.BEDROCK.defaultBlockState(), layers[0]); - assertEquals(Blocks.DIRT.defaultBlockState(), layers[1]); - assertEquals(Blocks.DIRT.defaultBlockState(), layers[2]); - assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), layers[3]); - for (int y = 4; y < layers.length; y++) assertNull(layers[y]); + java.util.List layers = flat.getLayers(); + assertEquals(4, layers.size()); + assertEquals(Blocks.BEDROCK.defaultBlockState(), layers.get(0)); + 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()); } @Test public void fullSaveCodecRetainsVanillaFlatIdentityAndAllDimensions() { - DynamicRegistries registries = DynamicRegistries.builtin(); - DimensionGeneratorSettings settings = Native116TestFlat.createType().createSettings(registries, 42L, false, false, ""); - JsonObject saved = DimensionGeneratorSettings.CODEC.encodeStart(WorldGenSettingsExport.create(JsonOps.INSTANCE, registries), settings) + RegistryAccess registries = RegistryAccess.builtin(); + WorldGenSettings settings = Native117TestFlat.createType().createSettings(registries, 42L, false, false, ""); + JsonObject saved = WorldGenSettings.CODEC.encodeStart(RegistryWriteOps.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()); @@ -54,13 +54,13 @@ public class Native116TestFlatTest { 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(Native116TestFlat.ID)); + assertFalse(saved.toString().contains(Native117TestFlat.ID)); } @Test public void flatJsonCreationSettingsAreRefused() { - DynamicRegistries registries = DynamicRegistries.builtin(); + RegistryAccess registries = RegistryAccess.builtin(); try { - Native116TestFlat.createType().createChunkGenerator(registries.registryOrThrow(Registry.BIOME_REGISTRY), + Native117TestFlat.createType().createChunkGenerator(registries.registryOrThrow(Registry.BIOME_REGISTRY), registries.registryOrThrow(Registry.NOISE_GENERATOR_SETTINGS_REGISTRY), 42L, "{}"); fail("Flat JSON must not enter the vanilla JsonOps path"); } catch (IllegalArgumentException expected) { diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeEntityDataTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeEntityDataTest.java new file mode 100644 index 00000000..005bc32a --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeEntityDataTest.java @@ -0,0 +1,29 @@ +package zone.moddev.mc.advantageworks; + +import static org.junit.Assert.*; + +import java.lang.reflect.Field; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.Entity; +import org.junit.Test; + +public class NativeEntityDataTest { + @Test public void acceptsOnlyTheExactVerifiedNativeField() throws Exception { + Field nativeField = Entity.class.getDeclaredField("persistentData"); + assertSame(nativeField, NativeEntityData.requireNativeField(nativeField)); + assertThrows(IllegalStateException.class, () -> NativeEntityData.requireNativeField(null)); + assertThrows(IllegalStateException.class, + () -> NativeEntityData.requireNativeField(Entity.class.getDeclaredField("level"))); + assertThrows(IllegalStateException.class, + () -> NativeEntityData.requireNativeField(Impostor.class.getDeclaredField("persistentData"))); + } + + @Test public void missingEntityFailsClosedInsteadOfMatching() { + assertThrows(NullPointerException.class, () -> NativeEntityData.containsString(null, "tag")); + assertThrows(NullPointerException.class, () -> NativeEntityData.string(null, "tag")); + } + + private static final class Impostor { + private CompoundTag persistentData; + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeEntityReadSelectorsTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeEntityReadSelectorsTest.java new file mode 100644 index 00000000..eeed0fbb --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeEntityReadSelectorsTest.java @@ -0,0 +1,93 @@ +package zone.moddev.mc.advantageworks; + +import static org.junit.Assert.*; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.vehicle.Boat; +import net.minecraft.world.level.Level; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class NativeEntityReadSelectorsTest { + @Parameterized.Parameters(name = "{0}-{1}") public static Collection cases() { + List cases = new ArrayList<>(); + for (String selector : new String[]{"actor", "dimension", "station"}) { + for (String data : new String[]{"absent", "empty", "unrelated", "wrongType", "tagged", "otherValue"}) { + cases.add(new Object[]{selector, data}); + } + } + return cases; + } + + @BeforeClass public static void bootstrap() { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); } + + private final String selector; + private final String scenario; + + public NativeEntityReadSelectorsTest(String selector, String scenario) { + this.selector = selector; + this.scenario = scenario; + } + + @Test public void selectionLeavesExactLiveAndSavedStateUntouched() throws Exception { + ObservedBoat entity = new ObservedBoat(Native117EntityContextTest.world(Level.OVERWORLD)); + Field field = Entity.class.getDeclaredField("persistentData"); + field.setAccessible(true); + String key = selector.equals("actor") ? "AdvWorksActor" + : selector.equals("dimension") ? "AdvWorksDimensionEntity" : "AdvantageWorksStation"; + CompoundTag data = scenario.equals("absent") ? null : new CompoundTag(); + if (scenario.equals("unrelated") || scenario.equals("tagged") || scenario.equals("otherValue")) { + data.putLong("PrivateLong", 987654321L); + CompoundTag nested = new CompoundTag(); + nested.putIntArray("Array", new int[]{2, 3, 5}); + data.put("PrivateNested", nested); + } + if (scenario.equals("wrongType")) data.putInt(key, 42); + if (scenario.equals("tagged")) data.putString(key, "station"); + if (scenario.equals("otherValue")) data.putString(key, "another station"); + field.set(entity, data); + entity.persistentReads = 0; + CompoundTag before = entity.saveWithoutId(new CompoundTag()).copy(); + int ticks = entity.tickCount; + for (int observation = 0; observation < 2; observation++) { + int serializations = entity.serializations; + boolean selected; + if (selector.equals("actor")) selected = ActorProbe.witnessed(entity); + else if (selector.equals("station")) selected = WorksController.StationOwnership.matches(entity, "station"); + else { + Method method = DimensionEntityProbe.class.getDeclaredMethod("witnessed", Entity.class); + method.setAccessible(true); + selected = (boolean) method.invoke(null, entity); + } + boolean expected = scenario.equals("tagged") || (scenario.equals("otherValue") && !selector.equals("station")); + assertEquals(expected, selected); + assertSame("Rejected and accepted entities must retain the exact persistent-data field", data, field.get(entity)); + assertEquals("Read selectors must never call the allocating getter", 0, entity.persistentReads); + assertEquals("Selection must not serialize an entity or invoke its save callbacks", serializations, entity.serializations); + assertEquals(before, entity.saveWithoutId(new CompoundTag())); + assertEquals(data != null, before.contains("ForgeData")); + assertEquals(ticks, entity.tickCount); + assertFalse(entity.isRemoved()); + } + } + + private static final class ObservedBoat extends Boat { + int persistentReads; + int serializations; + ObservedBoat(Level world) { super(EntityType.BOAT, world); } + @Override public CompoundTag getPersistentData() { persistentReads++; return super.getPersistentData(); } + @Override public CompoundTag saveWithoutId(CompoundTag tag) { serializations++; return super.saveWithoutId(tag); } + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeEntityTickGateTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeEntityTickGateTest.java new file mode 100644 index 00000000..f8109ecb --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeEntityTickGateTest.java @@ -0,0 +1,65 @@ +package zone.moddev.mc.advantageworks; + +import static org.junit.Assert.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.vehicle.Boat; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.entity.EntityTickList; +import org.junit.BeforeClass; +import org.junit.Test; + +public class NativeEntityTickGateTest { + @BeforeClass public static void bootstrap() { + net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); + } + + @Test public void actualNativeTickerIsHeldWithoutRemovingItsTrackingRegistration() throws Exception { + ServerLevel world = Native117EntityContextTest.world(Level.OVERWORLD); + java.lang.reflect.Field field = ServerLevel.class.getDeclaredField("entityTickList"); field.setAccessible(true); + EntityTickList original = (EntityTickList) field.get(world); + Boat first = new Boat(EntityType.BOAT, world), second = new Boat(EntityType.BOAT, world); + original.add(first); original.add(second); + NativeEntityHold hold = new NativeEntityHold(); + hold.hold(first); + NativeEntityTickGate gate = NativeEntityTickGate.of(world); + List actual = new ArrayList<>(); gate.forEach(actual::add); + assertEquals(Arrays.asList(second), actual); + assertFalse(gate.contains(first)); + assertTrue("Native registration is retained", original.contains(first)); + hold.restore(); + actual.clear(); gate.forEach(actual::add); + assertEquals(Arrays.asList(first, second), actual); + assertTrue(gate.contains(first)); assertSame(world, first.level); assertFalse(first.isRemoved()); + } + + @Test public void joinBeforeNativeRegistrationIsStillHeldAndAllOtherOperationsDelegate() throws Exception { + ServerLevel world = Native117EntityContextTest.world(Level.OVERWORLD); + Boat first = new Boat(EntityType.BOAT, world), other = new Boat(EntityType.BOAT, world); + NativeEntityHold hold = new NativeEntityHold(); hold.hold(first); + NativeEntityTickGate gate = NativeEntityTickGate.of(world); + gate.add(first); gate.add(other); + List actual = new ArrayList<>(); gate.forEach(actual::add); + assertEquals(Arrays.asList(other), actual); + gate.remove(other); + actual.clear(); gate.forEach(actual::add); assertTrue(actual.isEmpty()); + hold.restore(); + gate.forEach(actual::add); assertEquals(Arrays.asList(first), actual); + } + + @Test public void overlappingHoldsReleaseOnlyTheirOwnNativeGate() throws Exception { + ServerLevel world = Native117EntityContextTest.world(Level.OVERWORLD); + Boat boat = new Boat(EntityType.BOAT, world); + NativeEntityTickGate gate = NativeEntityTickGate.of(world); gate.add(boat); + NativeEntityHold first = new NativeEntityHold(), second = new NativeEntityHold(); + first.hold(boat); second.hold(boat); + first.restore(); + assertFalse(boat.canUpdate()); assertFalse(gate.contains(boat)); + second.restore(); + assertTrue(boat.canUpdate()); assertTrue(gate.contains(boat)); + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/SteamOperationalSourceContractTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/SteamOperationalSourceContractTest.java index 71748f52..06a4e243 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/SteamOperationalSourceContractTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/SteamOperationalSourceContractTest.java @@ -44,8 +44,17 @@ public class SteamOperationalSourceContractTest { Set families = new LinkedHashSet<>(); for (MethodNode method : readClass("SteamOperationalSupplementalCases").methods) { for (AbstractInsnNode instruction : method.instructions.toArray()) { - if (!(instruction instanceof LdcInsnNode)) continue; - Object value = ((LdcInsnNode) instruction).cst; + Object value = instruction instanceof LdcInsnNode ? ((LdcInsnNode)instruction).cst : null; + if (instruction instanceof org.objectweb.asm.tree.InvokeDynamicInsnNode) { + org.objectweb.asm.tree.InvokeDynamicInsnNode concat = (org.objectweb.asm.tree.InvokeDynamicInsnNode)instruction; + if (concat.bsm.getOwner().equals("java/lang/invoke/StringConcatFactory")) { + for (Object argument : concat.bsmArgs) if (argument instanceof String) { + String template = (String)argument; + int dynamic = template.indexOf('\u0001'); + value = dynamic < 0 ? template : template.substring(0, dynamic); + } + } + } if (!(value instanceof String)) continue; String text = (String) value; assertFalse("Supplemental IDs must not escape the steam group", text.startsWith("steam-extra/")); @@ -72,7 +81,7 @@ public class SteamOperationalSourceContractTest { } else if (instruction instanceof LdcInsnNode) { Object value = ((LdcInsnNode) instruction).cst; if ("MUSKET_DAMAGE".equals(value)) damageField = true; - if ("zone.moddev.mc.steamadvantage.SteamAdvantage".equals(value)) serverConfig = true; + if ("zone.moddev.mc.basesteam.SteamAdvantage".equals(value)) serverConfig = true; } } } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/WorksDefinitionTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/WorksDefinitionTest.java index 5b0de911..d1ca07a7 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/WorksDefinitionTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/WorksDefinitionTest.java @@ -19,7 +19,7 @@ public void steamNetworkDrillsHaveImmediateBedrockStops() { if (placement.block.equals("minecraft:bedrock")) stops.add(Arrays.toString(placement.pos)); } for (WorksDefinition.Placement placement : station.placements) { - if (!placement.block.equals("steamadvantage:steam_drill")) continue; + if (!placement.block.equals("basesteam:steam_drill")) continue; int[] target = placement.pos.clone(); assertTrue("Network drill must face up or south", placement.meta == 1 || placement.meta == 3); if (placement.meta == 1) target[1]++; else target[2]++; @@ -42,7 +42,7 @@ public void distillationAssertionsCountTheWholeCircuit() { assertEquals(1000.0, assertion.value, 0.0); assertEquals("Tank", assertion.nbtPath); assertEquals("equals", assertion.operator); - if (assertion.id.endsWith("-fluid")) assertEquals("poweradvantage:refined_oil", assertion.fluid); + if (assertion.id.endsWith("-fluid")) assertEquals("basepower:refined_oil", assertion.fluid); } } } @@ -88,58 +88,58 @@ public void completeWorksCoversEveryRegisteredComponent() { } assertContainsAll(blocks, - "poweradvantage:fluid_drain", "poweradvantage:fluid_discharge", - "poweradvantage:fluid_storage_tank", "poweradvantage:fluid_metal_tank", - "poweradvantage:still", "poweradvantage:fluid_pipe", - "poweradvantage:fluid_pipe_terminal", "poweradvantage:fluid_switch", - "poweradvantage:item_conveyor", "poweradvantage:item_filter_block", - "poweradvantage:item_filter_food", "poweradvantage:item_filter_fuel", - "poweradvantage:item_filter_inventory", "poweradvantage:item_filter_ore", - "poweradvantage:item_filter_plant", "poweradvantage:item_filter_smelt", - "poweradvantage:item_filter_overflow", "poweradvantage:steel_frame", - "poweradvantage:infinite_steam", "poweradvantage:infinite_electricity", - "poweradvantage:infinite_quantum", "poweradvantage:crude_oil", - "poweradvantage:refined_oil", "poweradvantage:converter_rf_steam", - "poweradvantage:converter_rf_electricity", "poweradvantage:converter_rf_quantum", - "poweradvantage:converter_tr_electricity"); + "basepower:fluid_drain", "basepower:fluid_discharge", + "basepower:fluid_storage_tank", "basepower:fluid_metal_tank", + "basepower:still", "basepower:fluid_pipe", + "basepower:fluid_pipe_terminal", "basepower:fluid_switch", + "basepower:item_conveyor", "basepower:item_filter_block", + "basepower:item_filter_food", "basepower:item_filter_fuel", + "basepower:item_filter_inventory", "basepower:item_filter_ore", + "basepower:item_filter_plant", "basepower:item_filter_smelt", + "basepower:item_filter_overflow", "basepower:steel_frame", + "basepower:infinite_steam", "basepower:infinite_electricity", + "basepower:infinite_quantum", "basepower:crude_oil", + "basepower:refined_oil", "basepower:converter_rf_steam", + "basepower:converter_rf_electricity", "basepower:converter_rf_quantum", + "basepower:converter_tr_electricity"); assertContainsAll(blocks, - "steamadvantage:steam_pipe", "steamadvantage:steam_track", - "steamadvantage:steam_boiler_coal", "steamadvantage:steam_boiler_electric", - "steamadvantage:steam_boiler_geothermal", "steamadvantage:steam_boiler_oil", - "steamadvantage:steam_tank", "steamadvantage:steam_furnace", - "steamadvantage:steam_crusher", "steamadvantage:steam_drill", - "steamadvantage:steam_elevator", "steamadvantage:steam_switch", - "steamadvantage:steam_still", "steamadvantage:steam_pump", - "steamadvantage:drillbit", "steamadvantage:platform", - "steamadvantage:pump_pipe_steam"); + "basesteam:steam_pipe", "basesteam:steam_track", + "basesteam:steam_boiler_coal", "basesteam:steam_boiler_electric", + "basesteam:steam_boiler_geothermal", "basesteam:steam_boiler_oil", + "basesteam:steam_tank", "basesteam:steam_furnace", + "basesteam:steam_crusher", "basesteam:steam_drill", + "basesteam:steam_elevator", "basesteam:steam_switch", + "basesteam:steam_still", "basesteam:steam_pump", + "basesteam:drillbit", "basesteam:platform", + "basesteam:pump_pipe_steam"); assertContainsAll(blocks, - "electricadvantage:electric_conduit", "electricadvantage:li_ore", - "electricadvantage:sulfur_ore", "electricadvantage:electric_track", - "electricadvantage:laser_turret", "electricadvantage:laser_turret_evil", - "electricadvantage:led_bar", "electricadvantage:photovoltaic_generator", - "electricadvantage:hydroelectric_generator", "electricadvantage:steam_powered_generator", - "electricadvantage:electric_furnace", "electricadvantage:electric_battery_array", - "electricadvantage:electric_crusher", "electricadvantage:electric_drill", - "electricadvantage:electric_fabricator", "electricadvantage:growth_chamber", - "electricadvantage:growth_chamber_controller", "electricadvantage:electric_oven", - "electricadvantage:electric_switch", "electricadvantage:electric_pump", - "electricadvantage:electric_still", "electricadvantage:plastic_refinery", - "electricadvantage:pump_pipe_electric"); + "baseelectric:electric_conduit", "baseelectric:li_ore", + "baseelectric:sulfur_ore", "baseelectric:electric_track", + "baseelectric:laser_turret", "baseelectric:laser_turret_evil", + "baseelectric:led_bar", "baseelectric:photovoltaic_generator", + "baseelectric:hydroelectric_generator", "baseelectric:steam_powered_generator", + "baseelectric:electric_furnace", "baseelectric:electric_battery_array", + "baseelectric:electric_crusher", "baseelectric:electric_drill", + "baseelectric:electric_fabricator", "baseelectric:growth_chamber", + "baseelectric:growth_chamber_controller", "baseelectric:electric_oven", + "baseelectric:electric_switch", "baseelectric:electric_pump", + "baseelectric:electric_still", "baseelectric:plastic_refinery", + "baseelectric:pump_pipe_electric"); assertContainsAll(items, - "poweradvantage:starch", "poweradvantage:bioplastic_ingot", - "poweradvantage:sprocket", "poweradvantage:rotator_tool", - "steamadvantage:steam_governor", "steamadvantage:steam_drill_bit", - "steamadvantage:blackpowder_cartridge", "steamadvantage:musket", - "electricadvantage:blank_circuit_board", "electricadvantage:control_circuit", - "electricadvantage:integrated_circuit", "electricadvantage:psu", - "electricadvantage:silicon_ingot", "electricadvantage:silicon_mix", - "electricadvantage:solder_mix", "electricadvantage:solder", - "electricadvantage:lead_acid_battery", "electricadvantage:nickel_hydride_battery", - "electricadvantage:alkaline_battery", "electricadvantage:lithium_battery", - "electricadvantage:li_ingot", "electricadvantage:li_powder", - "electricadvantage:li_nugget", "electricadvantage:sulfur_powder", - "electricadvantage:petrolplastic_ingot"); + "basepower:starch", "basepower:bioplastic_ingot", + "basepower:sprocket", "basepower:rotator_tool", + "basesteam:steam_governor", "basesteam:steam_drill_bit", + "basesteam:blackpowder_cartridge", "basesteam:musket", + "baseelectric:blank_circuit_board", "baseelectric:control_circuit", + "baseelectric:integrated_circuit", "baseelectric:psu", + "baseelectric:silicon_ingot", "baseelectric:silicon_mix", + "baseelectric:solder_mix", "baseelectric:solder", + "baseelectric:lead_acid_battery", "baseelectric:nickel_hydride_battery", + "baseelectric:alkaline_battery", "baseelectric:lithium_battery", + "baseelectric:li_ingot", "baseelectric:li_powder", + "baseelectric:li_nugget", "baseelectric:sulfur_powder", + "baseelectric:petrolplastic_ingot"); assertTrue(manual.toString().contains("SA-110-001")); assertTrue(manual.toString().contains("SA-110-002")); diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/WorldgenAsyncSamplingTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/WorldgenAsyncSamplingTest.java new file mode 100644 index 00000000..5b31acaf --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/WorldgenAsyncSamplingTest.java @@ -0,0 +1,209 @@ +package zone.moddev.mc.advantageworks; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import org.junit.Test; +import static org.junit.Assert.*; + +public class WorldgenAsyncSamplingTest { + @Test public void pendingNativeFuturesReturnEveryTickAndHoldExactlyFourSlots() { + ControlledWorld world = new ControlledWorld(); + WorldgenSamplingJob job = population(world); + job.tick(); + assertEquals(4, world.active.size()); + for (int tick = 0; tick < 100; tick++) job.tick(); + assertEquals(4, world.requests.size()); + assertEquals(0, world.reads); + assertEquals(WorldgenSamplingJob.Phase.POPULATE, job.phase()); + job.close(); + assertEquals(0, world.active.size()); + assertEquals(1, world.closes); + } + + @Test public void outOfOrderCompletionDoesNotChangePopulationOrderOrSkipTheHead() { + ControlledWorld world = new ControlledWorld(); + WorldgenSamplingJob job = population(world); + job.tick(); + List first = new ArrayList<>(world.active.keySet()); + for (int index = 3; index > 0; index--) world.active.get(first.get(index)).complete(world.chunk); + job.tick(); + assertTrue(world.released.isEmpty()); + assertEquals(4, world.requests.size()); + world.active.get(first.get(0)).complete(world.chunk); + job.tick(); + assertEquals(first, world.released); + assertEquals(8, world.requests.size()); + assertEquals(4, world.active.size()); + assertEquals(0, world.reads); + job.close(); + } + + @Test public void anExceptionalFutureBehindAPendingHeadFailsWithoutAPartialReport() { + ControlledWorld world = new ControlledWorld(); + WorldgenSamplingJob job = population(world); + job.tick(); + new ArrayList<>(world.active.values()).get(2).completeExceptionally(new IllegalStateException("native failure")); + job.tick(); + failedAndClosed(job, world); + assertTrue(job.failure().toString().contains("native failure")); + } + + @Test public void aLostNullChunkBehindAPendingHeadFailsInsteadOfBecomingEmptyTerrain() { + ControlledWorld world = new ControlledWorld(); + WorldgenSamplingJob job = population(world); + job.tick(); + new ArrayList<>(world.active.values()).get(1).complete(null); + job.tick(); + failedAndClosed(job, world); + assertTrue(job.failure().getMessage().contains("chunk was lost")); + } + + @Test public void aNullRequestFutureIsNotAcceptedAsReadiness() { + ControlledWorld world = new ControlledWorld(); + world.nullRequest = true; + WorldgenSamplingJob job = population(world); + job.tick(); + failedAndClosed(job, world); + assertTrue(job.failure().getMessage().contains("request was null")); + } + + @Test public void countLoadsAlsoYieldAndReadAtMostTwoCompletedFullChunksPerTick() { + ControlledWorld world = new ControlledWorld(); + WorldgenSamplingJob job = population(world); + while (job.phase() != WorldgenSamplingJob.Phase.COUNT) { + world.completeAll(); + job.tick(); + assertNull(job.failure()); + } + assertEquals(0, world.reads); + job.tick(); + for (int tick = 0; tick < 10; tick++) job.tick(); + assertEquals(4, world.active.size()); + assertEquals(0, world.reads); + world.completeAll(); + job.tick(); + assertEquals(2L * 16 * 16 * 97, world.reads); + assertEquals(WorldgenSamplingJob.Phase.COUNT, job.phase()); + job.close(); + assertTrue(world.active.isEmpty()); + } + + @Test public void releasingTheJobSlotCancelsAllOwnedPendingRequestsOnce() { + ControlledWorld world = new ControlledWorld(); + WorldgenSamplingJob job = population(world); + WorldgenSamplingJob.Slot slot = new WorldgenSamplingJob.Slot(); + slot.start(job); + job.tick(); + List> futures = new ArrayList<>(world.active.values()); + slot.release(); + slot.release(); + assertNull(slot.current()); + failedAndClosed(job, world); + for (CompletableFuture future : futures) assertTrue(future.isCancelled()); + job.tick(); + assertEquals(1, world.closes); + } + + @Test public void zeroCountsStillRequireTheComplete867ChunkInventoryAndAllReads() { + ControlledWorld world = new ControlledWorld(); + WorldgenSamplingJob job = new WorldgenSamplingJob(world, 0, 70, 0, 64, 8); + int ticks = 0; + while (!job.finished()) { + world.completeAll(); + int requests = world.requests.size(); + long reads = world.reads; + job.tick(); + assertTrue(world.requests.size() - requests <= 4); + assertTrue(world.reads - reads <= 2L * 16 * 16 * 97); + assertTrue(world.active.size() <= 4); + assertTrue(++ticks < 5000); + } + assertNull(job.failure()); + assertEquals(867, job.result().get("chunksScanned")); + assertEquals(867L * 16 * 16 * 97, world.reads); + for (Object count : ((Map) job.result().get("counts")).values()) assertEquals(0L, count); + assertEquals(1, world.closes); + assertTrue(world.active.isEmpty()); + } + + @Test public void ticketCleanupFailureCannotTurnIntoACompletedSample() { + ControlledWorld world = new ControlledWorld(); + world.failClose = true; + WorldgenSamplingJob job = population(world); + job.close(); + assertEquals(WorldgenSamplingJob.Phase.FAILED, job.phase()); + assertEquals(1, job.failure().getSuppressed().length); + requireNoResult(job); + } + + private static WorldgenSamplingJob population(ControlledWorld world) { + WorldgenSamplingJob job = new WorldgenSamplingJob(world, 0, 70, 0, 64, 1); + for (int tick = 0; job.phase() != WorldgenSamplingJob.Phase.POPULATE; tick++) { + assertTrue(tick < 1000); + job.tick(); + } + return job; + } + + private static void failedAndClosed(WorldgenSamplingJob job, ControlledWorld world) { + assertEquals(WorldgenSamplingJob.Phase.FAILED, job.phase()); + assertNotNull(job.failure()); + assertTrue(world.active.isEmpty()); + assertEquals(1, world.closes); + requireNoResult(job); + } + + private static void requireNoResult(WorldgenSamplingJob job) { + try { job.result(); fail("Incomplete native chunks produced a sample"); } + catch (IllegalStateException expected) { assertTrue(expected.getMessage().contains("not complete")); } + } + + private static final class ControlledWorld implements WorldgenSamplingJob.WorldAccess { + final Map> active = new LinkedHashMap<>(); + final List requests = new ArrayList<>(), released = new ArrayList<>(); + long reads; + int closes; + boolean nullRequest, failClose; + final WorldgenSamplingJob.SampleChunk chunk = new WorldgenSamplingJob.SampleChunk() { + @Override public boolean centerMatches(String expected) { return true; } + @Override public String blockName(int x, int y, int z) { + assertTrue(y >= 0 && y <= 96); + reads++; + return null; + } + }; + + @Override public boolean matchesBiome(String expected, int x, int y, int z) { + return z == 0 && x == ("DESERT".equals(expected) ? -1024 : 1024); + } + + @Override public CompletableFuture requestChunk(int x, int z, boolean counting) { + long key = ((long) x << 32) ^ (z & 0xffffffffL); + assertFalse(active.containsKey(key)); + CompletableFuture future = new CompletableFuture<>(); + active.put(key, future); + requests.add(key); + return nullRequest ? null : future; + } + + @Override public void releaseChunk(int x, int z) { + long key = ((long) x << 32) ^ (z & 0xffffffffL); + assertNotNull(active.remove(key)); + released.add(key); + } + + @Override public void close() { + closes++; + for (CompletableFuture future : active.values()) future.cancel(false); + active.clear(); + if (failClose) throw new IllegalStateException("ticket cleanup failed"); + } + + void completeAll() { + for (CompletableFuture future : active.values()) future.complete(chunk); + } + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/WorldgenSamplingJobTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/WorldgenSamplingJobTest.java index 147cbeab..d847a75b 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/WorldgenSamplingJobTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/WorldgenSamplingJobTest.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CompletableFuture; import org.junit.Test; import static org.junit.Assert.*; @@ -182,10 +183,10 @@ private static Map synchronousReference(FakeWorld world, int rad for (long packed : chunks) square(halo, (int) (packed >> 32), (int) packed, 2); for (long packed : halo) world.populateChunk((int) (packed >> 32), (int) packed); Map expected = new LinkedHashMap<>(); - expected.put("poweradvantage:crude_oil", "DESERT"); + expected.put("basepower:crude_oil", "DESERT"); expected.put("mineralogy:crude_oil", "OCEAN"); - expected.put("electricadvantage:li_ore", null); - expected.put("electricadvantage:sulfur_ore", null); + expected.put("baseelectric:li_ore", null); + expected.put("baseelectric:sulfur_ore", null); expected.put("mineralogy:sulfur_ore", null); Map counts = new LinkedHashMap<>(), violations = new LinkedHashMap<>(), boundaries = new LinkedHashMap<>(); for (String id : expected.keySet()) { @@ -243,12 +244,24 @@ private static final class FakeWorld implements WorldgenSamplingJob.WorldAccess return region && (y == 64 || ((x + z) & 1) != 0); } - @Override public void populateChunk(int x, int z) { + @Override public CompletableFuture requestChunk(int x, int z, boolean counting) { + if (counting) return CompletableFuture.completedFuture(sampleChunk(x, z)); + populateChunk(x, z); + return CompletableFuture.completedFuture(new WorldgenSamplingJob.SampleChunk() { + @Override public boolean centerMatches(String expected) { throw new AssertionError("Population was counted"); } + @Override public String blockName(int bx, int y, int bz) { throw new AssertionError("Population was counted"); } + }); + } + + @Override public void releaseChunk(int x, int z) { } + @Override public void close() { } + + public void populateChunk(int x, int z) { if (failPopulation) throw new IllegalStateException("population failed"); populated.add(pack(x, z)); } - @Override public WorldgenSamplingJob.SampleChunk sampleChunk(int chunkX, int chunkZ) { + public WorldgenSamplingJob.SampleChunk sampleChunk(int chunkX, int chunkZ) { sampled.add(pack(chunkX, chunkZ)); return new WorldgenSamplingJob.SampleChunk() { @Override public boolean centerMatches(String expected) { @@ -263,10 +276,10 @@ private static final class FakeWorld implements WorldgenSamplingJob.WorldAccess minY = Math.min(minY, y); maxY = Math.max(maxY, y); if ((z & 15) != 0) return null; - if ((x & 15) == 0 && y == 0) return "poweradvantage:crude_oil"; + if ((x & 15) == 0 && y == 0) return "basepower:crude_oil"; if ((x & 15) == 1 && y == 96) return "mineralogy:crude_oil"; - if ((x & 15) == 2 && y == 48) return "electricadvantage:li_ore"; - if ((x & 15) == 3 && y == 49) return "electricadvantage:sulfur_ore"; + if ((x & 15) == 2 && y == 48) return "baseelectric:li_ore"; + if ((x & 15) == 3 && y == 49) return "baseelectric:sulfur_ore"; if ((x & 15) == 4 && y == 50) return "mineralogy:sulfur_ore"; return "minecraft:stone"; } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native116ProfileTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native117ProfileTest.java similarity index 77% rename from test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native116ProfileTest.java rename to test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native117ProfileTest.java index 8f05f632..3df470cc 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native116ProfileTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native117ProfileTest.java @@ -10,10 +10,10 @@ import org.junit.Test; import static org.junit.Assert.*; -public class Native116ProfileTest { +public class Native117ProfileTest { @Test public void profilesSeparateUnqualifiedDestinationsFromHistoricalSources() throws Exception { - File[] files = new File("profiles/native116").listFiles((dir, name) -> name.endsWith(".json")); + File[] files = new File("profiles/native117").listFiles((dir, name) -> name.endsWith(".json")); assertNotNull(files); assertEquals(10, files.length); int ported = 0, blocked = 0; @@ -22,31 +22,33 @@ public void profilesSeparateUnqualifiedDestinationsFromHistoricalSources() throw try (FileReader reader = new FileReader(file)) { raw = new JsonParser().parse(reader).getAsJsonObject(); } - assertEquals("1.16.5", raw.get("minecraftVersion").getAsString()); + assertEquals("1.17.1", raw.get("minecraftVersion").getAsString()); assertEquals("UNQUALIFIED", raw.get("qualificationStatus").getAsString()); - assertEquals("build/works-native116", raw.get("runtimeBase").getAsString()); - assertEquals("1.16.5-36.2.34", raw.getAsJsonObject("forge").get("version").getAsString()); + assertEquals("build/works-native117", raw.get("runtimeBase").getAsString()); + assertEquals("1.17.1-37.1.1", raw.getAsJsonObject("forge").get("version").getAsString()); assertEquals(64, raw.getAsJsonObject("forge").get("sha256").getAsString().length()); String source = raw.get("legacySourceProfile").getAsString(); assertTrue(source, source.startsWith("../../../PA113/PowerAdvantage/test-rig/profiles/")); assertEquals(new File(".").getCanonicalFile(), WorksRunner.findTestRig(file)); if (raw.get("portStatus").getAsString().equals("UNPORTED")) { blocked++; - try { WorksRunner.readNative116Profile(file); fail("Unported profile was accepted: " + file); } + try { WorksRunner.readNative117Profile(file); fail("Unported profile was accepted: " + file); } catch (IllegalStateException expected) { assertTrue(expected.getMessage().contains("not ported")); } } else { ported++; - WorksRunner.readNative116Profile(file); + WorksRunner.readNative117Profile(file); int harness = 0; for (JsonElement element : raw.getAsJsonArray("mods")) { JsonObject mod = element.getAsJsonObject(); - assertTrue(file.toString(), mod.get("version").getAsString().endsWith(".116051")); + assertTrue(file.toString(), mod.get("version").getAsString().endsWith(".117011")); String path = mod.get("path").getAsString(); assertFalse(file.toString(), historicalWorkspacePath(path)); String artifact = new File(path).getName(); assertEquals(mod.get("file").getAsString(), artifact); assertTrue(artifact, artifact.contains("-" + mod.get("version").getAsString())); - assertTrue(mod.get("workspaceProduced").getAsBoolean()); + boolean owned = mod.get("id").getAsString().matches("base(?:power|steam|electric)|basemetals|advantageworkstest"); + assertEquals("Public dependency bytes must remain distinct from local candidates", owned, + mod.get("workspaceProduced").getAsBoolean()); if (mod.get("id").getAsString().equals("advantageworkstest")) harness++; } assertEquals(1, harness); @@ -57,7 +59,7 @@ public void profilesSeparateUnqualifiedDestinationsFromHistoricalSources() throw } @Test public void archiveHashDigitsDoNotIdentifyAHistoricalWorkspace() { - assertFalse(historicalWorkspacePath("build/native116-artifacts/B4CBB113B3/Native-1.0.0.116051.jar")); + assertFalse(historicalWorkspacePath("build/native117-artifacts/B4CBB113B3/Native-1.0.0.116051.jar")); for (String line : new String[] {"110", "112", "113", "114"}) { assertTrue(historicalWorkspacePath("D:/PowerAdvantage/PA" + line + "/PowerAdvantage/build/mod.jar")); assertTrue(historicalWorkspacePath("D:\\PowerAdvantage\\PA" + line + "\\PowerAdvantage\\build\\mod.jar")); @@ -71,12 +73,12 @@ private static boolean historicalWorkspacePath(String path) { @Test public void explicitlyUnportedProfilesStillFailClosed() throws Exception { - File file = File.createTempFile("unported-native116-", ".json"); + File file = File.createTempFile("unported-native117-", ".json"); try { Files.write(file.toPath(), ("{\"name\":\"blocked\",\"mode\":\"packaged\"," - + "\"minecraftVersion\":\"1.16.5\",\"portStatus\":\"UNPORTED\"," + + "\"minecraftVersion\":\"1.17.1\",\"portStatus\":\"UNPORTED\"," + "\"portBlocker\":\"missing genuine native dependency\"}").getBytes(StandardCharsets.UTF_8)); - try { WorksRunner.readNative116Profile(file); fail("Unported profile was accepted"); } + try { WorksRunner.readNative117Profile(file); fail("Unported profile was accepted"); } catch (IllegalStateException expected) { assertTrue(expected.getMessage().contains("not ported")); } } finally { Files.delete(file.toPath()); @@ -84,7 +86,7 @@ public void explicitlyUnportedProfilesStillFailClosed() throws Exception { } @Test - public void pendingNative116PinsFailBeforeAnyRuntimeIsCreated() throws Exception { + public void pendingNative117PinsFailBeforeAnyRuntimeIsCreated() throws Exception { WorksRunner.Profile profile = new com.google.gson.Gson().fromJson( "{\"mods\":[{\"id\":\"advantageworkstest\",\"sha256\":null}]}", WorksRunner.Profile.class); try { WorksRunner.requireInputPins(profile); fail("Pending candidate pins must fail closed"); } @@ -92,20 +94,20 @@ public void pendingNative116PinsFailBeforeAnyRuntimeIsCreated() throws Exception } @Test - public void historical114CannotBeLaunchedAsNative116() throws Exception { + public void historical114CannotBeLaunchedAsNative117() throws Exception { try { - WorksRunner.readNative116Profile(new File("profiles/native114/packaged-current.json")); - fail("Historical 1.14.4 profile must not launch native116"); + WorksRunner.readNative117Profile(new File("profiles/native114/packaged-current.json")); + fail("Historical 1.14.4 profile must not launch native117"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("Historical source profile")); } } @Test - public void historicalQualificationCannotBeLaunchedAsNative116() throws Exception { + public void historicalQualificationCannotBeLaunchedAsNative117() throws Exception { try { - WorksRunner.readNative116Profile(new File("profiles/packaged-current.json")); - fail("Historical 1.13.2 profile must not launch native116"); + WorksRunner.readNative117Profile(new File("profiles/packaged-current.json")); + fail("Historical 1.13.2 profile must not launch native117"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("Historical source profile")); } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native117RunnerLaunchTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native117RunnerLaunchTest.java new file mode 100644 index 00000000..da022213 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native117RunnerLaunchTest.java @@ -0,0 +1,163 @@ +package zone.moddev.mc.advantageworks.runner; + +import com.google.gson.Gson; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import static org.junit.Assert.*; + +/** Command construction only: fixtures never execute a JVM or start a server. */ +public class Native117RunnerLaunchTest { + @Rule public TemporaryFolder temporary = new TemporaryFolder(); + private static final String LIBRARY = "libraries/native/runtime.jar"; + + private WorksRunner.Profile profile() throws Exception { + WorksRunner.Profile profile = new Gson().fromJson("{\"mode\":\"packaged\",\"minecraftVersion\":\"1.17.1\"," + + "\"forge\":{\"version\":\"1.17.1-37.1.1\"}}", WorksRunner.Profile.class); + File javaHome = temporary.newFolder("java " + System.nanoTime()); + assertTrue(new File(javaHome, "bin").mkdir()); + String executable = System.getProperty("os.name").toLowerCase(java.util.Locale.ROOT).contains("win") ? "java.exe" : "java"; + Files.write(new File(javaHome, "bin/" + executable).toPath(), new byte[] {1}); + Files.writeString(new File(javaHome, "release").toPath(), + "JAVA_VERSION=\"16.0.2\"\nIMPLEMENTOR_VERSION=\"Temurin-16.0.2+7\"\n"); + profile.java16Home = javaHome.getAbsolutePath(); + return profile; + } + + private Path runtime(WorksRunner.Profile profile) throws Exception { + Path runtime = temporary.newFolder("runtime with spaces " + System.nanoTime()).toPath(); + Files.createDirectories(runtime.resolve(LIBRARY).getParent()); + Files.write(runtime.resolve(LIBRARY), new byte[] {1, 2}); + writeArgs(runtime, profile, modularArgs()); + writeLoggingJar(runtime, true); + return runtime; + } + + private static String modularArgs() { + return String.join("\n", "-p " + LIBRARY, "--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", + "-DignoreList=runtime.jar", "-DlibraryDirectory=libraries", "-DlegacyClassPath=" + LIBRARY, + "cpw.mods.bootstraplauncher.BootstrapLauncher", "--launchTarget forgeserver", + "--fml.forgeVersion 37.1.1", "--fml.mcVersion 1.17.1", "--fml.forgeGroup net.minecraftforge", + "--fml.mcpVersion 20210706.113038") + "\n"; + } + + private static void writeArgs(Path runtime, WorksRunner.Profile profile, String text) throws Exception { + Path args = runtime.resolve(WorksRunner.serverLauncherPath(profile)); + Files.createDirectories(args.getParent()); + Files.writeString(args, text); + } + + private static Path loggingJar(Path runtime) { + return runtime.resolve("libraries/net/minecraftforge/fmlloader/1.17.1-37.1.1/fmlloader-1.17.1-37.1.1.jar"); + } + + private static void writeLoggingJar(Path runtime, boolean config) throws Exception { + Path jar = loggingJar(runtime); + Files.createDirectories(jar.getParent()); + try (JarOutputStream output = new JarOutputStream(Files.newOutputStream(jar))) { + output.putNextEntry(new JarEntry(config ? "log4j2.xml" : "other.xml")); + output.write("".getBytes(StandardCharsets.UTF_8)); + output.closeEntry(); + } + } + + private interface Action { void run() throws Exception; } + private static Exception rejected(Action action) throws Exception { + try { action.run(); } catch (Exception failure) { return failure; } + throw new AssertionError("Unsafe native launch accepted"); + } + + @Test public void usesModularArgfilePinnedJavaAndForgesOwnLoggingConfiguration() throws Exception { + WorksRunner.Profile profile = profile(); + Path runtime = runtime(profile); + List command = WorksRunner.packagedCommand(profile, runtime.toFile()); + assertTrue(command.get(0), command.get(0).startsWith(profile.java16Home)); + assertFalse(command.contains("-jar")); + assertTrue(command.contains("@" + WorksRunner.serverLauncherPath(profile))); + assertTrue(command.contains("java.base/java.lang.invoke=ALL-UNNAMED")); + String logging = "-Dlog4j.configurationFile=jar:" + loggingJar(runtime).toFile().toURI().toASCIIString() + "!/log4j2.xml"; + assertTrue(command.toString(), command.contains(logging)); + assertTrue(logging, logging.contains("%20")); + assertTrue(command.indexOf(logging) < command.indexOf("@" + WorksRunner.serverLauncherPath(profile))); + assertEquals("nogui", command.get(command.size() - 1)); + } + + @Test public void missingArgfileOrLoggingConfigurationCannotFallBackToALegacyLauncher() throws Exception { + for (String missing : Arrays.asList("argfile", "logging-jar", "logging-entry")) { + WorksRunner.Profile profile = profile(); + Path runtime = runtime(profile); + Files.write(runtime.resolve("forge-1.17.1-37.1.1.jar"), new byte[] {9}); + if (missing.equals("argfile")) Files.delete(runtime.resolve(WorksRunner.serverLauncherPath(profile))); + if (missing.equals("logging-jar")) Files.delete(loggingJar(runtime)); + if (missing.equals("logging-entry")) writeLoggingJar(runtime, false); + rejected(() -> WorksRunner.packagedCommand(profile, runtime.toFile())); + } + } + + @Test public void customCommandsCannotBypassRuntimeOrLogCustody() throws Exception { + WorksRunner.Profile profile = profile(); + Path runtime = runtime(profile); + profile.launchCommand = Arrays.asList("java", "-jar", "unverified.jar"); + assertTrue(rejected(() -> WorksRunner.packagedCommand(profile, runtime.toFile())).getMessage().contains("cannot override")); + assertTrue(rejected(() -> WorksRunner.requireInputPins(profile)).getMessage().contains("cannot override")); + } + + @Test public void anotherJavaPatchBuildOrMajorCannotSatisfyTheRuntimePin() throws Exception { + for (String version : Arrays.asList("Temurin-16.0.2+6", "Temurin-16.0.1+9", "Temurin-17.0.1+12")) { + WorksRunner.Profile profile = profile(); + Path runtime = runtime(profile); + Files.writeString(new File(profile.java16Home, "release").toPath(), + "JAVA_VERSION=\"16.0.2\"\nIMPLEMENTOR_VERSION=\"" + version + "\"\n"); + assertTrue(rejected(() -> WorksRunner.packagedCommand(profile, runtime.toFile())).getMessage().contains("16.0.2+7")); + } + } + + @Test public void incompleteOrContradictoryJavaMetadataCannotSatisfyTheRuntimePin() throws Exception { + for (String metadata : Arrays.asList("IMPLEMENTOR_VERSION=\"Temurin-16.0.2+7\"\n", + "JAVA_VERSION=\"17.0.1\"\nIMPLEMENTOR_VERSION=\"Temurin-16.0.2+7\"\n", + "JAVA_VERSION=\"16.0.2\"\n")) { + WorksRunner.Profile profile = profile(); + Path runtime = runtime(profile); + Files.writeString(new File(profile.java16Home, "release").toPath(), metadata); + rejected(() -> WorksRunner.packagedCommand(profile, runtime.toFile())); + } + } + + @Test public void argfileCannotReadCodeOutsideThePreparedRuntime() throws Exception { + for (String path : Arrays.asList("../escape.jar", "libraries/../escape.jar", "libraries//escape.jar", + "/absolute.jar", "C:/absolute.jar", "libraries/a.jar@elsewhere")) { + WorksRunner.Profile profile = profile(); + Path runtime = runtime(profile); + writeArgs(runtime, profile, modularArgs().replace(LIBRARY, path)); + assertTrue(rejected(() -> WorksRunner.packagedCommand(profile, runtime.toFile())).getMessage().contains("code path")); + } + } + + @Test public void argfileCannotAddJvmAgentsNestedArgumentsOrDifferentLaunchTargets() throws Exception { + for (String text : Arrays.asList(modularArgs() + "-javaagent:outside.jar\n", modularArgs() + "@outside.txt\n", + modularArgs().replace("forgeserver", "forgeclient"), modularArgs().replace("--fml.mcVersion 1.17.1\n", ""), + modularArgs() + "--launchTarget forgeserver\n")) { + WorksRunner.Profile profile = profile(); + Path runtime = runtime(profile); + writeArgs(runtime, profile, text); + rejected(() -> WorksRunner.packagedCommand(profile, runtime.toFile())); + } + } + + @Test public void missingModularLibraryFailsBeforeAProcessCanStart() throws Exception { + WorksRunner.Profile profile = profile(); + Path runtime = runtime(profile); + Files.delete(runtime.resolve(LIBRARY)); + rejected(() -> WorksRunner.packagedCommand(profile, runtime.toFile())); + } +} 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 new file mode 100644 index 00000000..95427e19 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native117RunnerLogTest.java @@ -0,0 +1,247 @@ +package zone.moddev.mc.advantageworks.runner; + +import com.google.gson.JsonParser; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import static org.junit.Assert.*; + +/** Unit evidence for the log gate, not a substitute for a packaged operating run. */ +public class Native117RunnerLogTest { + @Rule public TemporaryFolder temporary = new TemporaryFolder(); + private static final String CLEAN = "[Server thread/INFO]: Done (1.0s)!\n[Server thread/INFO]: Stopping server\n"; + + private WorksRunner.BootLogs boot(String phase) throws Exception { + File runtime = temporary.newFolder(); + File results = new File(runtime, "advantage-works-results"); + assertTrue(results.mkdir()); + return new WorksRunner.BootLogs(runtime, results, phase); + } + + private static void write(File file, String text) throws Exception { + Files.createDirectories(file.toPath().getParent()); + Files.write(file.toPath(), text.getBytes(StandardCharsets.UTF_8)); + } + + private static File log(WorksRunner.BootLogs boot, String kind) { + return kind.equals("console") ? boot.console : new File(boot.runtime, "logs/" + kind + ".log"); + } + + private static void fill(WorksRunner.BootLogs boot) throws Exception { + for (String kind : Arrays.asList("console", "latest", "debug")) write(log(boot, kind), CLEAN); + } + + private static String receipt(WorksRunner.BootLogs boot) throws Exception { + return Files.readString(new File(boot.results, boot.phase + "-logs.json").toPath()); + } + + private interface Action { void run() throws Exception; } + private static Exception rejected(Action action) throws Exception { + try { action.run(); } catch (Exception failure) { return failure; } + throw new AssertionError("Invalid log evidence accepted"); + } + + @Test public void completeBootArchivesAllThreeExactLogsAndHashes() throws Exception { + WorksRunner.BootLogs boot = boot("first-run"); + fill(boot); + boot.archive(); + assertEquals("PASS", new JsonParser().parse(receipt(boot)).getAsJsonObject().get("status").getAsString()); + for (String kind : Arrays.asList("console", "latest", "debug")) { + assertEquals(CLEAN, Files.readString(new File(boot.results, "first-run-" + kind + ".log").toPath())); + } + assertEquals(3, new JsonParser().parse(receipt(boot)).getAsJsonObject().getAsJsonArray("logs").size()); + assertTrue(receipt(boot).contains("sha256")); + } + + @Test public void everyMissingLogBlocksPassAndStillArchivesTheOtherLogs() throws Exception { + for (String missing : Arrays.asList("console", "latest", "debug")) { + WorksRunner.BootLogs boot = boot("first-run"); + fill(boot); + Files.delete(log(boot, missing).toPath()); + rejected(boot::archive); + assertTrue(receipt(boot).contains("FAIL")); + assertTrue(receipt(boot).contains(missing)); + assertTrue(boot.archived); + assertFalse(boot.clean); + rejected(boot::clearLiveLogs); + for (String kind : Arrays.asList("console", "latest", "debug")) { + if (!kind.equals(missing)) assertTrue(new File(boot.results, "first-run-" + kind + ".log").isFile()); + } + } + } + + @Test public void anErrorInAnySingleLogBlocksOtherwiseCleanEvidence() throws Exception { + for (String kind : Arrays.asList("console", "latest", "debug")) { + for (String error : Arrays.asList("[Worker/ERROR]: failed", "[Worker/FATAL]: failed", + "java.lang.IllegalStateException: failure", "java.lang.Error: failure", "Caused by: trouble", "ERROR StatusLogger: broken logging", + "Failed to reload data packs", "Advantage Works command failed")) { + WorksRunner.BootLogs boot = boot("first-run"); + fill(boot); + write(log(boot, kind), CLEAN + error + "\n"); + rejected(boot::archive); + assertFalse(boot.clean); + assertTrue(receipt(boot).contains(error)); + assertTrue(new File(boot.results, "first-run-" + kind + ".log").isFile()); + } + } + } + + @Test public void emptyOrTruncatedLogsCannotPassEvenWithoutErrorText() throws Exception { + for (String text : Arrays.asList("", "[INFO]: Starting server\n", "Done (1s)!\n", "Stopping server\n")) { + WorksRunner.BootLogs boot = boot("first-run"); + fill(boot); + write(log(boot, "debug"), text); + assertTrue(rejected(boot::archive).getMessage().contains("startup/shutdown")); + } + } + + @Test public void aCleanEarlierBootCannotSupplyMissingRestartDebugLog() throws Exception { + WorksRunner.BootLogs first = boot("first-run"); + fill(first); + first.archive(); + rejected(() -> new WorksRunner.BootLogs(first.runtime, first.results, "restart-run")); + first.clearLiveLogs(); + WorksRunner.BootLogs second = new WorksRunner.BootLogs(first.runtime, first.results, "restart-run"); + write(second.console, CLEAN); + write(log(second, "latest"), CLEAN); + rejected(second::archive); + assertTrue(receipt(second).contains("FAIL")); + assertTrue(receipt(first).contains("PASS")); + } + + @Test public void aFullRestartHasSeparateImmutableReceipts() throws Exception { + WorksRunner.BootLogs first = boot("first-run"); + fill(first); + first.archive(); + String original = receipt(first); + first.clearLiveLogs(); + WorksRunner.BootLogs second = new WorksRunner.BootLogs(first.runtime, first.results, "restart-run"); + fill(second); + second.archive(); + assertTrue(second.clean); + assertEquals(original, receipt(first)); + rejected(first::archive); + assertEquals(original, receipt(first)); + } + + @Test public void logsChangedAfterArchiveArePreservedAndPreventRestart() throws Exception { + WorksRunner.BootLogs first = boot("first-run"); + fill(first); + first.archive(); + write(log(first, "debug"), CLEAN + "[Worker/ERROR]: late failure\n"); + rejected(first::clearLiveLogs); + assertTrue(log(first, "latest").isFile()); + assertTrue(log(first, "debug").isFile()); + } + + @Test public void staleConsoleAndUnsafePhaseCannotOverwriteOrEscape() throws Exception { + WorksRunner.BootLogs boot = boot("first-run"); + write(boot.console, "preserve me"); + rejected(() -> new WorksRunner.BootLogs(boot.runtime, boot.results, "first-run")); + assertEquals("preserve me", Files.readString(boot.console.toPath())); + rejected(() -> new WorksRunner.BootLogs(boot.runtime, boot.results, "../escape")); + assertFalse(new File(boot.runtime, "escape-console.log").exists()); + } + + @Test public void changedArchivesOrReceiptsCannotBeRepinnedByChangingLiveLogsToo() throws Exception { + for (String changed : Arrays.asList("console", "latest", "debug", "receipt")) { + WorksRunner.BootLogs boot = boot("first-run"); + fill(boot); + boot.archive(); + File archived = new File(boot.results, "first-run-" + (changed.equals("receipt") ? "logs.json" : changed + ".log")); + write(archived, CLEAN + "changed\n"); + if (!changed.equals("receipt")) write(log(boot, changed), CLEAN + "changed\n"); + rejected(boot::requireArchiveUnchanged); + rejected(boot::clearLiveLogs); + assertTrue(log(boot, "latest").isFile()); + assertTrue(log(boot, "debug").isFile()); + } + } + + private static final String MIXIN_CLEANUP = "[31Oct2026 06:47:24.115] [main/DEBUG] [mixin/]: " + + "Error cleaning class output directory: .mixin.out"; + + private void rejectEventInEveryLog(String event) throws Exception { + for (String kind : Arrays.asList("console", "latest", "debug")) { + WorksRunner.BootLogs boot = boot("first-run"); + fill(boot); + write(log(boot, kind), CLEAN + event + "\n"); + rejected(boot::archive); + assertFalse(kind + ": " + event, boot.clean); + assertTrue(Files.readString(new File(boot.results, "first-run-" + kind + ".log").toPath()) + .contains(event)); + } + } + + @Test public void exactNativeMixinDebugCleanupIsRetainedAndClassifiedSeparately() throws Exception { + for (String event : Arrays.asList(MIXIN_CLEANUP, + MIXIN_CLEANUP.replace("31Oct2026 06:47:24.115", "06:47:24"))) { + for (String kind : Arrays.asList("console", "latest", "debug")) { + WorksRunner.BootLogs boot = boot("first-run"); + fill(boot); + write(log(boot, kind), CLEAN + event + "\n"); + boot.archive(); + assertTrue(boot.clean); + String json = receipt(boot); + assertTrue(json.contains("nonFatalDiagnostics")); + assertTrue(json.contains("Mixin 0.8.4")); + assertTrue(json.contains("Error cleaning class output directory: .mixin.out")); + assertEquals(CLEAN + event + "\n", Files.readString( + new File(boot.results, "first-run-" + kind + ".log").toPath())); + boot.requireArchiveUnchanged(); + } + } + } + + @Test public void cleanupAtErrorOrFatalSeverityStillFails() throws Exception { + for (String severity : Arrays.asList("ERROR", "FATAL")) { + rejectEventInEveryLog(MIXIN_CLEANUP.replace("main/DEBUG", "main/" + severity)); + } + } + + @Test public void otherCleanupPathsLoggersThreadsAndExtraTextAreNotExempted() throws Exception { + for (String event : Arrays.asList( + MIXIN_CLEANUP.replace("[mixin/]", "[other/]"), + MIXIN_CLEANUP.replace("main/DEBUG", "worker/DEBUG"), + MIXIN_CLEANUP.replace("main/DEBUG", "main/INFO"), + MIXIN_CLEANUP + "/class", + MIXIN_CLEANUP + " java.io.IOException: denied", + MIXIN_CLEANUP + " extra text")) { + rejectEventInEveryLog(event); + } + } + + @Test public void debugDoesNotHideThrowableLinkageStackOrCommandFailures() throws Exception { + String prefix = "[31Oct2026 06:47:24.115] [main/DEBUG] [mixin/]: "; + for (String event : Arrays.asList( + "java.lang.IllegalStateException: broken", + "java.lang.LinkageError: broken", + "java.lang.NoClassDefFoundError: missing", + "java.lang.NoSuchMethodError: missing", + "java.lang.Error: broken", + "Exception in thread \"Server thread\" java.lang.IllegalStateException: broken", + "Caused by: java.io.IOException: broken", + "Advantage Works command failed")) { + rejectEventInEveryLog(prefix + event); + } + } + + @Test public void nativeStackFramesAreFailuresEvenIfTheHeaderWasTruncated() throws Exception { + for (String event : Arrays.asList( + "\tat zone.moddev.Native117.run(Native117.java:42)", + "\tat java.base/java.lang.Thread.run(Thread.java:833)", + "\t... 12 more")) { + rejectEventInEveryLog(event); + } + } + + @Test public void harmlessCleanupDoesNotMaskALaterRealFailure() throws Exception { + rejectEventInEveryLog(MIXIN_CLEANUP + "\n[Server thread/ERROR] [minecraft/MinecraftServer]: broken"); + rejectEventInEveryLog(MIXIN_CLEANUP + "\njava.lang.NoSuchMethodError: broken\n" + + "\tat zone.moddev.Native117.run(Native117.java:42)"); + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/PreparedServerSeedTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/PreparedServerSeedTest.java index 834753f3..e51c2e6d 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/PreparedServerSeedTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/PreparedServerSeedTest.java @@ -22,20 +22,23 @@ public class PreparedServerSeedTest { @Rule public TemporaryFolder temporary = new TemporaryFolder(); - private static final String LAUNCHER = "forge-1.16.5-36.2.34.jar"; - private static final String MINECRAFT = "minecraft_server.1.16.5.jar"; + private static final String LAUNCHER = WorksRunner.serverLauncherPath(new Gson().fromJson( + "{\"minecraftVersion\":\"1.17.1\",\"forge\":{\"version\":\"1.17.1-37.1.1\"}}", WorksRunner.Profile.class)); + private static final String MINECRAFT = "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-extra.jar"; private static final String LIBRARY = "libraries/native/runtime.jar"; private WorksRunner.Profile profile() { return new Gson().fromJson("{\"name\":\"seed-test\",\"mode\":\"packaged\"," - + "\"minecraftVersion\":\"1.16.5\",\"portStatus\":\"PORTED_UNQUALIFIED\"," + + "\"minecraftVersion\":\"1.17.1\",\"portStatus\":\"PORTED_UNQUALIFIED\"," + "\"qualificationStatus\":\"UNQUALIFIED\",\"mods\":[]," - + "\"forge\":{\"version\":\"1.16.5-36.2.34\"}}", WorksRunner.Profile.class); + + "\"forge\":{\"version\":\"1.17.1-37.1.1\"}}", WorksRunner.Profile.class); } private Path seed() throws Exception { Path root = temporary.newFolder().toPath(); Files.createDirectories(root.resolve("libraries/native")); + Files.createDirectories(root.resolve(LAUNCHER).getParent()); + Files.createDirectories(root.resolve(MINECRAFT).getParent()); Files.write(root.resolve(LAUNCHER), new byte[] {1, 2}); Files.write(root.resolve(MINECRAFT), new byte[] {3, 4}); Files.write(root.resolve(LIBRARY), new byte[] {5, 6}); @@ -79,6 +82,10 @@ private static String hash(Path path) throws Exception { private interface Action { void run() throws Exception; } + @Test public void runnerReceiptIsSeparateFromTheCanonicalPythonSeedInventory() { + assertEquals("native117-runner-code-inventory.json", WorksRunner.PreparedServerSeed.INVENTORY); + } + private static Exception rejected(Action action) throws Exception { try { action.run(); } catch (Exception expected) { return expected; } @@ -100,8 +107,9 @@ private static Exception rejected(Action action) throws Exception { Path target = runtime(); Path installer = rig.resolve("installer.jar"); Files.write(installer, new byte[] {7}); + Files.createDirectories(target.resolve(LAUNCHER).getParent()); Files.write(target.resolve(LAUNCHER), new byte[] {8}); - WorksRunner.Profile profile = new Gson().fromJson("{\"forge\":{\"version\":\"1.16.5-36.2.34\"," + WorksRunner.Profile profile = new Gson().fromJson("{\"minecraftVersion\":\"1.17.1\",\"forge\":{\"version\":\"1.17.1-37.1.1\"," + "\"installerPath\":\"installer.jar\",\"sha256\":\"" + hash(installer) + "\"}}", WorksRunner.Profile.class); assertNull(WorksRunner.preparedServerSeed(rig.toFile(), profile, WorksRunner.Arguments.parse(new String[] {"--profile", "p"}))); @@ -123,6 +131,30 @@ private static Exception rejected(Action action) throws Exception { profile.preparedSeed.verifySource(); } + @Test public void publicSeedOptionsSelectModularForgeAndTheLibraryServerJar() throws Exception { + Path source = seed(); + Path receipt = source.resolve(WorksRunner.PreparedServerSeed.INVENTORY); + WorksRunner.PreparedServerSeed prepared = WorksRunner.preparedServerSeed(temporary.getRoot(), profile(), + WorksRunner.Arguments.parse(new String[] {"--profile", "p", "--prepared-server-seed", source.toString(), + "--prepared-server-seed-inventory", receipt.toString(), "--prepared-server-seed-sha256", hash(receipt)})); + assertTrue(prepared.files.containsKey(LAUNCHER)); + assertTrue(prepared.files.containsKey(MINECRAFT)); + assertFalse(prepared.files.containsKey("forge-1.17.1-37.1.1.jar")); + } + + @Test public void canonicalHeaderedPythonReceiptMustNotBeSilentlyReinterpretedOrRewritten() throws Exception { + Path source = seed(); + JsonObject canonical = new Gson().fromJson(Files.readString(source.resolve(WorksRunner.PreparedServerSeed.INVENTORY)), JsonObject.class); + canonical.addProperty("format", 1); + canonical.addProperty("minecraft", "1.17.1"); + canonical.addProperty("forge", "37.1.1"); + Path receipt = temporary.newFile().toPath(); + Files.writeString(receipt, canonical.toString()); + byte[] original = Files.readAllBytes(receipt); + rejected(() -> new WorksRunner.PreparedServerSeed(source, receipt, hash(receipt), LAUNCHER, MINECRAFT)); + assertArrayEquals(original, Files.readAllBytes(receipt)); + } + @Test public void missingCorruptAndUnexpectedCodeNeverFallBackToInstaller() throws Exception { for (String mutation : Arrays.asList("missing", "corrupt", "extra-root", "extra-library")) { Path source = seed(); @@ -238,6 +270,7 @@ private static Exception rejected(Action action) throws Exception { rejected(() -> seal.stage(source.toFile())); rejected(() -> seal.stage(source.resolve("destination").toFile())); Path target = runtime(); + Files.createDirectories(target.resolve(LAUNCHER).getParent()); Files.write(target.resolve(LAUNCHER), new byte[] {99}); rejected(() -> seal.stage(target.toFile())); assertArrayEquals(new byte[] {99}, Files.readAllBytes(target.resolve(LAUNCHER))); @@ -275,7 +308,7 @@ private static Exception rejected(Action action) throws Exception { rejected(() -> WorksRunner.main(new String[] {"--profile", profileFile.toString(), "--validate-only", "--prepared-server-seed", source.toString(), "--prepared-server-seed-inventory", receipt.toString(), "--prepared-server-seed-sha256", pin})); - assertFalse(Files.exists(rig.resolve("build/works-native116"))); + assertFalse(Files.exists(rig.resolve("build/works-native117"))); } private void writeInventory(Path source, String content) throws Exception { diff --git a/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java b/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java new file mode 100644 index 00000000..c9780d80 --- /dev/null +++ b/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java @@ -0,0 +1,44 @@ +package zone.moddev.mc.advantageworks.unit; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.security.ProtectionDomain; + +/** Runs Forge's own token transform for plain JUnit, without replacing its capabilities. */ +public final class ForgeCapabilityBootstrap { + public static void premain(String options, Instrumentation instrumentation) { + instrumentation.addTransformer(new ClassFileTransformer() { + @Override public byte[] transform(ClassLoader loader, String name, Class type, + ProtectionDomain domain, byte[] bytes) { + if (loader == null || name == null || !(name.contains("$") + || name.equals("net/minecraftforge/common/capabilities/CapabilityToken"))) return null; + try { + Class readerType = Class.forName("org.objectweb.asm.ClassReader", true, loader); + Object reader = readerType.getConstructor(byte[].class).newInstance((Object)bytes); + String parent = (String)readerType.getMethod("getSuperName").invoke(reader); + String token = "net/minecraftforge/common/capabilities/CapabilityToken"; + if (!token.equals(name) && !token.equals(parent)) return null; + Class nodeType = Class.forName("org.objectweb.asm.tree.ClassNode", true, loader); + Object node = nodeType.getConstructor().newInstance(); + Class visitorType = Class.forName("org.objectweb.asm.ClassVisitor", true, loader); + readerType.getMethod("accept", visitorType, int.class).invoke(reader, node, 0); + Class asmType = Class.forName("org.objectweb.asm.Type", true, loader); + Object asmName = asmType.getMethod("getObjectType", String.class).invoke(null, name); + Class phaseType = Class.forName( + "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); + 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); + Object writer = writerType.getConstructor(int.class).newInstance(1); + nodeType.getMethod("accept", visitorType).invoke(node, writer); + return (byte[])writerType.getMethod("toByteArray").invoke(writer); + } catch (ReflectiveOperationException failure) { + throw new IllegalStateException("Cannot run Forge's native capability transform for " + name, failure); + } + } + }); + } +} diff --git a/tools/generate-recipe-unlocks.js b/tools/generate-recipe-unlocks.js index 27146226..4c3e37cb 100644 --- a/tools/generate-recipe-unlocks.js +++ b/tools/generate-recipe-unlocks.js @@ -30,7 +30,8 @@ for (const file of fs.readdirSync(recipes).filter(name => name.endsWith('.json') const criteria = {}; unique.forEach((ingredient, index) => { criteria[`has_ingredient_${index + 1}`] = { - trigger: 'minecraft:inventory_changed', conditions: {items: [ingredient]} + trigger: 'minecraft:inventory_changed', + conditions: {items: [ingredient.item ? {items: [ingredient.item]} : ingredient]} }; }); criteria.has_recipe = {trigger: 'minecraft:recipe_unlocked', conditions: {recipe: id}}; diff --git a/tools/verification/native117/AuditedPowerJUnit117.java b/tools/verification/native117/AuditedPowerJUnit117.java new file mode 100644 index 00000000..ff16dfe7 --- /dev/null +++ b/tools/verification/native117/AuditedPowerJUnit117.java @@ -0,0 +1,77 @@ +package zone.moddev.mc.poweradvantage.verification.native117; + +import java.io.FileOutputStream; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamWriter; +import org.junit.internal.TextListener; +import org.junit.runner.JUnitCore; +import org.junit.runner.Description; +import org.junit.runner.Result; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; + +/** Reports skipped and assumed tests separately from JUnit's success exit code. */ +public final class AuditedPowerJUnit117 { + private AuditedPowerJUnit117() {} + + public static void main(String[] args) throws Exception { + int expected = Integer.parseInt(args[1]); + Class[] suites = new Class[args.length - 2]; + for (int i = 0; i < suites.length; i++) suites[i] = Class.forName(args[i + 2]); + final int[] assumptions = {0}; + final int[] duplicateStarts = {0}; + final Map started = new LinkedHashMap<>(); + final Map finished = new LinkedHashMap<>(); + JUnitCore core = new JUnitCore(); + core.addListener(new TextListener(System.out)); + core.addListener(new RunListener() { + @Override public void testStarted(Description test) { + if (started.put(test.getDisplayName(), test) != null) duplicateStarts[0]++; + } + @Override public void testFinished(Description test) { + finished.put(test.getDisplayName(), test); + } + @Override public void testAssumptionFailure(Failure failure) { assumptions[0]++; } + }); + Result result = core.run(suites); + boolean passed = result.wasSuccessful() && result.getRunCount() == expected + && result.getIgnoreCount() == 0 && assumptions[0] == 0 && duplicateStarts[0] == 0 + && started.size() == expected && started.keySet().equals(finished.keySet()); + try (FileOutputStream output = new FileOutputStream(args[0])) { + XMLStreamWriter xml = XMLOutputFactory.newFactory().createXMLStreamWriter(output, "UTF-8"); + xml.writeStartDocument("UTF-8", "1.0"); + xml.writeStartElement("testsuite"); + xml.writeAttribute("name", "PowerAdvantage-1.17.1-ECJ"); + xml.writeAttribute("tests", Integer.toString(result.getRunCount())); + xml.writeAttribute("failures", Integer.toString(result.getFailureCount())); + xml.writeAttribute("ignored", Integer.toString(result.getIgnoreCount())); + xml.writeAttribute("assumptions", Integer.toString(assumptions[0])); + xml.writeAttribute("expected", Integer.toString(expected)); + xml.writeAttribute("passed", Boolean.toString(passed)); + xml.writeAttribute("duplicateStarts", Integer.toString(duplicateStarts[0])); + for (Map.Entry entry : started.entrySet()) { + Description test = entry.getValue(); + xml.writeStartElement("testcase"); + xml.writeAttribute("classname", test.getClassName()); + xml.writeAttribute("name", test.getMethodName()); + xml.writeAttribute("finished", Boolean.toString(finished.containsKey(entry.getKey()))); + xml.writeEndElement(); + } + for (Failure failure : result.getFailures()) { + xml.writeStartElement("failure"); + xml.writeAttribute("test", failure.getDescription().getDisplayName()); + xml.writeCharacters(failure.getTrace()); + xml.writeEndElement(); + } + xml.writeEndElement(); + xml.writeEndDocument(); + xml.close(); + } + System.out.println("AUDITED_JUNIT tests=" + result.getRunCount() + " expected=" + expected + + " failures=" + result.getFailureCount() + " ignored=" + result.getIgnoreCount() + + " assumptions=" + assumptions[0] + " passed=" + passed); + if (!passed) System.exit(1); + } +} diff --git a/tools/verification/native117/AuditedPowerJUnit117Control.java b/tools/verification/native117/AuditedPowerJUnit117Control.java new file mode 100644 index 00000000..934877eb --- /dev/null +++ b/tools/verification/native117/AuditedPowerJUnit117Control.java @@ -0,0 +1,21 @@ +package zone.moddev.mc.poweradvantage.verification.native117; + +import org.junit.Assume; +import org.junit.Ignore; +import org.junit.Test; + +/** Negative controls for the verification reporter, never production tests. */ +public final class AuditedPowerJUnit117Control { + public static final class Assumed { + @Test public void mustNotBeReportedAsPassing() { + Assume.assumeTrue(false); + } + } + + @Ignore("The verifier must reject ignored tests") + public static final class Ignored { + @Test public void mustNotRun() { + throw new AssertionError("Ignored fixture executed"); + } + } +} diff --git a/tools/verification/native117/README.md b/tools/verification/native117/README.md new file mode 100644 index 00000000..ebf4c406 --- /dev/null +++ b/tools/verification/native117/README.md @@ -0,0 +1,44 @@ +# Power 1.17 Eclipse compiler check + +This check compiles the pinned Power candidate with Eclipse's compiler, then +runs the complete production unit-test inventory from that fresh output. It +does not run Gradle, start Minecraft, or replace any built mod jar. + +Run it after the normal release build has generated `.classpath`, processed +resources and the 201-test Gradle inventory: + +```powershell +& ./tools/verification/native117/Verify-PowerEcj117.ps1 ` + -Evidence 'D:/PowerAdvantage/_verification/advantage-117-port-20261031/Power ECJ new receipt' +``` + +Choose a new evidence directory outside the checkout. Existing evidence is +never overwritten. The command accepts explicit compiler and Java installation +paths; its defaults identify the locally qualified installations. The compiler +jar has a required SHA-256 pin. + +## What it proves + +- Production, test and Forge test-bootstrap sources match basePower commit + `a6fc3050b0e92e89abf49bf18bbbfeb87fb3a1eb`. +- Libraries come from the actual `.classpath`, with the declared main/test + scopes and the exact local Base Metals and released OreSpawn hashes checked. +- ECJ runs on Java 17 against the Java 16 module image. Forbidden and discouraged + references remain errors; a restricted-JDK negative control must fail. +- Main, test, reporter and capability-bootstrap classes are freshly compiled + into the evidence directory. Every emitted class is checked for Java 16 + bytecode. No existing Power classes or Power jar can satisfy compilation. +- All 201 tests run on Java 16 with the freshly built Forge capability agent. + Structured JUnit evidence must match every baseline test identity, with no + failures, ignored tests, assumptions, duplicate starts or unfinished tests. + Separate ignored-test and assumption controls must be rejected. +- Input hashes and the protected Gradle/Eclipse output inventory are checked + again before success is reported. + +The report is `ecj-results.json`; compiler XML, compiler logs, argument files, +fresh classes and JUnit XML remain beside it. Warnings remain visible. A failed +check keeps its report and output rather than replacing them on a retry. + +The Java 8 `../Verify-EclipseCompilation.ps1` belongs to historical ports and is +not used by this native 1.17 check. This is compiler and worldless unit-test +evidence, not gameplay, migration or visual acceptance. diff --git a/tools/verification/native117/RestrictedJdkReference117.java b/tools/verification/native117/RestrictedJdkReference117.java new file mode 100644 index 00000000..332c8227 --- /dev/null +++ b/tools/verification/native117/RestrictedJdkReference117.java @@ -0,0 +1,4 @@ +// This fixture must fail against the production Java 16 module image. +final class RestrictedJdkReference117 { + sun.security.x509.X500Name forbidden; +} diff --git a/tools/verification/native117/Verify-PowerEcj117.ps1 b/tools/verification/native117/Verify-PowerEcj117.ps1 new file mode 100644 index 00000000..ad6cea0e --- /dev/null +++ b/tools/verification/native117/Verify-PowerEcj117.ps1 @@ -0,0 +1,230 @@ +param( + [string]$ProjectRoot = (Join-Path $PSScriptRoot '../../..'), + [Parameter(Mandatory = $true)][string]$Evidence, + [string]$Compiler = 'C:/Users/johnb/.p2/pool/plugins/org.eclipse.jdt.core.compiler.batch_3.46.0.v20260528-0407.jar', + [string]$Java17Home = 'C:/Users/johnb/.jdks/temurin-17.0.1+12/jdk-17.0.1+12', + [string]$Java16Home = 'C:/Users/johnb/.gradle/jdks/eclipse_foundation-16-amd64-windows.2' +) +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +$ProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot).Path +$Evidence = [IO.Path]::GetFullPath($Evidence) +if ($Evidence -eq $ProjectRoot -or $Evidence.StartsWith($ProjectRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -or + $ProjectRoot.StartsWith($Evidence.TrimEnd('\') + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { + throw 'ECJ evidence must be a separate directory outside the Power checkout' +} +if (Test-Path -LiteralPath $Evidence) { throw 'Refusing to overwrite ECJ evidence' } +$null = New-Item -ItemType Directory -Path $Evidence + +function Get-Pin([string]$Path) { + $file = Get-Item -LiteralPath $Path + [ordered]@{ path = $file.FullName; sha256 = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash } +} +function Save-Report { + $json = $report | ConvertTo-Json -Depth 15 + [IO.File]::WriteAllText((Join-Path $Evidence 'ecj-results.json'), $json + "`n", [Text.UTF8Encoding]::new($false)) +} +function Get-TreePins([string[]]$Roots) { + $pins = @() + foreach ($root in $Roots) { + if (!(Test-Path -LiteralPath $root)) { continue } + $pins += @(Get-ChildItem -LiteralPath $root -Recurse -File | Sort-Object FullName | ForEach-Object { Get-Pin $_.FullName }) + } + return $pins +} +function Assert-Unchanged($Pins) { + foreach ($pin in $Pins) { + if (!(Test-Path -LiteralPath $pin.path) -or (Get-FileHash -LiteralPath $pin.path -Algorithm SHA256).Hash -ne $pin.sha256) { + throw "Input or protected output changed during ECJ qualification: $($pin.path)" + } + } +} +function Test-ClassFiles([string]$Directory) { + $files = @(Get-ChildItem -LiteralPath $Directory -Recurse -File -Filter '*.class') + if (!$files.Count) { throw "ECJ emitted no classes in $Directory" } + foreach ($file in $files) { + $bytes = [IO.File]::ReadAllBytes($file.FullName) + if ($bytes.Length -lt 8 -or $bytes[0] -ne 0xCA -or $bytes[1] -ne 0xFE -or $bytes[2] -ne 0xBA -or $bytes[3] -ne 0xBE) { + throw "Invalid class file: $($file.FullName)" + } + if (($bytes[6] * 256 + $bytes[7]) -ne 60) { throw "Non-Java-16 ECJ output: $($file.FullName)" } + } + return $files.Count +} +function Invoke-Ecj([string]$Name, [string[]]$Sources, [string[]]$Libraries) { + $output = Join-Path $Evidence $Name + $null = New-Item -ItemType Directory -Path $output + $xmlLog = Join-Path $Evidence "$Name.xml" + # Java 16 uses the JDK module image; keep forbidden/discouraged accesses fatal. + $arguments = @('-16', '-proc:none', '-encoding', 'UTF-8', '-err:+forbidden,discouraged', '--system', + ('"' + $Java16Home.Replace('\', '/') + '"'), '-classpath', + ('"' + (($Libraries -replace '\\', '/') -join ';') + '"'), '-d', + ('"' + $output.Replace('\', '/') + '"'), '-log', ('"' + $xmlLog.Replace('\', '/') + '"')) + $arguments += @($Sources | ForEach-Object { '"' + $_.Replace('\', '/') + '"' }) + $argFile = Join-Path $Evidence "$Name.args" + [IO.File]::WriteAllLines($argFile, $arguments, [Text.UTF8Encoding]::new($false)) + & $java17 -Xmx2G -jar $Compiler "@$argFile" *> (Join-Path $Evidence "$Name-compile.log") + $exitCode = $LASTEXITCODE + $entry = [ordered]@{ exitCode = $exitCode; sourceCount = $Sources.Count; output = $output; log = "$Name-compile.log" } + $report.compilations[$Name] = $entry + if ($exitCode -ne 0) { throw "$Name ECJ compilation failed; retain its log and XML diagnostics" } + [xml]$diagnostics = Get-Content -LiteralPath $xmlLog -Raw + $problems = @($diagnostics.SelectNodes('//*[@severity]')) + $entry.errors = @($problems | Where-Object { $_.GetAttribute('severity') -ieq 'ERROR' }).Count + $entry.warnings = @($problems | Where-Object { $_.GetAttribute('severity') -ieq 'WARNING' }).Count + if ($entry.errors) { throw "$Name ECJ XML contains errors" } + $entry.classCount = Test-ClassFiles $output + return $output +} + +$report = [ordered]@{ status = 'RUNNING'; project = $ProjectRoot; evidence = $Evidence + scope = 'Pinned Power 1.17.1 candidate; fresh ECJ main/test/helper outputs and worldless Java 16 JUnit; no Gradle or Minecraft launch' + compiler = $null; toolchains = $null; libraries = @(); sources = @(); gradleInventory = @() + compilations = [ordered]@{}; junit = $null; negativeControl = $null; junitControls = @(); capabilityBootstrap = $null + productionSourceCommit = 'a6fc3050b0e92e89abf49bf18bbbfeb87fb3a1eb'; protectedBefore = @(); protectedAfter = @() } +try { + $properties = ConvertFrom-StringData -StringData (Get-Content -LiteralPath (Join-Path $ProjectRoot 'gradle.properties') -Raw) + if ($properties.mod_id -ne 'basepower' -or $properties.mod_version -ne '2.4.9.117011' -or + $properties.minecraft_version -ne '1.17.1' -or $properties.forge_version -ne '37.1.1' -or $properties.java_version -ne '16') { + throw 'This verifier requires the pinned Power 1.17.1 candidate' + } + $report.properties = Get-Pin (Join-Path $ProjectRoot 'gradle.properties') + & git -C $ProjectRoot diff --quiet $report.productionSourceCommit -- src/main src/test test-rig/unit-bootstrap + if ($LASTEXITCODE -ne 0) { throw 'Production, tests, or bootstrap differ from the pinned source commit' } + $untracked = @(& git -C $ProjectRoot ls-files --others --exclude-standard -- src/main src/test test-rig/unit-bootstrap) + if ($LASTEXITCODE -ne 0 -or $untracked.Count) { throw 'Untracked production/test/bootstrap inputs are not permitted' } + $protectedRoots = @('build/classes', 'build/libs', 'build/libs-dev', 'build/resources', 'build/unit-bootstrap', 'bin') | + ForEach-Object { Join-Path $ProjectRoot $_ } + $report.protectedBefore = @(Get-TreePins $protectedRoots) + $resourceInputs = @(Get-TreePins @((Join-Path $ProjectRoot 'src/main/resources'), (Join-Path $ProjectRoot 'src/test/resources'))) + $report.resources = $resourceInputs + $report.legacyVerifier = Get-Pin (Join-Path $ProjectRoot 'tools/verification/Verify-EclipseCompilation.ps1') + $report.verifier = Get-Pin $PSCommandPath + $report.compiler = Get-Pin $Compiler + if ($report.compiler.sha256 -ne 'D0D43F8E2D7003E5EFED612E2CBB5F01870043397D8F1BBE536FD9128F4FCBF7') { throw 'ECJ compiler pin mismatch' } + $java17 = Join-Path $Java17Home 'bin/java.exe' + $java16 = Join-Path $Java16Home 'bin/java.exe' + $jar16 = Join-Path $Java16Home 'bin/jar.exe' + $report.toolchains = [ordered]@{ compilerJava = (Get-Pin $java17); runtimeJava = (Get-Pin $java16) + java17Release = (Get-Pin (Join-Path $Java17Home 'release')); java16Release = (Get-Pin (Join-Path $Java16Home 'release')) + systemModules = (Get-Pin (Join-Path $Java16Home 'lib/modules')); jarTool = (Get-Pin $jar16) } + if ((Get-Content -LiteralPath (Join-Path $Java16Home 'release') -Raw) -notmatch 'JAVA_VERSION="16.0.2"') { + throw 'ECJ production system image must be Java 16.0.2' + } + $negativeSource = Join-Path $PSScriptRoot 'RestrictedJdkReference117.java' + $negativeLog = Join-Path $Evidence 'restricted-jdk-negative.log' + & $java17 -jar $Compiler -16 -proc:none '-err:+forbidden,discouraged' --system $Java16Home ` + -d (Join-Path $Evidence 'negative') $negativeSource *> $negativeLog + $negativeExit = $LASTEXITCODE + $report.negativeControl = [ordered]@{ source = (Get-Pin $negativeSource); exitCode = $negativeExit + log = 'restricted-jdk-negative.log'; expected = 'sun.security.x509.X500Name is not accessible' } + if ($negativeExit -eq 0 -or (Get-Content -LiteralPath $negativeLog -Raw) -notmatch 'X500Name is not accessible') { + throw 'Restricted-JDK negative control did not fail for the expected native module-access reason' + } + [xml]$classpath = Get-Content -LiteralPath (Join-Path $ProjectRoot '.classpath') -Raw + $report.classpath = Get-Pin (Join-Path $ProjectRoot '.classpath') + $mainLibraries = @() + $testLibraries = @() + foreach ($entry in @($classpath.classpath.classpathentry | Where-Object { $_.kind -eq 'lib' })) { + $file = if ([IO.Path]::IsPathRooted([string]$entry.path)) { [IO.Path]::GetFullPath([string]$entry.path) } + else { [IO.Path]::GetFullPath((Join-Path $ProjectRoot ([string]$entry.path))) } + if ([IO.Path]::GetFileName($file) -match '^(?:basePower|PowerAdvantage).*\.jar$') { throw 'Power output jar cannot satisfy its own ECJ compilation' } + $scopeNode = $entry.SelectSingleNode('attributes/attribute[@name="gradle_used_by_scope"]/@value') + $scope = if ($scopeNode) { $scopeNode.Value } else { '' } + $pin = Get-Pin $file + $pin.scope = $scope + $report.libraries += $pin + if (!$scope -or $scope.Split(',') -contains 'main') { $mainLibraries += $file } + if (!$scope -or $scope.Split(',') -contains 'test') { $testLibraries += $file } + } + foreach ($dependency in @(@{ prefix = 'BaseMetals-'; hash = $properties.basemetals_deobf_sha256 }, + @{ prefix = 'OreSpawn-'; hash = $properties.orespawn_deobf_sha256 })) { + $matching = @($report.libraries | Where-Object { [IO.Path]::GetFileName($_.path).StartsWith($dependency.prefix) }) + if ($matching.Count -ne 1 -or $matching[0].sha256 -ne $dependency.hash) { throw "Typed dependency pin mismatch: $($dependency.prefix)" } + } + $sources = @{} + foreach ($set in @('main', 'test')) { + $sources[$set] = @(Get-ChildItem -LiteralPath (Join-Path $ProjectRoot "src/$set/java") -Recurse -File -Filter '*.java' | Sort-Object FullName | ForEach-Object { $_.FullName }) + $report.sources += @($sources[$set] | ForEach-Object { Get-Pin $_ }) + } + $classes = @() + $expectedCases = @() + $expected = 0 + foreach ($xml in @(Get-ChildItem -LiteralPath (Join-Path $ProjectRoot 'build/test-results/test') -File -Filter 'TEST-*.xml' | Sort-Object Name)) { + [xml]$inventory = Get-Content -LiteralPath $xml.FullName -Raw + if ([int]$inventory.testsuite.failures -or [int]$inventory.testsuite.errors -or [int]$inventory.testsuite.skipped) { throw 'Gradle suite inventory contains failed/skipped tests' } + $classes += [string]$inventory.testsuite.name + $expected += [int]$inventory.testsuite.tests + $expectedCases += @($inventory.testsuite.testcase | ForEach-Object { $_.classname + '#' + $_.name }) + $report.gradleInventory += Get-Pin $xml.FullName + } + if ($classes.Count -ne 43 -or $expected -ne 201 -or $sources.test.Count -ne 43 -or $sources.main.Count -ne 144) { throw 'Unexpected Power 1.17.1 source/test inventory' } + if (@($expectedCases | Sort-Object -Unique).Count -ne $expected) { throw 'Duplicate or missing baseline test identities' } + $main = Invoke-Ecj 'main' $sources.main $mainLibraries + $test = Invoke-Ecj 'test' $sources.test (@($main) + $testLibraries) + $helperSource = Join-Path $PSScriptRoot 'AuditedPowerJUnit117.java' + $controlSource = Join-Path $PSScriptRoot 'AuditedPowerJUnit117Control.java' + $report.helperSources = @((Get-Pin $helperSource), (Get-Pin $controlSource)) + $helper = Invoke-Ecj 'helper' @($helperSource, $controlSource) $testLibraries + $bootstrapSources = @(Get-ChildItem -LiteralPath (Join-Path $ProjectRoot 'test-rig/unit-bootstrap') -File -Filter '*.java' | + Sort-Object FullName | ForEach-Object { $_.FullName }) + if ($bootstrapSources.Count -ne 1) { throw 'Unexpected capability bootstrap source inventory' } + $report.bootstrapSources = @($bootstrapSources | ForEach-Object { Get-Pin $_ }) + $bootstrapClasses = Invoke-Ecj 'bootstrap' $bootstrapSources $testLibraries + $bootstrapManifest = Join-Path $Evidence 'bootstrap-manifest.mf' + [IO.File]::WriteAllText($bootstrapManifest, "Manifest-Version: 1.0`nPremain-Class: zone.moddev.mc.advantageworks.unit.ForgeCapabilityBootstrap`n`n", [Text.UTF8Encoding]::new($false)) + $capabilityBootstrap = Join-Path $Evidence 'forge-native-unit-bootstrap.jar' + & $jar16 cfm $capabilityBootstrap $bootstrapManifest -C $bootstrapClasses . *> (Join-Path $Evidence 'bootstrap-jar.log') + if ($LASTEXITCODE -ne 0) { throw 'Fresh capability bootstrap jar creation failed' } + $report.capabilityBootstrap = Get-Pin $capabilityBootstrap + foreach ($control in @('Assumed', 'Ignored')) { + $controlXml = Join-Path $Evidence "control-$control.xml" + & $java16 -cp ((@($helper) + $testLibraries) -join ';') zone.moddev.mc.poweradvantage.verification.native117.AuditedPowerJUnit117 ` + $controlXml 1 ('zone.moddev.mc.poweradvantage.verification.native117.AuditedPowerJUnit117Control$' + $control) *> (Join-Path $Evidence "control-$control.log") + $controlExit = $LASTEXITCODE + [xml]$controlResult = Get-Content -LiteralPath $controlXml -Raw + $report.junitControls += [ordered]@{ name = $control; exitCode = $controlExit; xml = (Get-Pin $controlXml) } + if ($controlExit -eq 0 -or $controlResult.testsuite.passed -ne 'false' -or [int]$controlResult.testsuite.failures -ne 0 -or + ($control -eq 'Assumed' -and ([int]$controlResult.testsuite.assumptions -ne 1 -or [int]$controlResult.testsuite.tests -ne 1)) -or + ($control -eq 'Ignored' -and ([int]$controlResult.testsuite.ignored -ne 1 -or [int]$controlResult.testsuite.tests -ne 0))) { + throw "JUnit $control negative control was not rejected with structured evidence" + } + } + $junitXml = Join-Path $Evidence 'junit.xml' + $runtime = @($main, $test, $helper, (Join-Path $ProjectRoot 'build/resources/main'), (Join-Path $ProjectRoot 'build/resources/test')) + $testLibraries + $report.runtime = $runtime + Push-Location $ProjectRoot + try { + & $java16 "-javaagent:$capabilityBootstrap" -Xmx2G -cp ($runtime -join ';') zone.moddev.mc.poweradvantage.verification.native117.AuditedPowerJUnit117 $junitXml $expected @classes *> (Join-Path $Evidence 'junit.log') + $junitExit = $LASTEXITCODE + } finally { Pop-Location } + $report.junit = [ordered]@{ exitCode = $junitExit; expected = $expected; suites = $classes; log = 'junit.log'; xml = 'junit.xml' } + if ($junitExit -ne 0) { throw 'Fresh ECJ JUnit execution failed' } + [xml]$junit = Get-Content -LiteralPath $junitXml -Raw + if ($junit.testsuite.passed -ne 'true' -or [int]$junit.testsuite.tests -ne $expected -or + [int]$junit.testsuite.failures -or [int]$junit.testsuite.ignored -or [int]$junit.testsuite.assumptions) { throw 'Strict ECJ test counts failed' } + $actualCases = @($junit.testsuite.testcase | ForEach-Object { + if ($_.finished -ne 'true') { throw 'A started ECJ test did not finish' } + $_.classname + '#' + $_.name + }) + if ($actualCases.Count -ne $expected -or @($actualCases | Sort-Object -Unique).Count -ne $expected -or + @(Compare-Object ($expectedCases | Sort-Object) ($actualCases | Sort-Object)).Count) { + throw 'Fresh ECJ test identities differ from the complete Gradle inventory' + } + $report.junit.identities = $actualCases + $report.protectedAfter = @(Get-TreePins $protectedRoots) + if (($report.protectedBefore | ConvertTo-Json -Depth 5 -Compress) -cne ($report.protectedAfter | ConvertTo-Json -Depth 5 -Compress)) { + throw 'Protected Gradle/Eclipse output inventory changed during standalone verification' + } + $pins = @($report.sources) + @($report.libraries) + @($report.helperSources) + @($report.negativeControl.source) + + @($report.bootstrapSources) + @($report.capabilityBootstrap) + @($report.gradleInventory) + @($report.resources) + + @($report.compiler) + @($report.classpath) + @($report.properties) + @($report.verifier) + @($report.legacyVerifier) + @($report.toolchains.Values) + Assert-Unchanged $pins + $report.status = 'PASS' +} catch { + $report.status = 'FAIL' + $report.error = $_.ToString() + throw +} finally { Save-Report } +Get-Content -LiteralPath (Join-Path $Evidence 'junit.log') -Tail 8 +Write-Output "ECJ PASS: $($sources.main.Count) main, $($sources.test.Count) test sources; $expected tests; Java 16 class files"