diff --git a/.github/test-base-metals-pin.sh b/.github/test-base-metals-pin.sh new file mode 100644 index 00000000..8d5da556 --- /dev/null +++ b/.github/test-base-metals-pin.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +verify="$(cd "$(dirname "$0")" && pwd)/verify-base-metals-pin.sh" +temporary_root="$(cd "${TMPDIR:-/tmp}" && pwd -P)" +scratch="$(mktemp -d "$temporary_root/basepower-pin.XXXXXXXX")" +cleanup() { + local resolved + resolved="$(cd "$scratch" && pwd -P)" + case "$resolved" in + "$temporary_root"/basepower-pin.*) rm -rf -- "$resolved" ;; + *) echo 'Refusing to clear an unexpected test directory' >&2; return 1 ;; + esac +} +trap cleanup EXIT +mkdir -p "$scratch/bin" "$scratch/project" +cat > "$scratch/bin/gh" <<'MOCK' +#!/usr/bin/env bash +set -euo pipefail +[[ "$#" == 4 && "$1" == api && "$2" == "$EXPECTED_COMPARE" && "$3" == --jq && "$4" == .status ]] +printf '%s\n' "$PIN_STATUS" +MOCK +chmod +x "$scratch/bin/gh" +export PATH="$scratch/bin:$PATH" +commit=1111111111111111111111111111111111111111 +cd "$scratch/project" +for minecraft in 1.20.6 1.21.1 1.21.11; do + printf 'minecraft_version=%s\nbasemetals_source_commit=%s\n' "$minecraft" "$commit" > gradle.properties + export EXPECTED_COMPARE="repos/MinecraftModDevelopmentMods/BaseMetals/compare/$commit...master-$minecraft" + export GITHUB_OUTPUT="$scratch/output" + for PIN_STATUS in identical ahead; do + export PIN_STATUS + : > "$GITHUB_OUTPUT" + bash "$verify" + grep -qx "commit=$commit" "$GITHUB_OUTPUT" + done + for PIN_STATUS in behind diverged; do + export PIN_STATUS + : > "$GITHUB_OUTPUT" + if bash "$verify" > "$scratch/rejected.log" 2>&1; then + echo "Incorrectly accepted $minecraft / $PIN_STATUS" >&2 + exit 1 + fi + test ! -s "$GITHUB_OUTPUT" + done +done +printf 'minecraft_version=invalid\nbasemetals_source_commit=%s\n' "$commit" > gradle.properties +if bash "$verify" > "$scratch/invalid.log" 2>&1; then + echo 'Incorrectly accepted an invalid target' >&2 + exit 1 +fi +echo 'PASS: target-derived Base Metals ancestry guards and rejection controls' diff --git a/.github/verify-base-metals-pin.sh b/.github/verify-base-metals-pin.sh index 255270db..e526e396 100644 --- a/.github/verify-base-metals-pin.sh +++ b/.github/verify-base-metals-pin.sh @@ -7,7 +7,12 @@ if [[ ! "$commit" =~ ^[0-9a-f]{40}$ ]]; then exit 1 fi -branch=master-1.20.6 +minecraft="$(sed -n 's/^minecraft_version=//p' gradle.properties)" +if [[ ! "$minecraft" =~ ^1\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Expected a complete Minecraft target version in gradle.properties" + exit 1 +fi +branch="master-$minecraft" if ! status="$(gh api "repos/MinecraftModDevelopmentMods/BaseMetals/compare/$commit...$branch" --jq '.status')"; then echo "::error::Could not verify Base Metals $commit on upstream $branch" exit 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 760c6786..c7e311bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,16 @@ -name: basePower 1.20.6 CI +name: basePower 1.21.1 CI on: push: - branches: [master-1.20.6, 'feature/**'] + branches: [master-1.21.1, 'feature/**'] pull_request: - branches: [master-1.20.6] + branches: [master-1.21.1] permissions: contents: read concurrency: - group: basepower-1.20.6-${{ github.workflow }}-${{ github.ref }} + group: basepower-1.21.1-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -21,7 +21,9 @@ jobs: steps: - name: Check out PowerAdvantage uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Require the Base Metals pin on upstream master-1.20.6 + - name: Test target-derived dependency guards + run: bash .github/test-base-metals-pin.sh + - name: Require the Base Metals pin on upstream master-1.21.1 id: base-metals-pin env: GH_TOKEN: ${{ github.token }} @@ -32,13 +34,13 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/OreSpawn - ref: 4085315345ff043448b316d75e03f4b8da45402d + ref: 62c08169c71f4d4fd8f107289e63fd2a4c7c655c path: .ci-dependencies/OreSpawn - name: Check out pinned Base Metals uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/BaseMetals - ref: 9e706db5c85fa2bc7f36dee94f39a5f2fa8ab367 + ref: 327c197bcdd0bb5e883d844bb6a6c8791bc580b7 path: .ci-dependencies/BaseMetals - name: Install pinned Java 25 Mavenizer runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 @@ -74,11 +76,10 @@ jobs: mavenizer="$orespawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar" mirror="$RUNNER_TEMP/orespawn-mirror" (cd "$basemetals" && bash gradle/stage-orespawn-release.sh "$mirror") - chmod +x "$orespawn/gradlew" "$basemetals/gradlew" - (cd "$orespawn" && ./gradlew $MMD_GRADLE_TOOLCHAINS jar --no-daemon --stacktrace) - (cd "$basemetals" && ./gradlew $MMD_GRADLE_TOOLCHAINS jar -PorespawnVerificationRepository="$mirror" --no-daemon --stacktrace) - osjar="$orespawn/build/libs/OreSpawn-4.1.0.120061.jar" - bmjar="$basemetals/build/libs/BaseMetals-3.1.2.120061.jar" + (cd "$orespawn" && bash gradlew $MMD_GRADLE_TOOLCHAINS jar --no-daemon --stacktrace) + (cd "$basemetals" && bash gradlew $MMD_GRADLE_TOOLCHAINS jar -PorespawnVerificationRepository="$mirror" --no-daemon --stacktrace) + osjar="$orespawn/build/libs/OreSpawn-4.1.0.121011.jar" + bmjar="$basemetals/build/libs/BaseMetals-3.1.2.121011.jar" test -f "$osjar" && test -f "$bmjar" echo "orespawn=$osjar" >> "$GITHUB_OUTPUT" echo "basemetals=$bmjar" >> "$GITHUB_OUTPUT" @@ -89,25 +90,25 @@ jobs: writeReleaseChecksums verifyEclipseProductionClasspath -PbaseMetalsDeobfJar=${{ steps.dependencies.outputs.basemetals }} -PoreSpawnDeobfJar=${{ steps.dependencies.outputs.orespawn }} - -Pforge50MavenizerJar=${{ steps.dependencies.outputs.mavenizer }} + -Pforge52MavenizerJar=${{ steps.dependencies.outputs.mavenizer }} --no-daemon --stacktrace - name: Upload audited release candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: basePower-1.20.6-${{ github.sha }} + name: basePower-1.21.1-${{ github.sha }} if-no-files-found: error retention-days: 30 path: | - build/libs/basePower-2.4.13.120061.jar - build/libs/basePower-2.4.13.120061-sources.jar - build/libs/basePower-2.4.13.120061-javadoc.jar + build/libs/basePower-2.4.14.121011.jar + build/libs/basePower-2.4.14.121011-sources.jar + build/libs/basePower-2.4.14.121011-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt - name: Upload diagnostics on failure if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: basePower-1.20.6-diagnostics-${{ github.sha }} + name: basePower-1.21.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 c0b91bf7..95af822a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,9 +2,9 @@ name: CodeQL on: push: - branches: [master-1.20.6, 'feature/**'] + branches: [master-1.21.1, 'feature/**'] pull_request: - branches: [master-1.20.6] + branches: [master-1.21.1] schedule: - cron: '47 7 * * 4' @@ -21,7 +21,9 @@ jobs: steps: - name: Check out PowerAdvantage uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Require the Base Metals pin on upstream master-1.20.6 + - name: Test target-derived dependency guards + run: bash .github/test-base-metals-pin.sh + - name: Require the Base Metals pin on upstream master-1.21.1 id: base-metals-pin env: GH_TOKEN: ${{ github.token }} @@ -32,13 +34,13 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/OreSpawn - ref: 4085315345ff043448b316d75e03f4b8da45402d + ref: 62c08169c71f4d4fd8f107289e63fd2a4c7c655c path: .ci-dependencies/OreSpawn - name: Check out pinned Base Metals uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/BaseMetals - ref: 9e706db5c85fa2bc7f36dee94f39a5f2fa8ab367 + ref: 327c197bcdd0bb5e883d844bb6a6c8791bc580b7 path: .ci-dependencies/BaseMetals - name: Install pinned Java 25 Mavenizer runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 @@ -77,8 +79,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 -PorespawnVerificationRepository="$mirror" --no-daemon --stacktrace) - echo "orespawn=$orespawn/build/libs/OreSpawn-4.1.0.120061.jar" >> "$GITHUB_OUTPUT" - echo "basemetals=$basemetals/build/libs/BaseMetals-3.1.2.120061.jar" >> "$GITHUB_OUTPUT" + echo "orespawn=$orespawn/build/libs/OreSpawn-4.1.0.121011.jar" >> "$GITHUB_OUTPUT" + echo "basemetals=$basemetals/build/libs/BaseMetals-3.1.2.121011.jar" >> "$GITHUB_OUTPUT" echo "mavenizer=$mavenizer" >> "$GITHUB_OUTPUT" - name: Initialize CodeQL uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 @@ -91,6 +93,6 @@ jobs: ./gradlew $MMD_GRADLE_TOOLCHAINS clean classes verifyReleaseDependencies --rerun-tasks --no-build-cache --no-daemon --stacktrace \ -PbaseMetalsDeobfJar=${{ steps.dependencies.outputs.basemetals }} \ -PoreSpawnDeobfJar=${{ steps.dependencies.outputs.orespawn }} \ - -Pforge50MavenizerJar=${{ steps.dependencies.outputs.mavenizer }} + -Pforge52MavenizerJar=${{ steps.dependencies.outputs.mavenizer }} - name: Analyze uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 diff --git a/.github/workflows/validate-gradle-build.yml b/.github/workflows/validate-gradle-build.yml index 2c47d128..2f2c732b 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.20.6 + - master-1.21.1 - 'feature/**' pull_request: branches: - - master-1.20.6 + - master-1.21.1 permissions: contents: read diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4c214e6e..7a2db808 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,13 @@ +basePower 2.4.14.121011 (Minecraft 1.21.1) + +- Bring basePower to Forge 52 and Java 21, keeping existing blocks, items and machine data. +- Use the new recipe, advancement, loot and tag folders without changing their in-game names. +- Update fluid gauges to the current renderer while keeping the full texture visible. +- Preserve custom attributes on old modded entities when Minecraft upgrades their save format. +- Handle the new data-driven painting and enchantment registries when upgrading 1.20.6 worlds. Keep a recovery copy of the old enchantment indexes and map the renamed trial-spawner sounds. +- Keep the shared Base Metals upgrade hook for old Advantage and released base* worlds. +- Correct the local Base Metals path used by Eclipse and check dependency pins before compilation. + basePower 2.4.13.120061 (Minecraft 1.20.6) - Bring basePower to Forge 50 and Java 21 without changing the names of existing machines or items. diff --git a/README.md b/README.md index a193c8bf..02392ed9 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,34 @@ # basePower -**Minecraft 1.20.6 (Forge), version 2.4.13.120061.** +**Minecraft 1.21.1 (Forge), version 2.4.14.121011.** -See the [1.20.6 port record](docs/1.20.6/PORT-STATUS.md) for the current checks. -Results from earlier versions do not qualify this build. +This version keeps the existing machines and saved-world data while updating +to Forge 52 and Java 21. See the [1.21.1 test results](docs/1.21.1/TEST-RESULTS.md) +for the checked upgrade routes, machine tests and remaining modpack limits. + +For the combined Eclipse test, use `runClientAllAdvantage.launch` in this +project. It loads the matching baseSteam and baseElectric candidates. The +[port record](docs/1.21.1/PORT-STATUS.md) separates the automated checks from +the remaining manual gameplay and visual checks. basePower continues DrCyano's Power Advantage. It provides shared power, fluids and item transport for baseSteam and baseElectric. -## Playing On 1.20.6 +## Playing On 1.21.1 -- Install [MMD OreSpawn 4.1.0.120061](https://www.curseforge.com/minecraft/mc-mods/mmd-orespawn/files/9183384) on both client and server. +- Install [MMD OreSpawn 4.1.0.121011](https://www.curseforge.com/minecraft/mc-mods/mmd-orespawn/files/9189594) on both client and server. - Base Metals is optional for a new basePower world. For a world that already - uses Base Metals, keep it installed and use the matching Forge 1.20.6 build - `3.1.2.120061`. Older Base Metals builds do not have the shared upgrade hook. + uses Base Metals, keep it installed and use the matching Forge 1.21.1 build + `3.1.2.121011` or a later compatible Forge 52 release. Older Base Metals + builds do not have the shared upgrade hook. - baseSteam and baseElectric are separate mods. Use only their matching - 1.20.6 builds. Additional Loot Tables is not needed for the built-in loot. + 1.21.1 builds. Additional Loot Tables is not needed for the built-in loot. **Back up the entire world before upgrading. Test only disposable copies.** The port keeps existing registry names and machine data. The upgrade checks use disposable copies of original Advantage and released base* saves, including connected machines, and check them again after a full restart. Check that the -other mods in your world support 1.20.6 before you upgrade. +other mods in your world support 1.21.1 before you upgrade. The accepted [1.14 results](docs/1.14/BROADER-QUALIFICATION.md) are historical evidence, not qualification of this port or a whole modpack. @@ -29,21 +36,21 @@ 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.20.6 +## Building Minecraft 1.21.1 -The build uses Forge 50.2.0, official `1.20.6` mappings, -MCP `20240429.135109`, ForgeGradle 7.0.34 and Gradle 9.6.1. +The build uses Forge 52.1.0, official `1.21.1` mappings, +MCP `20240808.132146`, ForgeGradle 7.0.34 and Gradle 9.6.1. Use Java 21 for Gradle, compilation and Minecraft. Mavenizer also needs Java 25; the development launcher helper uses Java 8. Pass the installed homes using `-Porg.gradle.java.installations.paths=,,` and disable automatic discovery and downloads for a reproducibility check. -Use the pinned Base Metals `3.1.2.120061` and OreSpawn `4.1.0.120061` jars. +Use the pinned Base Metals `3.1.2.121011` and OreSpawn `4.1.0.121011` jars. The source pins and checksums are in `gradle.properties`. -Forge 50 also needs the Mavenizer compatibility jar in the pinned OreSpawn +Forge 52 also needs the Mavenizer compatibility jar in the pinned OreSpawn checkout's `ci-fixtures/tools/` directory. It fixes decompiled Forge sources on a clean build and uses Java 25. It is not part of the mod or game runtime. -Set `-Pforge50MavenizerJar=` when that checkout is in another location. +Set `-Pforge52MavenizerJar=` when that checkout is in another location. If Base Metals is installed, use the pinned 3.1.2 build when testing an upgrade. It lets both mods preserve old items and machines before Minecraft converts the chunk. An older Base Metals jar is not a substitute for this upgrade path. @@ -57,8 +64,8 @@ gradlew.bat clean check build javadoc genEclipseRuns eclipse isolateEclipseProdu The default dependency jars are: ```text -D:\BaseMetals\.verification-basemetals1206\Advantage shared hook\build\libs\BaseMetals-3.1.2.120061.jar -D:\MinecraftMineralogy\OS1206\OreSpawn\build\libs\OreSpawn-4.1.0.120061.jar +D:\BaseMetals\.verification-basemetals1211\Advantage shared hook\build\libs\BaseMetals-3.1.2.121011.jar +D:\MinecraftMineralogy\OS1211\OreSpawn\build\libs\OreSpawn-4.1.0.121011.jar ``` Other layouts can pass `-PbaseMetalsDeobfJar=` and `-PoreSpawnDeobfJar=`. @@ -67,10 +74,10 @@ Dependencies are never embedded in PowerAdvantage artifacts. Dependency checksums and source commits are recorded in `gradle.properties`. Unknown artifacts fail the dependency audit. The release audit also rejects a player jar that differs from the frozen test kit. Hosted CI requires the Base -Metals pin to be on its upstream `master-1.20.1` branch; a locally tested pin +Metals pin to be on its upstream `master-1.21.1` branch; a locally tested pin does not qualify a hosted build. -Release jars are written to `build/libs`. Forge 50 uses the same mod jar for play +Release jars are written to `build/libs`. Forge 52 uses the same mod jar for play and development; `deobfJar` is kept as a task alias for older build scripts. For Eclipse, import this repository as an existing Gradle project and run: @@ -83,10 +90,10 @@ Refresh the project in Eclipse, allow its Java build to finish, and launch the g Eclipse copies production resources from `build/resources/main`, not directly from `src/main/resources`. This prevents its builder from overwriting the expanded `mods.toml` with raw `${version}` placeholders. After updating this setup, use **Gradle > Refresh Gradle Project**, then **Project > Clean** before relaunching. After editing resources or version properties, run `processResources` or refresh the Gradle project before the Eclipse build. See the [metadata regression notes](docs/1.13/ECLIPSE-METADATA.md). -To prepare a combined client, build the baseSteam and baseElectric `2.2.14.120061` jars, +To prepare a combined client, build the baseSteam and baseElectric `2.2.15.121011` jars, run `gradlew.bat prepareCombinedEclipseLaunch`, refresh Eclipse, then launch `runClientAllAdvantage.launch`. Inputs must match -`test-rig/profiles/native1206/dev-core.json`; alternative layouts can pass +`test-rig/profiles/native1211/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. @@ -122,6 +129,6 @@ its presence is not proof that an upgrade is safe. The [legacy migration guide](docs/1.13/MIGRATION.md) describes the earlier protocols. The [Advantage Works 1.20.1 guide](test-rig/NATIVE1201.md) describes the prior -qualification and the checks carried into 1.20.6. +qualification and the checks carried into 1.21.1. 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 65e1cd6e..5fecf6ca 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ plugins { id 'net.minecraftforge.gradle' version '7.0.34' } -apply from: 'gradle/forge50-mavenizer.gradle' +apply from: 'gradle/forge52-mavenizer.gradle' group = project.mod_group version = project.mod_version @@ -133,7 +133,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.20.6 launches.' + description = 'Merges classes and processed resources for Forge 1.21.1 launches.' dependsOn tasks.named('classes') from sourceSets.main.output into forgeRunModClassesDirectory @@ -169,11 +169,11 @@ repositories { // build to a drive letter. CI and other layouts can override either path. def baseMetalsDeobfJar = file(providers.gradleProperty('baseMetalsJar') .orElse(providers.gradleProperty('baseMetalsDeobfJar')) - .getOrElse('../../../BaseMetals/.verification-basemetals1206/Advantage shared hook/build/libs/' + .getOrElse('../../../BaseMetals/.verification-basemetals1211/Advantage shared hook/build/libs/' + "BaseMetals-${project.basemetals_version}.jar")) def oreSpawnDeobfJar = file(providers.gradleProperty('oreSpawnJar') .orElse(providers.gradleProperty('oreSpawnDeobfJar')) - .getOrElse('../../../MinecraftMineralogy/OS1206/OreSpawn/build/libs/' + .getOrElse('../../../MinecraftMineralogy/OS1211/OreSpawn/build/libs/' + "OreSpawn-${project.orespawn_version}.jar")) dependencies { @@ -252,7 +252,7 @@ tasks.named('jar', Jar) { } } -// Forge 50 uses the official names at runtime, so the same jar serves both uses. +// Forge 52 uses the official names at runtime, so the same jar serves both uses. def releaseJar = tasks.named('jar', Jar) tasks.register('deobfJar') { @@ -305,17 +305,17 @@ tasks.register('verifyReleaseConfiguration') { description = 'Validates release identity, target metadata, and optional integration policy.' doLast { def pack = new JsonSlurper().parse(file('src/main/resources/pack.mcmeta')).pack - if (pack.pack_format != 32 || pack.supported_formats != [min_inclusive: 32, max_inclusive: 41]) { - throw new GradleException('Minecraft 1.20.6 requires resource format 32 and data format 41') + if (pack.pack_format != 34 || pack.supported_formats != [min_inclusive: 34, max_inclusive: 48]) { + throw new GradleException('Minecraft 1.21.1 requires resource format 34 and data format 48') } - if (project.mod_version != '2.4.13.120061') { + if (project.mod_version != '2.4.14.121011') { throw new GradleException("Unexpected PowerAdvantage release version: ${project.mod_version}") } - if (project.minecraft_version != '1.20.6' - || project.forge_version != '50.2.0' + if (project.minecraft_version != '1.21.1' + || project.forge_version != '52.1.0' || project.mapping_channel != 'official' - || project.mapping_version != '1.20.6' - || project.mcp_version != '20240429.135109') { + || project.mapping_version != '1.21.1' + || project.mcp_version != '20240808.132146') { throw new GradleException('Unexpected Minecraft, Forge, or mappings target') } if (project.loader_name != 'forge' || project.loader_code != '1' @@ -332,19 +332,19 @@ tasks.register('verifyReleaseConfiguration') { } if (project.cf_requirements != 'base-metals,mmd-orespawn' || project.cf_optionals != 'additional-loot-tables' - || project.cf_versions != '1.20.6') { + || project.cf_versions != '1.21.1') { throw new GradleException('Unexpected CurseForge dependency or game-version metadata') } - if (project.basemetals_version != '3.1.2.120061' - || project.orespawn_version != '4.1.0.120061' + if (project.basemetals_version != '3.1.2.121011' + || project.orespawn_version != '4.1.0.121011' || !(project.basemetals_source_commit ==~ /[a-f0-9]{40}/) - || project.orespawn_source_commit != '4085315345ff043448b316d75e03f4b8da45402d') { + || project.orespawn_source_commit != '62c08169c71f4d4fd8f107289e63fd2a4c7c655c') { throw new GradleException('Unexpected Base Metals or OreSpawn dependency identity') } String mainSource = file( 'src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java').getText('UTF-8') if (!mainSource.contains('MODID = "basepower"') - || !mainSource.contains('VERSION = "2.4.13.120061"') + || !mainSource.contains('VERSION = "2.4.14.121011"') || !mainSource.contains('@Mod(PowerAdvantage.MODID)')) { throw new GradleException('PowerAdvantage source identity changed') } @@ -376,7 +376,7 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Base Metals must remain Forge-optional; CurseForge installs it for players') } String steelBarsTag = file( - 'src/main/resources/data/forge/tags/items/bars/steel.json').getText('UTF-8') + 'src/main/resources/data/forge/tags/item/bars/steel.json').getText('UTF-8') if (!steelBarsTag.contains('"required": false') || !steelBarsTag.contains('"basemetals:steel_bars"')) { throw new GradleException('Steel bars must use Forge optional tag entries when Base Metals is absent') } @@ -391,7 +391,7 @@ tasks.register('verifyReleaseConfiguration') { 'rods/copper', 'rods/wooden', 'stone', 'storage_blocks/diamond', 'storage_blocks/steel' ].each { tagPath -> - if (!file("src/main/resources/data/forge/tags/items/${tagPath}.json").isFile()) { + if (!file("src/main/resources/data/forge/tags/item/${tagPath}.json").isFile()) { throw new GradleException("Missing decoupled Forge item tag declaration: ${tagPath}") } } @@ -450,6 +450,11 @@ tasks.register('verifyReleaseDependencies') { } } +// IDE synchronization and plain compileJava must audit the same pins as a release build. +tasks.named('compileJava') { + dependsOn tasks.named('verifyReleaseDependencies') +} + tasks.register('verifyDevelopmentRuntimeArchives') { group = 'verification' description = 'Reads every development runtime jar entry and verifies its CRC before launching Minecraft.' @@ -542,8 +547,8 @@ tasks.register('verifyReleaseArtifacts') { 'cyano/poweradvantage/api/PoweredEntity.class', 'cyano/poweradvantage/api/fluid/FluidNetworkApi.class', 'assets/basepower/lang/en_us.json', - 'data/basepower/recipes/fluid_drain.json', - 'data/basepower/advancements/recipes/sprocket_rod_normal.json'].each { required -> + 'data/basepower/recipe/fluid_drain.json', + 'data/basepower/advancement/recipes/sprocket_rod_normal.json'].each { required -> if (!names.contains(required)) { throw new GradleException("Release jar is missing ${required}") } @@ -1009,7 +1014,7 @@ tasks.register('verifyEclipseProductionClasspath') { if (main != 'net.minecraftforge.launcher.Main' || !program.startsWith('--main net.minecraftforge.bootstrap.ForgeBootstrap ') || !program.contains(" -- --launchTarget ${target} ")) { - throw new GradleException("${name} does not start the Forge 50 bootstrap") + throw new GradleException("${name} does not start the Forge 52 bootstrap") } ['--cache', '--metadata', '--to-srg', '--to-obf'].each { option -> def argument = program =~ /(?:^|\s)${option}\s+(?:"([^"]+)"|([^"\s]+))(?=\s+--|$)/ @@ -1155,7 +1160,7 @@ tasks.register('verifyModelAssets') { tasks.register('verifyRecipeTagGuards') { group = 'verification' description = 'Rejects recipes that can collapse when a Forge material tag is empty.' - File recipeRoot = file("src/main/resources/data/${project.mod_id}/recipes") + File recipeRoot = file("src/main/resources/data/${project.mod_id}/recipe") inputs.dir(recipeRoot) doLast { Closure collectTags @@ -1240,9 +1245,9 @@ tasks.register('clientResourceProbeJar', Jar) { // The expansion jars are runtime inputs, never release contents or compile dependencies. def advantageExtensionJars = [ basesteam: file(providers.gradleProperty('steamAdvantageDeobfJar').getOrElse( - '../SteamAdvantage/SteamAdvantage/build/libs/baseSteam-2.2.14.120061.jar')), + '../SteamAdvantage/SteamAdvantage/build/libs/baseSteam-2.2.15.121011.jar')), baseelectric: file(providers.gradleProperty('electricAdvantageDeobfJar').getOrElse( - '../ElectricAdvantage/ElectricAdvantage/build/libs/baseElectric-2.2.14.120061.jar')) + '../ElectricAdvantage/ElectricAdvantage/build/libs/baseElectric-2.2.15.121011.jar')) ] tasks.register('prepareCombinedEclipseLaunch') { @@ -1252,7 +1257,7 @@ tasks.register('prepareCombinedEclipseLaunch') { dependsOn tasks.named('deobfJar') doLast { File inputProfile = file(providers.gradleProperty('advantageDevelopmentProfile').getOrElse( - 'test-rig/profiles/native1206/dev-core.json')) + 'test-rig/profiles/native1211/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.21.1/PORT-STATUS.md b/docs/1.21.1/PORT-STATUS.md new file mode 100644 index 00000000..0e026db8 --- /dev/null +++ b/docs/1.21.1/PORT-STATUS.md @@ -0,0 +1,108 @@ +# Minecraft 1.21.1 Port + +Status: automated qualification and manual acceptance passed on +11 November 2026. Publication is handled separately by the guarded release +workflow. See the +[test results](TEST-RESULTS.md) for the exact scope and remaining checks. + +The port starts from the published 1.20.6 basePower source, commit +`d1cdc73d19551786ab4524879cb9abca8c881251`. It uses Forge 52.1.0, +ForgeGradle 7.0.34 and Java 21. The matching baseSteam and baseElectric +ports are being qualified together. + +## Dependencies + +- MMD OreSpawn `4.1.0.121011`, published file 9189594. +- Base Metals `3.1.2.121011`, merged migration-hook commit + `327c197bcdd0bb5e883d844bb6a6c8791bc580b7`. + +Do not substitute an older Base Metals jar for a world-upgrade test. +All source worlds and earlier qualification +receipts are kept unchanged; each run uses a disposable copy. + +## Port Changes + +Recipes, advancement files, loot tables and item tags use Minecraft's new +singular folder names. Their registry and logical advancement names remain +unchanged. Native recipe inputs, block-entity validation and rendering APIs +replace their previous equivalents. + +Minecraft 1.21 also changes entity attributes and makes enchantments +data-driven. A regression first demonstrated loss of a modded entity's swim +speed, then verified the repair through the native entity reader. Saved tool +checks now verify the loaded enchantment level, not just the stored JSON/NBT. + +The 1.20.6 upgrade check exposed old Forge painting indexes and two renamed +trial-spawner sounds. The port retires only the 30 vanilla painting indexes +whose names still exist in the native data-pack registry, and maps those two +sounds to their current names. The obsolete static enchantment index is +archived in `basepower-retired-registry-indexes.dat`, without changing any +item enchantments or the pre-flattening numeric-ID archive. The original +`level.dat` is backed up before changes. Unknown painting entries are not +silently discarded. + +## Validation Gates + +- Power unit, dependency, identity, resource, archive and Eclipse audits: passed. +- Packaged Power fluid, shared-crushing and full-restart checks: passed. +- Combined finite machine jobs: 132 cases passed, followed by typed saved-NBT + inspection and a full process restart. Both stopped saves are checked. +- Recipe discovery and survival progression: passed in all three recipe modes, + with and without rubber, including reload and full restart. +- Direct original Advantage 1.10, released base* 1.10 and 1.12 upgrades: passed. + A retained 1.20.6 fixture also passed conversion, restart and connected operation. +- Historical player and entity inventories: passed. Nether and End fixtures + retain a separately recorded native wolf difference, described below. +- Partial networks, bridge unloads and extended moving drills: passed. + The lifecycle test passed all six facings, five chunk cycles, full restart, + tagged inventory conservation and mid-block progress checks. +- Fresh and migrated endurance: both passed 30 minutes, full restart and + resumed processing. An independent replay passed all 33 checkpoints and + 528 station snapshots per world, at more than 19.8 sampled ticks per second. +- Mineralogy oil transport, generation ownership, legacy OreSpawn imports + and optional power-converter profiles: passed, including restarts. +- Combined client: 94 items, 905 block states, 87 creative entries and recipe-book + grouping passed. All 26 tank-gauge images matched independent sprite renders. + All 351 required-caption cases passed; 355 broken controls were rejected. +- Independent cache, path-with-spaces and reproducibility checks: passed for + all three mods, with matching player, sources and Javadoc jars. +- Unit checks: 226 Power, 228 Steam, 106 Electric and 329 harness JUnit tests, + plus 2,247 Python regression tests, passed. Separate Eclipse ECJ compilation + and expansion GameTests passed. These counts are not combined across reruns. +- Manual gameplay and visual acceptance: passed. + +## Native Entity Difference + +Minecraft's own 1.21.1 data fixer changes the old tamed-wolf fixture from +health 8 and maximum 20 to health 16 and maximum 40. Independent controls +with no Advantage classes reproduce that change for both old data versions. +The raw strict comparison remains failed in the report. A separate +`PASS_WITH_NATIVE_DIFFERENCE` assessment accepts only those two values, +with the same owner and UUID; all other fields must still match. Inventory +loss, another changed value or a changed control checksum fails the check. + +## Manual Testing + +Open the existing `D:\PowerAdvantage\PA1211` Eclipse workspace, refresh the +Gradle projects and use Power's `runClientAllAdvantage.launch`. It loads all +three mods with the pinned dependencies. The individual launches remain +available for isolated checks. Use Java 21 to run Minecraft. + +Check the creative tab, recipe discovery during normal play, machine screens +and their tooltips, live turbine/drill animation, and your usual connected +setups. A real player login and multiplayer menu interaction are still manual +checks. The automated client uses isolated, hashed development output; +packaged server tests use the exact frozen player jars. Neither is a claim +that every third-party modpack or resource pack is compatible. + +The Metal Fluid Tank still drops empty, as in the accepted earlier ports. +The Portable Fluid Tank is the one that keeps its fluid when picked up in +Survival. Do not use an original world for a manual upgrade test. + +Exact input hashes, failed runs and subsequent results are retained locally +under `_verification/base-1211-port-20261111` and the rig's ignored `build` +directory. The final index is `final-qualification.json`; the five-player-jar +manual kit is in `manual-test-jars/`, with checksums and no harness. +These checks qualify only their recorded inputs, not an arbitrary +modpack. Merged CI and public downloads must reproduce the accepted player +jar before the dependent releases proceed. diff --git a/docs/1.21.1/TEST-RESULTS.md b/docs/1.21.1/TEST-RESULTS.md new file mode 100644 index 00000000..788e812b --- /dev/null +++ b/docs/1.21.1/TEST-RESULTS.md @@ -0,0 +1,107 @@ +# 1.21.1 Local Test Results + +The coordinated port passed automated qualification and manual acceptance. +These results were recorded on 11 November 2026 using Forge 52.1.0 and Java 21. +Release checks still verify the merged artifact and public downloads separately. + +## Frozen Candidates + +| Mod | Version | Player jar SHA-256 | +| --- | --- | --- | +| basePower | 2.4.14.121011 | `F91A613EB5FAA218AAD2DC3C881DEC7B897A5EE874215282A439E34B915C1AE6` | +| baseSteam | 2.2.15.121011 | `EA305B9060BC2A84021C7299249AA79AA593EA8A37665C18D0B3483902F1046A` | +| baseElectric | 2.2.15.121011 | `CFE7EF9CDAB3BD4DB0342562B2D9CEBAA19E78119C4D4AAD4B637B5BF0A59560` | + +The dependencies are OreSpawn 4.1.0.121011 and Base Metals +3.1.2.121011, which includes the shared migration hook. +The complete input hashes, source commits and receipt hashes are in +`D:\PowerAdvantage\_verification\base-1211-port-20261111\final-qualification.json`. + +## What Passed + +- Full release, dependency, identity, resource, bytecode, archive and Eclipse + checks for all three mods. No dependency or harness classes enter player jars. +- Clean-cache and path-with-spaces builds reproduced each mod's player, + sources and Javadoc jars byte-for-byte. +- 226 Power, 228 Steam, 106 Electric and 329 harness JUnit tests, with no + failures, errors or skips. The Python suite passed 2,247 tests. Separate + Eclipse ECJ compilation, expansion resource tests and GameTests passed. +- All 21 Works bays in shared and decoupled runtimes. The finite-operation + suite passed 132 machine jobs, raw saved-NBT checks and cold-restart checks. +- Recipe discovery and progression in NORMAL, TECH_PROGRESSION and + APOCALYPTIC modes, with and without rubber, before and after restart. +- Direct upgrades from original Advantage 1.10 and released base* 1.10 and + 1.12 fixtures. Each passed conversion, full restart and connected operation + without rebuilding the machines. A retained 1.20.6 fixture passed too. +- Historical player inventories and nested items, entity inventories and + stopped entity storage. Nether and End fixtures passed with the separately + recorded native wolf difference described in the port record. +- Partial-network and bridge unloads, machine-only and supply-only unloads, + extended drill travel, harvest and the public fluid API. +- Moving-machine lifecycle in all six facings: tagged items, rear output, + five chunk cycles, progress restoration and full restart. +- Both Advantage oils and Mineralogy crude oil through bucket placement, + drains, pipes and both tanks, including full-process restarts. Distillation, + generation ownership, legacy OreSpawn import and converter profiles passed. +- Combined client resources: 94 items, 905 block states, 87 creative entries + and recipe-book grouping. All 26 whole-gauge images matched independent + native sprite renders. All 351 required-caption cases passed, and all 355 + deliberately broken controls were rejected. + +## Endurance + +Both a fresh world and a copy of the original-1.10 migrated fixture ran for +30 measured minutes. They then stopped completely, restarted and completed +another minute of processing. No source fixture or server seed changed. + +An independent replay verified 33 checkpoints and 528 station snapshots in +each world. Sampled rates were 19.82 and 19.81 game ticks per second, above +the required 18. Each world produced 30,000 mB of distilled output, 480 items +in each conveyor/filter lane and 120 products in each measured processing +lane, with the recorded inputs and outputs reconciled. + +The station cards still say `MANUAL_REQUIRED` for their human checks. That +does not mean those checks were performed automatically. The separate item, +fluid, energy, structural and restart assertions are what passed. + +## Repairs And Failed Evidence + +An Eclipse follow-up found that Power's default Base Metals location still +pointed at the previous version's checkout. Earlier qualification commands +supplied the correct jar explicitly, so they did not test that local default. +The default now points at the 1.21.1 migration-hook build, and ordinary +`compileJava` checks the dependency hashes before compiling. Local IDE setup +must also be checked without dependency-path overrides; an override-only pass +does not qualify the defaults. The follow-up passed the default build and +Eclipse generation without dependency overrides, both Eclipse compiler scopes, +and the combined client using Eclipse output and its bundled Java 21.0.12. +A wrong dependency was rejected before compilation. The player jar remains +byte-for-byte identical to the qualified candidate, so this build-only repair +does not replace or invalidate its machine and endurance receipts. + +The port fixes the loss of old modded-entity attributes and handles the +vanilla registry changes encountered when opening 1.20.6 worlds. The old +enchantment registry index is archived rather than silently discarded. + +Failed and held runs remain intact. Driver repairs included the native chunk +ticket radius, inventory-component commands and representation accessors, +LED light observation, and a tick-freeze barrier only while priming endurance +inputs. Normal processing is measured with ticks running. The mining and +conservation expectations were not reduced to make those runs pass. + +## Manual And Modpack Checks + +Use [the combined Eclipse launch](../../runClientAllAdvantage.launch) in the +PA1211 workspace, or the exact jars in the local `manual-test-jars` folder. +Check ordinary play, tooltips, live animations and real player/menu interaction. +The hydroturbine renderer still needs in-world visual inspection; its geometry +probe explicitly reports that gap rather than claiming a pixel pass. + +The automated client uses hashed development classes and resources. Packaged +server tests use the frozen player jars. Neither qualifies third-party resource +packs or an entire modpack. No full Sylvester-world qualification was performed +in this pass. Only disposable fixture copies were loaded. + +Manual acceptance is complete. Dependency publication, hosted CI, tags and +release uploads are checked separately; acceptance does not itself establish +public availability. diff --git a/gradle.properties b/gradle.properties index 6f3528b2..69296fad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,15 +5,15 @@ org.gradle.caching=true org.gradle.parallel=false net.minecraftforge.gradle.merge-source-sets=false -minecraft_version=1.20.6 -minecraft_version_range=[1.20.6] -forge_version=50.2.0 -forge_installer_sha256=8789098B077F5486449311A11DC05E977FEB9430913CB15F30B21A2D8AA213EA -forge_version_range=[50.2.0,51) -loader_version_range=[50,) +minecraft_version=1.21.1 +minecraft_version_range=[1.21.1] +forge_version=52.1.0 +forge_installer_sha256=F1B620F2879AD6A5BBE15DABA4D8F81EAB9F5E08004967604960B98996BDEBC4 +forge_version_range=[52.1.0,53) +loader_version_range=[52,) mapping_channel=official -mapping_version=1.20.6 -mcp_version=20240429.135109 +mapping_version=1.21.1 +mcp_version=20240808.132146 loader_name=forge loader_code=1 @@ -26,22 +26,22 @@ curseforge_project_id=1780140 mod_id=basepower mod_name=basePower mod_license=LGPL-3.0-only -mod_version=2.4.13.120061 -release_player_sha256=E53BDD64776741B2EBAE36A646049DC98487E1BAD1CF69F1E3FCD95188F907F1 +mod_version=2.4.14.121011 +release_player_sha256=F91A613EB5FAA218AAD2DC3C881DEC7B897A5EE874215282A439E34B915C1AE6 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.20.6 +cf_versions=1.21.1 -basemetals_version=3.1.2.120061 -basemetals_source_commit=9e706db5c85fa2bc7f36dee94f39a5f2fa8ab367 +basemetals_version=3.1.2.121011 +basemetals_source_commit=327c197bcdd0bb5e883d844bb6a6c8791bc580b7 basemetals_curse_project_id=240967 -basemetals_deobf_sha256=6186AC62C7E2E333373EA0D8AD8DE53FF859C520EBF568DFB47517C4F77C8C5D +basemetals_deobf_sha256=7C0B961A8D4991C9B69379518AAD700A71C1E4B2076CF5DEF292C58029408A07 -orespawn_version=4.1.0.120061 -orespawn_source_commit=4085315345ff043448b316d75e03f4b8da45402d +orespawn_version=4.1.0.121011 +orespawn_source_commit=62c08169c71f4d4fd8f107289e63fd2a4c7c655c orespawn_curse_project_id=245586 -orespawn_deobf_sha256=FAA7107B9FB84E2C62ED847CAFD880AA3C4238A37799BAD955EA9602A944AA26 +orespawn_deobf_sha256=EB369B7D7CE6D6C453658347A420C2F0783B7F4D1AC7383C87176AC05192B5A0 diff --git a/gradle/forge50-mavenizer.gradle b/gradle/forge52-mavenizer.gradle similarity index 59% rename from gradle/forge50-mavenizer.gradle rename to gradle/forge52-mavenizer.gradle index 8e9c6dd7..46987e3c 100644 --- a/gradle/forge50-mavenizer.gradle +++ b/gradle/forge52-mavenizer.gradle @@ -1,16 +1,16 @@ import java.security.MessageDigest import org.gradle.jvm.toolchain.JvmVendorSpec -// Use the same source preparation tool as OreSpawn on Forge 50. +// Use the same source preparation tool as OreSpawn on Forge 52. def tool = 'ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar' -def defaultJar = project.extensions.extraProperties.has('forge50MavenizerDefault') - ? project.forge50MavenizerDefault - : "../../../MinecraftMineralogy/OS1206/OreSpawn/${tool}" -def fixture = file(providers.gradleProperty('forge50MavenizerJar').getOrElse(defaultJar)) -def expected = '4B14145595E900F178A082F2253A756F63897E6F61C2B8B5E7C819B82CD79E8C' +def defaultJar = project.extensions.extraProperties.has('forge52MavenizerDefault') + ? project.forge52MavenizerDefault + : "../../../MinecraftMineralogy/OS1211/OreSpawn/${tool}" +def fixture = file(providers.gradleProperty('forge52MavenizerJar').getOrElse(defaultJar)) +def expected = '019032A4A26ECB473D403EF017054D5892CE59A9904E8831B452287CD756397E' if (!fixture.isFile() || MessageDigest.getInstance('SHA-256').digest(fixture.bytes) .encodeHex().toString().toUpperCase(Locale.ROOT) != expected) { - throw new GradleException("Missing or altered Forge 50 Mavenizer fixture: ${fixture}") + throw new GradleException("Missing or altered Forge 52 Mavenizer fixture: ${fixture}") } fgtools.configure('mavenizer') { diff --git a/src/main/java/cyano/poweradvantage/api/migration/LegacyMigrationApi.java b/src/main/java/cyano/poweradvantage/api/migration/LegacyMigrationApi.java index a31200b6..e103ac4c 100644 --- a/src/main/java/cyano/poweradvantage/api/migration/LegacyMigrationApi.java +++ b/src/main/java/cyano/poweradvantage/api/migration/LegacyMigrationApi.java @@ -57,7 +57,7 @@ public static synchronized void registerNamespaceAlias(String legacyNamespace, S public static synchronized ResourceLocation registryTarget(ResourceLocation id) { String current = NAMESPACE_ALIASES.get(id.getNamespace()); - return current == null ? id : new ResourceLocation(current, id.getPath()); + return current == null ? id : ResourceLocation.fromNamespaceAndPath(current, id.getPath()); } public static synchronized void registerBlockAlias(ResourceLocation legacyId, ResourceLocation currentId) { diff --git a/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java b/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java index 4c1988fe..1f8d8174 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java +++ b/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java @@ -43,7 +43,7 @@ public class SimpleMachineGUI implements ITileEntityGUI { /** * This is the texture resource for drawing the player's inventory. */ - public final static ResourceLocation playerInventoryBox = new ResourceLocation(PowerAdvantage.MODID+":"+"textures/gui/container/player_inventory.png"); + public final static ResourceLocation playerInventoryBox = ResourceLocation.parse(PowerAdvantage.MODID+":"+"textures/gui/container/player_inventory.png"); /** Texture resource for the machine's GUI */ protected final ResourceLocation guiDisplayImage; /** Dimensions of the assembled screen, including its label rows. */ @@ -92,7 +92,7 @@ public SimpleMachineGUI(ResourceLocation guiImage, Integer2D... inventorySlotCoo * inventory index of the TileEntity's inventory. */ public SimpleMachineGUI(String guiImage, Integer2D... inventorySlotCoordinates){ - this(new ResourceLocation(guiImage),inventorySlotCoordinates); + this(ResourceLocation.parse(guiImage),inventorySlotCoordinates); } diff --git a/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java b/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java index abeb0358..ed47dc11 100644 --- a/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java +++ b/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java @@ -17,7 +17,7 @@ import net.minecraftforge.fml.DistExecutor; import net.minecraftforge.fml.ModList; -import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.config.ModConfig; import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; @@ -42,7 +42,7 @@ public final class PowerAdvantage { public static final String MODID = "basepower"; public static final String NAME = "basePower"; - public static final String VERSION = "2.4.13.120061"; + public static final String VERSION = "2.4.14.121011"; public static final Logger LOGGER = LogManager.getLogger(MODID); private static PowerAdvantage instance; @@ -71,8 +71,9 @@ public final class PowerAdvantage { /** Retained for source compatibility; the labelled converter now uses Forge Energy. */ @Deprecated public static boolean detectedTechReborn = true; - public PowerAdvantage() { + public PowerAdvantage(FMLJavaModLoadingContext context) { instance = this; + IEventBus modBus = context.getModEventBus(); AdvantageLegacyMigrations.registerPowerAdvantage(); zone.moddev.mc.basepower.migration.VanillaRegistryRemaps.register(); @@ -80,23 +81,23 @@ public PowerAdvantage() { MinecraftForge.EVENT_BUS.addListener(ConduitRegistry.getInstance()::onChunkUnload); MinecraftForge.EVENT_BUS.addListener(RecipeDiscovery::onPlayerLogin); MinecraftForge.EVENT_BUS.addListener(zone.moddev.mc.basepower.init.Villages::onTrades); - FMLJavaModLoadingContext.get().getModEventBus().addListener( + modBus.addListener( zone.moddev.mc.basepower.recipe.MaterialTags::registerPack); - FMLJavaModLoadingContext.get().getModEventBus().addListener(MachineGUIRegistry::registerTypes); + modBus.addListener(MachineGUIRegistry::registerTypes); Fluids.init(); - ItemGroups.register(FMLJavaModLoadingContext.get().getModEventBus()); - RecipeConditions.register(FMLJavaModLoadingContext.get().getModEventBus()); - zone.moddev.mc.basepower.recipe.PartsLoot.register(FMLJavaModLoadingContext.get().getModEventBus()); + ItemGroups.register(modBus); + RecipeConditions.register(modBus); + zone.moddev.mc.basepower.recipe.PartsLoot.register(modBus); migrateLegacyOreSpawnConfiguration(); migrateLegacyConfiguration(); - ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, PowerAdvantageConfig.SPEC); - FMLJavaModLoadingContext.get().getModEventBus().addListener(PowerAdvantageConfig::onConfigLoading); - FMLJavaModLoadingContext.get().getModEventBus().addListener(this::commonSetup); + context.registerConfig(ModConfig.Type.COMMON, PowerAdvantageConfig.SPEC); + modBus.addListener(PowerAdvantageConfig::onConfigLoading); + modBus.addListener(this::commonSetup); DistExecutor.runWhenOn(Dist.CLIENT, () -> () -> { - FMLJavaModLoadingContext.get().getModEventBus().addListener( + modBus.addListener( (net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent event) -> event.enqueueWork(() -> { MachineGUIRegistry.registerScreens(); diff --git a/src/main/java/zone/moddev/mc/basepower/client/ClientOilRenderer.java b/src/main/java/zone/moddev/mc/basepower/client/ClientOilRenderer.java index 54e78c89..8726aa33 100644 --- a/src/main/java/zone/moddev/mc/basepower/client/ClientOilRenderer.java +++ b/src/main/java/zone/moddev/mc/basepower/client/ClientOilRenderer.java @@ -11,8 +11,6 @@ public final class ClientOilRenderer { private ClientOilRenderer() {} 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, @@ -20,10 +18,6 @@ public static void configureRenderLayers() { 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()); } } diff --git a/src/main/java/zone/moddev/mc/basepower/compat/BaseMetalsCompat.java b/src/main/java/zone/moddev/mc/basepower/compat/BaseMetalsCompat.java index db9bf2e3..07cfa88c 100644 --- a/src/main/java/zone/moddev/mc/basepower/compat/BaseMetalsCompat.java +++ b/src/main/java/zone/moddev/mc/basepower/compat/BaseMetalsCompat.java @@ -30,7 +30,7 @@ public static void addCrusherRecipe(Block input, ItemStack output) { } public static ItemStack getSteelPlate() { - Item item = ForgeRegistries.ITEMS.getValue(new ResourceLocation("basemetals", "steel_plate")); + Item item = ForgeRegistries.ITEMS.getValue(ResourceLocation.fromNamespaceAndPath("basemetals", "steel_plate")); return item == null ? null : new ItemStack(item); } diff --git a/src/main/java/zone/moddev/mc/basepower/gui/FluidTankGUI.java b/src/main/java/zone/moddev/mc/basepower/gui/FluidTankGUI.java index a963b39f..76ce8027 100644 --- a/src/main/java/zone/moddev/mc/basepower/gui/FluidTankGUI.java +++ b/src/main/java/zone/moddev/mc/basepower/gui/FluidTankGUI.java @@ -6,6 +6,7 @@ import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vertex.Tesselator; import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferUploader; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.renderer.texture.TextureAtlas; import com.mojang.blaze3d.vertex.DefaultVertexFormat; @@ -83,9 +84,9 @@ public static void drawFluidFilledRectangle( var pose = guiContainer.getMatrices().last().pose(); RenderSystem.setShaderTexture(0, sprite.atlasLocation()); Tesselator tessellator = Tesselator.getInstance(); - BufferBuilder vertexbuffer = tessellator.getBuilder(); RenderSystem.setShader(net.minecraft.client.renderer.GameRenderer::getPositionTexColorShader); - vertexbuffer.begin(com.mojang.blaze3d.vertex.VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR); + BufferBuilder vertexbuffer = tessellator.begin(com.mojang.blaze3d.vertex.VertexFormat.Mode.QUADS, + DefaultVertexFormat.POSITION_TEX_COLOR); final int tileSize = 16; for (int dy = 0; dy < height; dy += tileSize) { int tileHeight = Math.min(tileSize, height - dy); @@ -94,13 +95,13 @@ public static void drawFluidFilledRectangle( // Sprite coordinates are fractions of the tile, even when drawing only part of it. float u0 = sprite.getU0(), u1 = sprite.getU((float) tileWidth / tileSize); float v0 = sprite.getV0(), v1 = sprite.getV((float) tileHeight / tileSize); - vertexbuffer.vertex(pose, x + dx, y + dy + tileHeight, zLevel).uv(u0, v1).color(red, green, blue, 1F).endVertex(); - vertexbuffer.vertex(pose, x + dx + tileWidth, y + dy + tileHeight, zLevel).uv(u1, v1).color(red, green, blue, 1F).endVertex(); - vertexbuffer.vertex(pose, x + dx + tileWidth, y + dy, zLevel).uv(u1, v0).color(red, green, blue, 1F).endVertex(); - vertexbuffer.vertex(pose, x + dx, y + dy, zLevel).uv(u0, v0).color(red, green, blue, 1F).endVertex(); + vertexbuffer.addVertex(pose, x + dx, y + dy + tileHeight, zLevel).setUv(u0, v1).setColor(red, green, blue, 1F); + vertexbuffer.addVertex(pose, x + dx + tileWidth, y + dy + tileHeight, zLevel).setUv(u1, v1).setColor(red, green, blue, 1F); + vertexbuffer.addVertex(pose, x + dx + tileWidth, y + dy, zLevel).setUv(u1, v0).setColor(red, green, blue, 1F); + vertexbuffer.addVertex(pose, x + dx, y + dy, zLevel).setUv(u0, v0).setColor(red, green, blue, 1F); } } - tessellator.end(); + BufferUploader.drawWithShader(vertexbuffer.buildOrThrow()); } } diff --git a/src/main/java/zone/moddev/mc/basepower/init/Blocks.java b/src/main/java/zone/moddev/mc/basepower/init/Blocks.java index 6eac06c8..638ac2ea 100644 --- a/src/main/java/zone/moddev/mc/basepower/init/Blocks.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Blocks.java @@ -138,7 +138,7 @@ public static void registerBlocks(RegisterEvent event) { if (!ForgeRegistries.Keys.BLOCKS.equals(event.getRegistryKey())) return; init(); event.register(ForgeRegistries.Keys.BLOCKS, helper -> ALL_BLOCKS.forEach((id, block) -> - helper.register(new ResourceLocation(PowerAdvantage.MODID, id), block))); + helper.register(ResourceLocation.fromNamespaceAndPath(PowerAdvantage.MODID, id), block))); } @SubscribeEvent @@ -150,7 +150,7 @@ public static void registerBlockItems(RegisterEvent event) { if (!"fluid_pipe_terminal".equals(id) && !(block instanceof LiquidBlock)) { ItemGroups.show(item); } - helper.register(new ResourceLocation(PowerAdvantage.MODID, id), item); + helper.register(ResourceLocation.fromNamespaceAndPath(PowerAdvantage.MODID, id), item); })); } diff --git a/src/main/java/zone/moddev/mc/basepower/init/Entities.java b/src/main/java/zone/moddev/mc/basepower/init/Entities.java index 6292e346..b284e167 100644 --- a/src/main/java/zone/moddev/mc/basepower/init/Entities.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Entities.java @@ -85,7 +85,7 @@ private static BlockEntityType register( RegisterEvent event, String id, BlockEntityType.BlockEntitySupplier factory, net.minecraft.world.level.block.Block... validBlocks) { BlockEntityType type = BlockEntityType.Builder.of(factory, validBlocks).build(null); - event.register(ForgeRegistries.Keys.BLOCK_ENTITY_TYPES, new ResourceLocation(PowerAdvantage.MODID, id), () -> type); + event.register(ForgeRegistries.Keys.BLOCK_ENTITY_TYPES, ResourceLocation.fromNamespaceAndPath(PowerAdvantage.MODID, id), () -> type); return type; } } diff --git a/src/main/java/zone/moddev/mc/basepower/init/Fluids.java b/src/main/java/zone/moddev/mc/basepower/init/Fluids.java index af1e364d..9a063ca1 100644 --- a/src/main/java/zone/moddev/mc/basepower/init/Fluids.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Fluids.java @@ -85,7 +85,7 @@ static synchronized void prepareOilFluids() { } private static ResourceLocation id(String name) { - return new ResourceLocation(PowerAdvantage.MODID, name); + return ResourceLocation.fromNamespaceAndPath(PowerAdvantage.MODID, name); } private static FluidType oilType(String name, int density, int viscosity) { @@ -125,11 +125,11 @@ public static Fluid conduitTypeToFluid(ConduitType type) { public static Fluid byName(String name) { if (name == null || name.trim().isEmpty()) return null; - ResourceLocation id = name.indexOf(':') >= 0 ? new ResourceLocation(name) - : new ResourceLocation(PowerAdvantage.MODID, name); + ResourceLocation id = name.indexOf(':') >= 0 ? ResourceLocation.parse(name) + : ResourceLocation.fromNamespaceAndPath(PowerAdvantage.MODID, name); Fluid fluid = registeredFluid(id); if (fluid == null && name.indexOf(':') < 0) { - fluid = registeredFluid(new ResourceLocation("minecraft", name)); + fluid = registeredFluid(ResourceLocation.fromNamespaceAndPath("minecraft", name)); } return fluid; } diff --git a/src/main/java/zone/moddev/mc/basepower/init/GUI.java b/src/main/java/zone/moddev/mc/basepower/init/GUI.java index 35abd6b0..92498b9d 100644 --- a/src/main/java/zone/moddev/mc/basepower/init/GUI.java +++ b/src/main/java/zone/moddev/mc/basepower/init/GUI.java @@ -17,15 +17,15 @@ public static void init() { Blocks.init(); - Blocks.fluid_drain.setGuiID(MachineGUIRegistry.addGUI(new FluidTankGUI(new ResourceLocation(PowerAdvantage.MODID + ":" + "textures/gui/container/fluid_drain_gui.png"))), PowerAdvantage.getInstance()); + Blocks.fluid_drain.setGuiID(MachineGUIRegistry.addGUI(new FluidTankGUI(ResourceLocation.parse(PowerAdvantage.MODID + ":" + "textures/gui/container/fluid_drain_gui.png"))), PowerAdvantage.getInstance()); - Blocks.fluid_discharge.setGuiID(MachineGUIRegistry.addGUI(new FluidTankGUI(new ResourceLocation(PowerAdvantage.MODID + ":" + "textures/gui/container/fluid_discharge_gui.png"))), PowerAdvantage.getInstance()); + Blocks.fluid_discharge.setGuiID(MachineGUIRegistry.addGUI(new FluidTankGUI(ResourceLocation.parse(PowerAdvantage.MODID + ":" + "textures/gui/container/fluid_discharge_gui.png"))), PowerAdvantage.getInstance()); - Blocks.storage_tank.setGuiID(MachineGUIRegistry.addGUI(new FluidTankGUI(new ResourceLocation(PowerAdvantage.MODID + ":" + "textures/gui/container/fluid_storage_tank_gui.png"))), PowerAdvantage.getInstance()); + Blocks.storage_tank.setGuiID(MachineGUIRegistry.addGUI(new FluidTankGUI(ResourceLocation.parse(PowerAdvantage.MODID + ":" + "textures/gui/container/fluid_storage_tank_gui.png"))), PowerAdvantage.getInstance()); - Blocks.metal_storage_tank.setGuiID(MachineGUIRegistry.addGUI(new FilteredFluidTankGUI(new ResourceLocation(PowerAdvantage.MODID + ":" + "textures/gui/container/fluid_metal_tank_gui.png"))), PowerAdvantage.getInstance()); + Blocks.metal_storage_tank.setGuiID(MachineGUIRegistry.addGUI(new FilteredFluidTankGUI(ResourceLocation.parse(PowerAdvantage.MODID + ":" + "textures/gui/container/fluid_metal_tank_gui.png"))), PowerAdvantage.getInstance()); - Blocks.still.setGuiID(MachineGUIRegistry.addGUI(new StillGUI(new ResourceLocation(PowerAdvantage.MODID + ":" + "textures/gui/container/still_gui.png"))), PowerAdvantage.getInstance()); + Blocks.still.setGuiID(MachineGUIRegistry.addGUI(new StillGUI(ResourceLocation.parse(PowerAdvantage.MODID + ":" + "textures/gui/container/still_gui.png"))), PowerAdvantage.getInstance()); Blocks.item_conveyor.setGuiID(MachineGUIRegistry.addGUI(new SimpleMachineGUI( PowerAdvantage.MODID + ":textures/gui/container/item_conveyor.png", @@ -57,4 +57,4 @@ public static void init() { initDone = true; } -} \ No newline at end of file +} diff --git a/src/main/java/zone/moddev/mc/basepower/init/Items.java b/src/main/java/zone/moddev/mc/basepower/init/Items.java index eb4348f0..4ae8a609 100644 --- a/src/main/java/zone/moddev/mc/basepower/init/Items.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Items.java @@ -58,7 +58,7 @@ public static void register(RegisterEvent event) { if (!ForgeRegistries.Keys.ITEMS.equals(event.getRegistryKey())) return; init(); event.register(ForgeRegistries.Keys.ITEMS, helper -> allItems.forEach((id, item) -> - helper.register(new ResourceLocation(PowerAdvantage.MODID, id), ItemGroups.show(item)))); + helper.register(ResourceLocation.fromNamespaceAndPath(PowerAdvantage.MODID, id), ItemGroups.show(item)))); } public static Item getItemByName(String id) { return allItems.get(id); } 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 index 188187d7..b6302870 100644 --- a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntitySmeltableFilter.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntitySmeltableFilter.java @@ -1,6 +1,6 @@ package zone.moddev.mc.basepower.machines.conveyors; -import net.minecraft.world.SimpleContainer; +import net.minecraft.world.item.crafting.SingleRecipeInput; import net.minecraft.world.item.ItemStack; import net.minecraft.network.chat.Component; import net.minecraft.world.item.crafting.RecipeType; @@ -12,8 +12,7 @@ public class TileEntitySmeltableFilter extends TileEntityConveyorFilter { @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()); + SingleRecipeInput probe = new SingleRecipeInput(item.copy()); return level.getRecipeManager().getRecipeFor(RecipeType.SMELTING, probe, level).isPresent(); } diff --git a/src/main/java/zone/moddev/mc/basepower/math/BlockPos4D.java b/src/main/java/zone/moddev/mc/basepower/math/BlockPos4D.java index 03709a29..7fd6da95 100644 --- a/src/main/java/zone/moddev/mc/basepower/math/BlockPos4D.java +++ b/src/main/java/zone/moddev/mc/basepower/math/BlockPos4D.java @@ -293,7 +293,7 @@ public static BlockPos4D fromNBT(Tag tag) { int[] pos = data.getIntArray("Pos"); if (pos.length != 3) throw new IllegalArgumentException("Invalid position"); return new BlockPos4D(ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, - new ResourceLocation(data.getString("Dimension"))), pos[0], pos[1], pos[2]); + ResourceLocation.parse(data.getString("Dimension"))), pos[0], pos[1], pos[2]); } /** Only the three vanilla numeric IDs have an unambiguous legacy meaning. */ diff --git a/src/main/java/zone/moddev/mc/basepower/migration/AdvantageLegacyMigrations.java b/src/main/java/zone/moddev/mc/basepower/migration/AdvantageLegacyMigrations.java index 6185a607..53aaf15a 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/AdvantageLegacyMigrations.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/AdvantageLegacyMigrations.java @@ -88,6 +88,6 @@ private static BlockState decodeState(Block block, int metadata) { } private static ResourceLocation id(String path) { - return new ResourceLocation(PowerAdvantage.MODID, path); + return ResourceLocation.fromNamespaceAndPath(PowerAdvantage.MODID, path); } } diff --git a/src/main/java/zone/moddev/mc/basepower/migration/BaseMetalsLegacyMigration.java b/src/main/java/zone/moddev/mc/basepower/migration/BaseMetalsLegacyMigration.java index 8f949a47..933fa8d2 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/BaseMetalsLegacyMigration.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/BaseMetalsLegacyMigration.java @@ -12,7 +12,7 @@ public final class BaseMetalsLegacyMigration implements Legacy112MigrationExtens private BaseMetalsLegacyMigration() {} public static void register() { - Legacy112MigrationExtension.register(new ResourceLocation("basepower", "legacy_world"), + Legacy112MigrationExtension.register(ResourceLocation.fromNamespaceAndPath("basepower", "legacy_world"), new BaseMetalsLegacyMigration()); } diff --git a/src/main/java/zone/moddev/mc/basepower/migration/LegacyEntityDataFixes.java b/src/main/java/zone/moddev/mc/basepower/migration/LegacyEntityDataFixes.java index d879a840..8e3ddfc0 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/LegacyEntityDataFixes.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyEntityDataFixes.java @@ -8,8 +8,10 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; 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.util.datafix.fixes.EntityUUIDFix; @@ -67,6 +69,7 @@ private static CompoundTag upgrade(CompoundTag source, int version, int current, } else { // All entities inherit this root UUID format, including unknown mod types. entity = (CompoundTag) EntityUUIDFix.updateEntityUUID(dynamic).getValue(); + upgradeAttributePayload(entity, version, current, fixer); } // Vanilla's UUID fixer rejects zero halves, although they are valid UUID bits. if (source.hasUUID("UUID")) { @@ -89,6 +92,21 @@ private static CompoundTag upgrade(CompoundTag source, int version, int current, return entity; } + private static void upgradeAttributePayload(CompoundTag entity, int version, int current, DataFixer fixer) { + if (!entity.contains("Attributes")) return; + if (entity.contains("attributes")) throw new IllegalStateException("Ambiguous old and native entity attributes"); + CompoundTag input = new CompoundTag(); + input.put("Attributes", entity.get("Attributes").copy()); + // Attribute records have the same native schema on players and living entities. + // Isolate this vanilla-owned field rather than submitting an unknown mod entity choice. + CompoundTag fixed = DataFixTypes.PLAYER.update(fixer, input, version, current); + if (!fixed.contains("attributes", Tag.TAG_LIST)) { + throw new IllegalStateException("Native attribute data fix did not produce the current format"); + } + entity.remove("Attributes"); + entity.put("attributes", fixed.get("attributes")); + } + static void preparePlayerAttributes(CompoundTag player) { renameForgeAttributes(player); if (!player.contains("RootVehicle")) return; diff --git a/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldDataHook.java b/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldDataHook.java index 2b655f8a..ea899a1b 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldDataHook.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldDataHook.java @@ -9,6 +9,7 @@ import java.lang.reflect.Modifier; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Arrays; @@ -43,7 +44,7 @@ * worlds. Add-ons contribute their identities through {@link LegacyMigrationApi}. */ public final class LegacyWorldDataHook { - private static final ResourceLocation BLOCK_REGISTRY = new ResourceLocation("minecraft", "blocks"); + private static final ResourceLocation BLOCK_REGISTRY = ResourceLocation.fromNamespaceAndPath("minecraft", "blocks"); private static final Set PREPARED_WORLDS = ConcurrentHashMap.newKeySet(); private static final String REGISTRY_SIDECAR = "basepower-legacy-registry.dat"; private static final BitSet LEGACY_BLOCK_IDS = new BitSet(); @@ -108,10 +109,51 @@ static void prepareForgeRegistryMetadata(File levelDat) { } boolean changed = removeRetiredVanillaProfessionRegistry(root); changed |= renameFlattenedBiomeRegistry(root); - if (changed) writeWithBackup(levelDat, root); + changed |= archiveDataDrivenEnchantmentIndex(root); + if (changed) { + preserveRetiredRegistryArchive(levelDat, root.getCompound("basepower:legacy_registry_indexes")); + writeWithBackup(levelDat, root); + } } catch (IOException exception) { - throw new IllegalStateException("Could not prepare the old villager registry in " + levelDat, exception); + throw new IllegalStateException("Could not prepare historical Forge registry indexes in " + levelDat, exception); + } + } + + static boolean archiveDataDrivenEnchantmentIndex(CompoundTag root) { + // Flattened items use named enchantments. Forge 52 no longer has this + // static registry; keep its exact index for recovery, not in GameData. + // Uppercase pre-flattening FML remains untouched for numeric-ID migration. + CompoundTag registries = root.getCompound("fml").getCompound("Registries"); + String key = "minecraft:enchantment"; + if (!registries.contains(key, Tag.TAG_COMPOUND)) return false; + CompoundTag snapshot = registries.getCompound(key); + CompoundTag archive = root.getCompound("basepower:legacy_registry_indexes"); + if (archive.contains(key) && !snapshot.equals(archive.get(key))) { + throw new IllegalStateException("Conflicting historical enchantment registry indexes; keep the world backup"); + } + archive.put(key, snapshot.copy()); + root.put("basepower:legacy_registry_indexes", archive); + registries.remove(key); + return true; + } + + private static void preserveRetiredRegistryArchive(File levelDat, CompoundTag archive) throws IOException { + if (archive.isEmpty()) return; + Path target = levelDat.toPath().resolveSibling("basepower-retired-registry-indexes.dat"); + if (Files.exists(target)) { + if (!archive.equals(NbtIo.readCompressed(target, NbtAccounter.unlimitedHeap()))) { + throw new IOException("Conflicting retained registry indexes in " + target); + } + return; + } + Path temporary = Files.createTempFile(target.getParent(), "basepower-registry-indexes-", ".tmp"); + NbtIo.writeCompressed(archive, temporary); + try { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE); + } catch (java.nio.file.AtomicMoveNotSupportedException unsupported) { + Files.move(temporary, target); } + PowerAdvantage.LOGGER.info("Retained historical Forge registry indexes in {}", target); } static boolean renameFlattenedBiomeRegistry(CompoundTag root) { @@ -525,9 +567,9 @@ public static Map legacyPreparedBlockCounts() { static int indexLegacyChunks(File worldDirectory) { LEGACY_CHUNKS.clear(); - return indexLegacyChunks(new ResourceLocation("overworld"), new File(worldDirectory, "region")) - + indexLegacyChunks(new ResourceLocation("the_nether"), new File(worldDirectory, "DIM-1/region")) - + indexLegacyChunks(new ResourceLocation("the_end"), new File(worldDirectory, "DIM1/region")); + return indexLegacyChunks(ResourceLocation.parse("overworld"), new File(worldDirectory, "region")) + + indexLegacyChunks(ResourceLocation.parse("the_nether"), new File(worldDirectory, "DIM-1/region")) + + indexLegacyChunks(ResourceLocation.parse("the_end"), new File(worldDirectory, "DIM1/region")); } private static int indexLegacyChunks(ResourceLocation dimension, File regionDirectory) { @@ -621,7 +663,7 @@ private static String normalizeIdentity(String value) { } private static ResourceLocation safeId(String value) { - try { return value == null || value.isEmpty() ? null : new ResourceLocation(value.toLowerCase(Locale.ROOT)); } + try { return value == null || value.isEmpty() ? null : ResourceLocation.parse(value.toLowerCase(Locale.ROOT)); } catch (RuntimeException ignored) { return null; } } diff --git a/src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java b/src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java index bc9ebe12..4407dd7b 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java @@ -1,6 +1,7 @@ package zone.moddev.mc.basepower.migration; import java.util.function.UnaryOperator; +import java.util.Set; import cyano.poweradvantage.api.migration.LegacyMigrationApi; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.ai.memory.MemoryModuleType; @@ -8,7 +9,6 @@ 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; @@ -24,7 +24,11 @@ /** Vanilla's data fixers rename saved data, but not Forge's registry snapshot. */ public final class VanillaRegistryRemaps { - private static final ResourceLocation BIOME_REGISTRY = new ResourceLocation("minecraft:worldgen/biome"); + private static final ResourceLocation BIOME_REGISTRY = ResourceLocation.parse("minecraft:worldgen/biome"); + private static final Set LEGACY_PAINTINGS = Set.of("kebab", "aztec", "alban", "aztec2", "bomb", + "plant", "wasteland", "pool", "courbet", "sea", "sunset", "creebet", "wanderer", "graham", + "match", "bust", "stage", "void", "skull_and_roses", "wither", "fighters", "pointer", "pigscene", + "burning_skull", "skeleton", "donkey_kong", "earth", "wind", "water", "fire"); private VanillaRegistryRemaps() {} @@ -38,10 +42,10 @@ public static void register() { MinecraftForge.EVENT_BUS.addListener(EventPriority.HIGHEST, VanillaRegistryRemaps::pointsOfInterest); MinecraftForge.EVENT_BUS.addListener(EventPriority.HIGHEST, VanillaRegistryRemaps::blockEntities); MinecraftForge.EVENT_BUS.addListener(EventPriority.HIGHEST, VanillaRegistryRemaps::fluids); - MinecraftForge.EVENT_BUS.addListener(EventPriority.HIGHEST, VanillaRegistryRemaps::enchantments); MinecraftForge.EVENT_BUS.addListener(EventPriority.HIGHEST, VanillaRegistryRemaps::potions); MinecraftForge.EVENT_BUS.addListener(EventPriority.HIGHEST, VanillaRegistryRemaps::menus); MinecraftForge.EVENT_BUS.addListener(EventPriority.HIGHEST, VanillaRegistryRemaps::recipeSerializers); + MinecraftForge.EVENT_BUS.addListener(EventPriority.HIGHEST, VanillaRegistryRemaps::paintings); } private static void blocks(MissingMappingsEvent event) { @@ -55,24 +59,21 @@ private static void items(MissingMappingsEvent event) { private static void entities(MissingMappingsEvent event) { remap(event, ForgeRegistries.ENTITY_TYPES); remapAdvantage(event, ForgeRegistries.ENTITY_TYPES, - id -> new ResourceLocation(LegacyMigrationApi.entityTarget(id.toString()))); + id -> ResourceLocation.parse(LegacyMigrationApi.entityTarget(id.toString()))); } private static void sounds(MissingMappingsEvent event) { remap(event, ForgeRegistries.SOUND_EVENTS); } + private static void paintings(MissingMappingsEvent event) { remap(event, ForgeRegistries.PAINTING_VARIANTS); } private static void biomes(MissingMappingsEvent event) { remap(event, ForgeRegistries.BIOMES); } private static void memories(MissingMappingsEvent event) { remap(event, ForgeRegistries.MEMORY_MODULE_TYPES); } private static void pointsOfInterest(MissingMappingsEvent event) { remap(event, ForgeRegistries.POI_TYPES); } private static void blockEntities(MissingMappingsEvent event) { remapAdvantage(event, ForgeRegistries.BLOCK_ENTITY_TYPES, - id -> new ResourceLocation(LegacyMigrationApi.tileTarget(id.toString()))); + id -> ResourceLocation.parse(LegacyMigrationApi.tileTarget(id.toString()))); } private static void fluids(MissingMappingsEvent event) { remapAdvantage(event, ForgeRegistries.FLUIDS, - id -> new ResourceLocation(LegacyMigrationApi.fluidTarget(id.toString()))); - } - private static void enchantments(MissingMappingsEvent event) { - remap(event, ForgeRegistries.ENCHANTMENTS); - remapAdvantage(event, ForgeRegistries.ENCHANTMENTS, LegacyMigrationApi::registryTarget); + id -> ResourceLocation.parse(LegacyMigrationApi.fluidTarget(id.toString()))); } private static void potions(MissingMappingsEvent event) { remap(event, ForgeRegistries.POTIONS); } private static void menus(MissingMappingsEvent event) { @@ -95,6 +96,12 @@ private static void remapAdvantage(MissingMappingsEvent event, static void remap(MissingMappingsEvent event, IForgeRegistry active) { for (MissingMappingsEvent.Mapping mapping : event.getMappings(active.getRegistryKey(), "minecraft")) { + if (retiredPaintingIndex(active.getRegistryName(), mapping.getKey())) { + // These names still exist in the native data-pack registry. Only + // Forge's old numeric index is retired; entity variant NBT is unchanged. + mapping.ignore(); + continue; + } if (active.getRegistryName().equals(ResourceLocation.parse("minecraft:potion")) && mapping.getKey().equals(ResourceLocation.parse("minecraft:empty"))) { // An absent potion is now represented by empty potion contents, not a registry entry. @@ -136,14 +143,19 @@ static void remap(MissingMappingsEvent event, IForgeRegistry active) { } } + static boolean retiredPaintingIndex(ResourceLocation registry, ResourceLocation old) { + return registry.equals(ResourceLocation.parse("minecraft:painting_variant")) + && old.getNamespace().equals("minecraft") && LEGACY_PAINTINGS.contains(old.getPath()); + } + static boolean retiredTransientMemory(ResourceLocation registry, ResourceLocation old) { - return registry.equals(new ResourceLocation("minecraft:memory_module_type")) + return registry.equals(ResourceLocation.parse("minecraft:memory_module_type")) && old.getNamespace().equals("minecraft") && (old.getPath().equals("golem_last_seen_time") || old.getPath().equals("opened_doors")); } static boolean retiredVanillaPoi(ResourceLocation registry, ResourceLocation old) { - return registry.equals(new ResourceLocation("minecraft:point_of_interest_type")) + return registry.equals(ResourceLocation.parse("minecraft:point_of_interest_type")) && old.getNamespace().equals("minecraft") && (old.getPath().equals("nitwit") || old.getPath().equals("unemployed")); } @@ -153,13 +165,13 @@ static ResourceLocation vanillaBiomeFixTarget(ResourceLocation registry, Resourc String name = old.toString(); String updated = name.equals("minecraft:deep_warm_ocean") ? "minecraft:warm_ocean" : CavesAndCliffsRenames.RENAMES.get(name); - return updated == null ? null : new ResourceLocation(updated); + return updated == null ? null : ResourceLocation.parse(updated); } static ResourceLocation replacement(ResourceLocation registry, ResourceLocation old) { if (!"minecraft".equals(old.getNamespace())) return null; String target = null; - if ((registry.equals(new ResourceLocation("minecraft:block")) || registry.equals(new ResourceLocation("minecraft:item"))) + if ((registry.equals(ResourceLocation.parse("minecraft:block")) || registry.equals(ResourceLocation.parse("minecraft:item"))) && old.getPath().equals("grass_path")) { target = "dirt_path"; } else if ((registry.equals(ResourceLocation.parse("minecraft:block")) || registry.equals(ResourceLocation.parse("minecraft:item"))) @@ -167,11 +179,11 @@ static ResourceLocation replacement(ResourceLocation registry, ResourceLocation target = "short_grass"; } else if (registry.equals(ResourceLocation.parse("minecraft:enchantment")) && old.getPath().equals("sweeping")) { target = "sweeping_edge"; - } else if (registry.equals(new ResourceLocation("minecraft:entity_type")) && old.getPath().equals("zombie_pigman")) { + } else if (registry.equals(ResourceLocation.parse("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")) { + } else if (registry.equals(ResourceLocation.parse("minecraft:item")) && old.getPath().equals("zombie_pigman_spawn_egg")) { target = "zombified_piglin_spawn_egg"; - } else if (registry.equals(new ResourceLocation("minecraft:item"))) { + } else if (registry.equals(ResourceLocation.parse("minecraft:item"))) { // Forge keeps the 1.19.4 experimental IDs even when that data pack was not enabled. target = switch (old.getPath()) { case "pottery_shard_archer" -> "archer_pottery_sherd"; @@ -181,9 +193,11 @@ static ResourceLocation replacement(ResourceLocation registry, ResourceLocation case "scute" -> "turtle_scute"; default -> null; }; - } else if (registry.equals(new ResourceLocation("minecraft:worldgen/biome")) && old.getPath().equals("nether")) { + } else if (registry.equals(ResourceLocation.parse("minecraft:worldgen/biome")) && old.getPath().equals("nether")) { target = "nether_wastes"; - } else if (registry.equals(new ResourceLocation("minecraft:sound_event"))) { + } else if (registry.equals(ResourceLocation.parse("minecraft:sound_event"))) { + if (old.getPath().equals("block.trial_spawner.ambient_charged")) target = "block.trial_spawner.ambient_ominous"; + if (old.getPath().equals("block.trial_spawner.charge_activate")) target = "block.trial_spawner.ominous_activate"; for (String sound : new String[] {"ambient", "angry", "death", "hurt"}) { if (old.getPath().equals("entity.zombie_pigman." + sound)) target = "entity.zombified_piglin." + sound; } @@ -195,6 +209,6 @@ static ResourceLocation replacement(ResourceLocation registry, ResourceLocation // Native biomes choose their own new music; this alias is for the old shared registry ID. if (old.getPath().equals("music.overworld.jungle_and_forest")) target = "music.overworld.forest"; } - return target == null ? null : new ResourceLocation("minecraft", target); + return target == null ? null : ResourceLocation.fromNamespaceAndPath("minecraft", target); } } diff --git a/src/main/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPack.java b/src/main/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPack.java index c0bddba6..cd291e48 100644 --- a/src/main/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPack.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPack.java @@ -58,7 +58,7 @@ 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()); + resources.put(ResourceLocation.fromNamespaceAndPath("forge", "tags/item/" + path + ".json"), tag.toString()); } @Override public IoSupplier getRootResource(String... names) { return null; } @@ -82,7 +82,7 @@ private void put(String path, JsonArray values) { @Override @SuppressWarnings("unchecked") public T getMetadataSection(MetadataSectionSerializer serializer) { return serializer == PackMetadataSection.TYPE - ? (T)new PackMetadataSection(Component.literal(NAME), 41, Optional.empty()) : null; + ? (T)new PackMetadataSection(Component.literal(NAME), 48, Optional.empty()) : null; } @Override public PackLocationInfo location() { return LOCATION; } @Override public void close() {} diff --git a/src/main/java/zone/moddev/mc/basepower/recipe/PartsLoot.java b/src/main/java/zone/moddev/mc/basepower/recipe/PartsLoot.java index c9e2ad61..39c6e7f4 100644 --- a/src/main/java/zone/moddev/mc/basepower/recipe/PartsLoot.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/PartsLoot.java @@ -44,9 +44,9 @@ public final class PartsLoot extends LootModifier { protected ObjectArrayList doApply(ObjectArrayList loot, LootContext context) { ResourceLocation table = context.getQueriedLootTableId(); if (!isChest(table) || PowerAdvantage.chestLootFactor <= 0 || hasAdditionalLootTables()) return loot; - // Forge 50 fires LootTableLoadEvent for all packs and doesn't include the source. + // Keep data-pack replacements authoritative instead of adding parts to their loot. var resourceManager = context.getLevel().getServer().getResourceManager(); - String source = resourceManager.getResource(table.withPath(path -> "loot_tables/" + path + ".json")) + String source = resourceManager.getResource(table.withPath(path -> "loot_table/" + path + ".json")) .map(Resource::sourcePackId).orElse(""); for (ResourceLocation extra : partTables(table, source, ModList.get()::isLoaded)) { var key = ResourceKey.create(Registries.LOOT_TABLE, extra); diff --git a/src/main/java/zone/moddev/mc/basepower/recipe/RecipeDiscovery.java b/src/main/java/zone/moddev/mc/basepower/recipe/RecipeDiscovery.java index 45bb6ee6..c6f026bf 100644 --- a/src/main/java/zone/moddev/mc/basepower/recipe/RecipeDiscovery.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/RecipeDiscovery.java @@ -25,7 +25,7 @@ public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { if (!NAMESPACES.contains(recipe.id().getNamespace()) || recipe.value().isSpecial() || player .getRecipeBook().contains(recipe)) continue; AdvancementHolder advancement = player.getServer().getAdvancements().get( - new ResourceLocation(recipe.id().getNamespace(), "recipes/" + recipe.id().getPath())); + ResourceLocation.fromNamespaceAndPath(recipe.id().getNamespace(), "recipes/" + recipe.id().getPath())); if (advancement != null && player .getAdvancements().getOrStartProgress(advancement).isDone()) { player .awardRecipes(Collections.singletonList(recipe)); } diff --git a/src/main/java/zone/moddev/mc/basepower/registry/MachineGUIRegistry.java b/src/main/java/zone/moddev/mc/basepower/registry/MachineGUIRegistry.java index c71e326d..d3418d8c 100644 --- a/src/main/java/zone/moddev/mc/basepower/registry/MachineGUIRegistry.java +++ b/src/main/java/zone/moddev/mc/basepower/registry/MachineGUIRegistry.java @@ -50,7 +50,7 @@ public static int addGUI(ITileEntityGUI gui) { public static void registerTypes(RegisterEvent event) { if (!ForgeRegistries.Keys.MENU_TYPES.equals(event.getRegistryKey())) return; TYPE = IForgeMenuType.create(MachineGUIRegistry::createClientContainer); - event.register(ForgeRegistries.Keys.MENU_TYPES, new ResourceLocation(PowerAdvantage.MODID, "machine"), () -> TYPE); + event.register(ForgeRegistries.Keys.MENU_TYPES, ResourceLocation.fromNamespaceAndPath(PowerAdvantage.MODID, "machine"), () -> TYPE); } private static SimpleMachineGUI.Container createClientContainer( diff --git a/src/main/resources/assets/basepower/models/block/steel_frame.json b/src/main/resources/assets/basepower/models/block/steel_frame.json index a423e34b..2087f5d3 100644 --- a/src/main/resources/assets/basepower/models/block/steel_frame.json +++ b/src/main/resources/assets/basepower/models/block/steel_frame.json @@ -1,5 +1,6 @@ { "parent": "block/block", + "render_type": "minecraft:cutout", "textures": { "face": "basepower:blocks/steel_frame", "particle": "basepower:materials/steel" diff --git a/src/main/resources/data/basepower/advancements/recipes/bioplastic_from_starch.json b/src/main/resources/data/basepower/advancement/recipes/bioplastic_from_starch.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/bioplastic_from_starch.json rename to src/main/resources/data/basepower/advancement/recipes/bioplastic_from_starch.json diff --git a/src/main/resources/data/basepower/advancements/recipes/converter_rf_electricity.json b/src/main/resources/data/basepower/advancement/recipes/converter_rf_electricity.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/converter_rf_electricity.json rename to src/main/resources/data/basepower/advancement/recipes/converter_rf_electricity.json diff --git a/src/main/resources/data/basepower/advancements/recipes/converter_rf_quantum.json b/src/main/resources/data/basepower/advancement/recipes/converter_rf_quantum.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/converter_rf_quantum.json rename to src/main/resources/data/basepower/advancement/recipes/converter_rf_quantum.json diff --git a/src/main/resources/data/basepower/advancements/recipes/converter_rf_steam.json b/src/main/resources/data/basepower/advancement/recipes/converter_rf_steam.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/converter_rf_steam.json rename to src/main/resources/data/basepower/advancement/recipes/converter_rf_steam.json diff --git a/src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_iron.json b/src/main/resources/data/basepower/advancement/recipes/converter_tr_electricity_iron.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_iron.json rename to src/main/resources/data/basepower/advancement/recipes/converter_tr_electricity_iron.json diff --git a/src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_refined_iron.json b/src/main/resources/data/basepower/advancement/recipes/converter_tr_electricity_refined_iron.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_refined_iron.json rename to src/main/resources/data/basepower/advancement/recipes/converter_tr_electricity_refined_iron.json diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_discharge.json b/src/main/resources/data/basepower/advancement/recipes/fluid_discharge.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/fluid_discharge.json rename to src/main/resources/data/basepower/advancement/recipes/fluid_discharge.json diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_drain.json b/src/main/resources/data/basepower/advancement/recipes/fluid_drain.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/fluid_drain.json rename to src/main/resources/data/basepower/advancement/recipes/fluid_drain.json diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_iron_normal.json b/src/main/resources/data/basepower/advancement/recipes/fluid_metal_tank_iron_normal.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_iron_normal.json rename to src/main/resources/data/basepower/advancement/recipes/fluid_metal_tank_iron_normal.json diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_steel.json b/src/main/resources/data/basepower/advancement/recipes/fluid_metal_tank_steel.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_steel.json rename to src/main/resources/data/basepower/advancement/recipes/fluid_metal_tank_steel.json diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_apocalyptic.json b/src/main/resources/data/basepower/advancement/recipes/fluid_pipe_copper_apocalyptic.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_apocalyptic.json rename to src/main/resources/data/basepower/advancement/recipes/fluid_pipe_copper_apocalyptic.json diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_normal.json b/src/main/resources/data/basepower/advancement/recipes/fluid_pipe_copper_normal.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_normal.json rename to src/main/resources/data/basepower/advancement/recipes/fluid_pipe_copper_normal.json diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_apocalyptic.json b/src/main/resources/data/basepower/advancement/recipes/fluid_pipe_iron_apocalyptic.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_apocalyptic.json rename to src/main/resources/data/basepower/advancement/recipes/fluid_pipe_iron_apocalyptic.json diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_normal.json b/src/main/resources/data/basepower/advancement/recipes/fluid_pipe_iron_normal.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_normal.json rename to src/main/resources/data/basepower/advancement/recipes/fluid_pipe_iron_normal.json diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_tech.json b/src/main/resources/data/basepower/advancement/recipes/fluid_pipe_iron_tech.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_tech.json rename to src/main/resources/data/basepower/advancement/recipes/fluid_pipe_iron_tech.json diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_lead_apocalyptic.json b/src/main/resources/data/basepower/advancement/recipes/fluid_pipe_lead_apocalyptic.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/fluid_pipe_lead_apocalyptic.json rename to src/main/resources/data/basepower/advancement/recipes/fluid_pipe_lead_apocalyptic.json diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_storage_tank.json b/src/main/resources/data/basepower/advancement/recipes/fluid_storage_tank.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/fluid_storage_tank.json rename to src/main/resources/data/basepower/advancement/recipes/fluid_storage_tank.json diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_switch.json b/src/main/resources/data/basepower/advancement/recipes/fluid_switch.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/fluid_switch.json rename to src/main/resources/data/basepower/advancement/recipes/fluid_switch.json diff --git a/src/main/resources/data/basepower/advancements/recipes/item_conveyor.json b/src/main/resources/data/basepower/advancement/recipes/item_conveyor.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/item_conveyor.json rename to src/main/resources/data/basepower/advancement/recipes/item_conveyor.json diff --git a/src/main/resources/data/basepower/advancements/recipes/item_filter_block.json b/src/main/resources/data/basepower/advancement/recipes/item_filter_block.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/item_filter_block.json rename to src/main/resources/data/basepower/advancement/recipes/item_filter_block.json diff --git a/src/main/resources/data/basepower/advancements/recipes/item_filter_food.json b/src/main/resources/data/basepower/advancement/recipes/item_filter_food.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/item_filter_food.json rename to src/main/resources/data/basepower/advancement/recipes/item_filter_food.json diff --git a/src/main/resources/data/basepower/advancements/recipes/item_filter_fuel.json b/src/main/resources/data/basepower/advancement/recipes/item_filter_fuel.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/item_filter_fuel.json rename to src/main/resources/data/basepower/advancement/recipes/item_filter_fuel.json diff --git a/src/main/resources/data/basepower/advancements/recipes/item_filter_inventory.json b/src/main/resources/data/basepower/advancement/recipes/item_filter_inventory.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/item_filter_inventory.json rename to src/main/resources/data/basepower/advancement/recipes/item_filter_inventory.json diff --git a/src/main/resources/data/basepower/advancements/recipes/item_filter_ore.json b/src/main/resources/data/basepower/advancement/recipes/item_filter_ore.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/item_filter_ore.json rename to src/main/resources/data/basepower/advancement/recipes/item_filter_ore.json diff --git a/src/main/resources/data/basepower/advancements/recipes/item_filter_overflow.json b/src/main/resources/data/basepower/advancement/recipes/item_filter_overflow.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/item_filter_overflow.json rename to src/main/resources/data/basepower/advancement/recipes/item_filter_overflow.json diff --git a/src/main/resources/data/basepower/advancements/recipes/item_filter_plant.json b/src/main/resources/data/basepower/advancement/recipes/item_filter_plant.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/item_filter_plant.json rename to src/main/resources/data/basepower/advancement/recipes/item_filter_plant.json diff --git a/src/main/resources/data/basepower/advancements/recipes/item_filter_smelt.json b/src/main/resources/data/basepower/advancement/recipes/item_filter_smelt.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/item_filter_smelt.json rename to src/main/resources/data/basepower/advancement/recipes/item_filter_smelt.json diff --git a/src/main/resources/data/basepower/advancements/recipes/rotator_tool.json b/src/main/resources/data/basepower/advancement/recipes/rotator_tool.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/rotator_tool.json rename to src/main/resources/data/basepower/advancement/recipes/rotator_tool.json diff --git a/src/main/resources/data/basepower/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json b/src/main/resources/data/basepower/advancement/recipes/sprocket_bronze_nugget_tech_no_rod.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json rename to src/main/resources/data/basepower/advancement/recipes/sprocket_bronze_nugget_tech_no_rod.json diff --git a/src/main/resources/data/basepower/advancements/recipes/sprocket_rod_normal.json b/src/main/resources/data/basepower/advancement/recipes/sprocket_rod_normal.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/sprocket_rod_normal.json rename to src/main/resources/data/basepower/advancement/recipes/sprocket_rod_normal.json diff --git a/src/main/resources/data/basepower/advancements/recipes/sprocket_rod_tech.json b/src/main/resources/data/basepower/advancement/recipes/sprocket_rod_tech.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/sprocket_rod_tech.json rename to src/main/resources/data/basepower/advancement/recipes/sprocket_rod_tech.json diff --git a/src/main/resources/data/basepower/advancements/recipes/sprocket_stickwood_normal.json b/src/main/resources/data/basepower/advancement/recipes/sprocket_stickwood_normal.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/sprocket_stickwood_normal.json rename to src/main/resources/data/basepower/advancement/recipes/sprocket_stickwood_normal.json diff --git a/src/main/resources/data/basepower/advancements/recipes/starch_from_potato.json b/src/main/resources/data/basepower/advancement/recipes/starch_from_potato.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/starch_from_potato.json rename to src/main/resources/data/basepower/advancement/recipes/starch_from_potato.json diff --git a/src/main/resources/data/basepower/advancements/recipes/steel_frame.json b/src/main/resources/data/basepower/advancement/recipes/steel_frame.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/steel_frame.json rename to src/main/resources/data/basepower/advancement/recipes/steel_frame.json diff --git a/src/main/resources/data/basepower/advancements/recipes/still.json b/src/main/resources/data/basepower/advancement/recipes/still.json similarity index 100% rename from src/main/resources/data/basepower/advancements/recipes/still.json rename to src/main/resources/data/basepower/advancement/recipes/still.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/converter_rf_electricity.json b/src/main/resources/data/basepower/loot_table/blocks/converter_rf_electricity.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/converter_rf_electricity.json rename to src/main/resources/data/basepower/loot_table/blocks/converter_rf_electricity.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/converter_rf_quantum.json b/src/main/resources/data/basepower/loot_table/blocks/converter_rf_quantum.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/converter_rf_quantum.json rename to src/main/resources/data/basepower/loot_table/blocks/converter_rf_quantum.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/converter_rf_steam.json b/src/main/resources/data/basepower/loot_table/blocks/converter_rf_steam.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/converter_rf_steam.json rename to src/main/resources/data/basepower/loot_table/blocks/converter_rf_steam.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/converter_tr_electricity.json b/src/main/resources/data/basepower/loot_table/blocks/converter_tr_electricity.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/converter_tr_electricity.json rename to src/main/resources/data/basepower/loot_table/blocks/converter_tr_electricity.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/crude_oil.json b/src/main/resources/data/basepower/loot_table/blocks/crude_oil.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/crude_oil.json rename to src/main/resources/data/basepower/loot_table/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_table/blocks/fluid_discharge.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/fluid_discharge.json rename to src/main/resources/data/basepower/loot_table/blocks/fluid_discharge.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/fluid_drain.json b/src/main/resources/data/basepower/loot_table/blocks/fluid_drain.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/fluid_drain.json rename to src/main/resources/data/basepower/loot_table/blocks/fluid_drain.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/fluid_metal_tank.json b/src/main/resources/data/basepower/loot_table/blocks/fluid_metal_tank.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/fluid_metal_tank.json rename to src/main/resources/data/basepower/loot_table/blocks/fluid_metal_tank.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/fluid_pipe.json b/src/main/resources/data/basepower/loot_table/blocks/fluid_pipe.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/fluid_pipe.json rename to src/main/resources/data/basepower/loot_table/blocks/fluid_pipe.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/fluid_pipe_terminal.json b/src/main/resources/data/basepower/loot_table/blocks/fluid_pipe_terminal.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/fluid_pipe_terminal.json rename to src/main/resources/data/basepower/loot_table/blocks/fluid_pipe_terminal.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/fluid_storage_tank.json b/src/main/resources/data/basepower/loot_table/blocks/fluid_storage_tank.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/fluid_storage_tank.json rename to src/main/resources/data/basepower/loot_table/blocks/fluid_storage_tank.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/fluid_switch.json b/src/main/resources/data/basepower/loot_table/blocks/fluid_switch.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/fluid_switch.json rename to src/main/resources/data/basepower/loot_table/blocks/fluid_switch.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/infinite_electricity.json b/src/main/resources/data/basepower/loot_table/blocks/infinite_electricity.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/infinite_electricity.json rename to src/main/resources/data/basepower/loot_table/blocks/infinite_electricity.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/infinite_quantum.json b/src/main/resources/data/basepower/loot_table/blocks/infinite_quantum.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/infinite_quantum.json rename to src/main/resources/data/basepower/loot_table/blocks/infinite_quantum.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/infinite_steam.json b/src/main/resources/data/basepower/loot_table/blocks/infinite_steam.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/infinite_steam.json rename to src/main/resources/data/basepower/loot_table/blocks/infinite_steam.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_conveyor.json b/src/main/resources/data/basepower/loot_table/blocks/item_conveyor.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/item_conveyor.json rename to src/main/resources/data/basepower/loot_table/blocks/item_conveyor.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_filter_block.json b/src/main/resources/data/basepower/loot_table/blocks/item_filter_block.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/item_filter_block.json rename to src/main/resources/data/basepower/loot_table/blocks/item_filter_block.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_filter_food.json b/src/main/resources/data/basepower/loot_table/blocks/item_filter_food.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/item_filter_food.json rename to src/main/resources/data/basepower/loot_table/blocks/item_filter_food.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_filter_fuel.json b/src/main/resources/data/basepower/loot_table/blocks/item_filter_fuel.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/item_filter_fuel.json rename to src/main/resources/data/basepower/loot_table/blocks/item_filter_fuel.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_filter_inventory.json b/src/main/resources/data/basepower/loot_table/blocks/item_filter_inventory.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/item_filter_inventory.json rename to src/main/resources/data/basepower/loot_table/blocks/item_filter_inventory.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_filter_ore.json b/src/main/resources/data/basepower/loot_table/blocks/item_filter_ore.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/item_filter_ore.json rename to src/main/resources/data/basepower/loot_table/blocks/item_filter_ore.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_filter_overflow.json b/src/main/resources/data/basepower/loot_table/blocks/item_filter_overflow.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/item_filter_overflow.json rename to src/main/resources/data/basepower/loot_table/blocks/item_filter_overflow.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_filter_plant.json b/src/main/resources/data/basepower/loot_table/blocks/item_filter_plant.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/item_filter_plant.json rename to src/main/resources/data/basepower/loot_table/blocks/item_filter_plant.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/item_filter_smelt.json b/src/main/resources/data/basepower/loot_table/blocks/item_filter_smelt.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/item_filter_smelt.json rename to src/main/resources/data/basepower/loot_table/blocks/item_filter_smelt.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/refined_oil.json b/src/main/resources/data/basepower/loot_table/blocks/refined_oil.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/refined_oil.json rename to src/main/resources/data/basepower/loot_table/blocks/refined_oil.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/steel_frame.json b/src/main/resources/data/basepower/loot_table/blocks/steel_frame.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/steel_frame.json rename to src/main/resources/data/basepower/loot_table/blocks/steel_frame.json diff --git a/src/main/resources/data/basepower/loot_tables/blocks/still.json b/src/main/resources/data/basepower/loot_table/blocks/still.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/blocks/still.json rename to src/main/resources/data/basepower/loot_table/blocks/still.json diff --git a/src/main/resources/data/basepower/loot_tables/chests/advantage_parts.json b/src/main/resources/data/basepower/loot_table/chests/advantage_parts.json similarity index 100% rename from src/main/resources/data/basepower/loot_tables/chests/advantage_parts.json rename to src/main/resources/data/basepower/loot_table/chests/advantage_parts.json diff --git a/src/main/resources/data/basepower/recipes/bioplastic_from_starch.json b/src/main/resources/data/basepower/recipe/bioplastic_from_starch.json similarity index 100% rename from src/main/resources/data/basepower/recipes/bioplastic_from_starch.json rename to src/main/resources/data/basepower/recipe/bioplastic_from_starch.json diff --git a/src/main/resources/data/basepower/recipes/converter_rf_electricity.json b/src/main/resources/data/basepower/recipe/converter_rf_electricity.json similarity index 100% rename from src/main/resources/data/basepower/recipes/converter_rf_electricity.json rename to src/main/resources/data/basepower/recipe/converter_rf_electricity.json diff --git a/src/main/resources/data/basepower/recipes/converter_rf_quantum.json b/src/main/resources/data/basepower/recipe/converter_rf_quantum.json similarity index 100% rename from src/main/resources/data/basepower/recipes/converter_rf_quantum.json rename to src/main/resources/data/basepower/recipe/converter_rf_quantum.json diff --git a/src/main/resources/data/basepower/recipes/converter_rf_steam.json b/src/main/resources/data/basepower/recipe/converter_rf_steam.json similarity index 100% rename from src/main/resources/data/basepower/recipes/converter_rf_steam.json rename to src/main/resources/data/basepower/recipe/converter_rf_steam.json diff --git a/src/main/resources/data/basepower/recipes/converter_tr_electricity_iron.json b/src/main/resources/data/basepower/recipe/converter_tr_electricity_iron.json similarity index 100% rename from src/main/resources/data/basepower/recipes/converter_tr_electricity_iron.json rename to src/main/resources/data/basepower/recipe/converter_tr_electricity_iron.json diff --git a/src/main/resources/data/basepower/recipes/converter_tr_electricity_refined_iron.json b/src/main/resources/data/basepower/recipe/converter_tr_electricity_refined_iron.json similarity index 100% rename from src/main/resources/data/basepower/recipes/converter_tr_electricity_refined_iron.json rename to src/main/resources/data/basepower/recipe/converter_tr_electricity_refined_iron.json diff --git a/src/main/resources/data/basepower/recipes/crushing_fluid_discharge.json b/src/main/resources/data/basepower/recipe/crushing_fluid_discharge.json similarity index 100% rename from src/main/resources/data/basepower/recipes/crushing_fluid_discharge.json rename to src/main/resources/data/basepower/recipe/crushing_fluid_discharge.json diff --git a/src/main/resources/data/basepower/recipes/crushing_fluid_drain.json b/src/main/resources/data/basepower/recipe/crushing_fluid_drain.json similarity index 100% rename from src/main/resources/data/basepower/recipes/crushing_fluid_drain.json rename to src/main/resources/data/basepower/recipe/crushing_fluid_drain.json diff --git a/src/main/resources/data/basepower/recipes/crushing_fluid_storage_tank.json b/src/main/resources/data/basepower/recipe/crushing_fluid_storage_tank.json similarity index 100% rename from src/main/resources/data/basepower/recipes/crushing_fluid_storage_tank.json rename to src/main/resources/data/basepower/recipe/crushing_fluid_storage_tank.json diff --git a/src/main/resources/data/basepower/recipes/crushing_item_conveyor.json b/src/main/resources/data/basepower/recipe/crushing_item_conveyor.json similarity index 100% rename from src/main/resources/data/basepower/recipes/crushing_item_conveyor.json rename to src/main/resources/data/basepower/recipe/crushing_item_conveyor.json diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_block.json b/src/main/resources/data/basepower/recipe/crushing_item_filter_block.json similarity index 100% rename from src/main/resources/data/basepower/recipes/crushing_item_filter_block.json rename to src/main/resources/data/basepower/recipe/crushing_item_filter_block.json diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_food.json b/src/main/resources/data/basepower/recipe/crushing_item_filter_food.json similarity index 100% rename from src/main/resources/data/basepower/recipes/crushing_item_filter_food.json rename to src/main/resources/data/basepower/recipe/crushing_item_filter_food.json diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_fuel.json b/src/main/resources/data/basepower/recipe/crushing_item_filter_fuel.json similarity index 100% rename from src/main/resources/data/basepower/recipes/crushing_item_filter_fuel.json rename to src/main/resources/data/basepower/recipe/crushing_item_filter_fuel.json diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_inventory.json b/src/main/resources/data/basepower/recipe/crushing_item_filter_inventory.json similarity index 100% rename from src/main/resources/data/basepower/recipes/crushing_item_filter_inventory.json rename to src/main/resources/data/basepower/recipe/crushing_item_filter_inventory.json diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_ore.json b/src/main/resources/data/basepower/recipe/crushing_item_filter_ore.json similarity index 100% rename from src/main/resources/data/basepower/recipes/crushing_item_filter_ore.json rename to src/main/resources/data/basepower/recipe/crushing_item_filter_ore.json diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_overflow.json b/src/main/resources/data/basepower/recipe/crushing_item_filter_overflow.json similarity index 100% rename from src/main/resources/data/basepower/recipes/crushing_item_filter_overflow.json rename to src/main/resources/data/basepower/recipe/crushing_item_filter_overflow.json diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_plant.json b/src/main/resources/data/basepower/recipe/crushing_item_filter_plant.json similarity index 100% rename from src/main/resources/data/basepower/recipes/crushing_item_filter_plant.json rename to src/main/resources/data/basepower/recipe/crushing_item_filter_plant.json diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_smelt.json b/src/main/resources/data/basepower/recipe/crushing_item_filter_smelt.json similarity index 100% rename from src/main/resources/data/basepower/recipes/crushing_item_filter_smelt.json rename to src/main/resources/data/basepower/recipe/crushing_item_filter_smelt.json diff --git a/src/main/resources/data/basepower/recipes/crushing_potato_starch.json b/src/main/resources/data/basepower/recipe/crushing_potato_starch.json similarity index 100% rename from src/main/resources/data/basepower/recipes/crushing_potato_starch.json rename to src/main/resources/data/basepower/recipe/crushing_potato_starch.json diff --git a/src/main/resources/data/basepower/recipes/fluid_discharge.json b/src/main/resources/data/basepower/recipe/fluid_discharge.json similarity index 100% rename from src/main/resources/data/basepower/recipes/fluid_discharge.json rename to src/main/resources/data/basepower/recipe/fluid_discharge.json diff --git a/src/main/resources/data/basepower/recipes/fluid_drain.json b/src/main/resources/data/basepower/recipe/fluid_drain.json similarity index 100% rename from src/main/resources/data/basepower/recipes/fluid_drain.json rename to src/main/resources/data/basepower/recipe/fluid_drain.json diff --git a/src/main/resources/data/basepower/recipes/fluid_metal_tank_iron_normal.json b/src/main/resources/data/basepower/recipe/fluid_metal_tank_iron_normal.json similarity index 100% rename from src/main/resources/data/basepower/recipes/fluid_metal_tank_iron_normal.json rename to src/main/resources/data/basepower/recipe/fluid_metal_tank_iron_normal.json diff --git a/src/main/resources/data/basepower/recipes/fluid_metal_tank_steel.json b/src/main/resources/data/basepower/recipe/fluid_metal_tank_steel.json similarity index 100% rename from src/main/resources/data/basepower/recipes/fluid_metal_tank_steel.json rename to src/main/resources/data/basepower/recipe/fluid_metal_tank_steel.json diff --git a/src/main/resources/data/basepower/recipes/fluid_pipe_copper_apocalyptic.json b/src/main/resources/data/basepower/recipe/fluid_pipe_copper_apocalyptic.json similarity index 100% rename from src/main/resources/data/basepower/recipes/fluid_pipe_copper_apocalyptic.json rename to src/main/resources/data/basepower/recipe/fluid_pipe_copper_apocalyptic.json diff --git a/src/main/resources/data/basepower/recipes/fluid_pipe_copper_normal.json b/src/main/resources/data/basepower/recipe/fluid_pipe_copper_normal.json similarity index 100% rename from src/main/resources/data/basepower/recipes/fluid_pipe_copper_normal.json rename to src/main/resources/data/basepower/recipe/fluid_pipe_copper_normal.json diff --git a/src/main/resources/data/basepower/recipes/fluid_pipe_iron_apocalyptic.json b/src/main/resources/data/basepower/recipe/fluid_pipe_iron_apocalyptic.json similarity index 100% rename from src/main/resources/data/basepower/recipes/fluid_pipe_iron_apocalyptic.json rename to src/main/resources/data/basepower/recipe/fluid_pipe_iron_apocalyptic.json diff --git a/src/main/resources/data/basepower/recipes/fluid_pipe_iron_normal.json b/src/main/resources/data/basepower/recipe/fluid_pipe_iron_normal.json similarity index 100% rename from src/main/resources/data/basepower/recipes/fluid_pipe_iron_normal.json rename to src/main/resources/data/basepower/recipe/fluid_pipe_iron_normal.json diff --git a/src/main/resources/data/basepower/recipes/fluid_pipe_iron_tech.json b/src/main/resources/data/basepower/recipe/fluid_pipe_iron_tech.json similarity index 100% rename from src/main/resources/data/basepower/recipes/fluid_pipe_iron_tech.json rename to src/main/resources/data/basepower/recipe/fluid_pipe_iron_tech.json diff --git a/src/main/resources/data/basepower/recipes/fluid_pipe_lead_apocalyptic.json b/src/main/resources/data/basepower/recipe/fluid_pipe_lead_apocalyptic.json similarity index 100% rename from src/main/resources/data/basepower/recipes/fluid_pipe_lead_apocalyptic.json rename to src/main/resources/data/basepower/recipe/fluid_pipe_lead_apocalyptic.json diff --git a/src/main/resources/data/basepower/recipes/fluid_storage_tank.json b/src/main/resources/data/basepower/recipe/fluid_storage_tank.json similarity index 100% rename from src/main/resources/data/basepower/recipes/fluid_storage_tank.json rename to src/main/resources/data/basepower/recipe/fluid_storage_tank.json diff --git a/src/main/resources/data/basepower/recipes/fluid_switch.json b/src/main/resources/data/basepower/recipe/fluid_switch.json similarity index 100% rename from src/main/resources/data/basepower/recipes/fluid_switch.json rename to src/main/resources/data/basepower/recipe/fluid_switch.json diff --git a/src/main/resources/data/basepower/recipes/item_conveyor.json b/src/main/resources/data/basepower/recipe/item_conveyor.json similarity index 100% rename from src/main/resources/data/basepower/recipes/item_conveyor.json rename to src/main/resources/data/basepower/recipe/item_conveyor.json diff --git a/src/main/resources/data/basepower/recipes/item_filter_block.json b/src/main/resources/data/basepower/recipe/item_filter_block.json similarity index 100% rename from src/main/resources/data/basepower/recipes/item_filter_block.json rename to src/main/resources/data/basepower/recipe/item_filter_block.json diff --git a/src/main/resources/data/basepower/recipes/item_filter_food.json b/src/main/resources/data/basepower/recipe/item_filter_food.json similarity index 100% rename from src/main/resources/data/basepower/recipes/item_filter_food.json rename to src/main/resources/data/basepower/recipe/item_filter_food.json diff --git a/src/main/resources/data/basepower/recipes/item_filter_fuel.json b/src/main/resources/data/basepower/recipe/item_filter_fuel.json similarity index 100% rename from src/main/resources/data/basepower/recipes/item_filter_fuel.json rename to src/main/resources/data/basepower/recipe/item_filter_fuel.json diff --git a/src/main/resources/data/basepower/recipes/item_filter_inventory.json b/src/main/resources/data/basepower/recipe/item_filter_inventory.json similarity index 100% rename from src/main/resources/data/basepower/recipes/item_filter_inventory.json rename to src/main/resources/data/basepower/recipe/item_filter_inventory.json diff --git a/src/main/resources/data/basepower/recipes/item_filter_ore.json b/src/main/resources/data/basepower/recipe/item_filter_ore.json similarity index 100% rename from src/main/resources/data/basepower/recipes/item_filter_ore.json rename to src/main/resources/data/basepower/recipe/item_filter_ore.json diff --git a/src/main/resources/data/basepower/recipes/item_filter_overflow.json b/src/main/resources/data/basepower/recipe/item_filter_overflow.json similarity index 100% rename from src/main/resources/data/basepower/recipes/item_filter_overflow.json rename to src/main/resources/data/basepower/recipe/item_filter_overflow.json diff --git a/src/main/resources/data/basepower/recipes/item_filter_plant.json b/src/main/resources/data/basepower/recipe/item_filter_plant.json similarity index 100% rename from src/main/resources/data/basepower/recipes/item_filter_plant.json rename to src/main/resources/data/basepower/recipe/item_filter_plant.json diff --git a/src/main/resources/data/basepower/recipes/item_filter_smelt.json b/src/main/resources/data/basepower/recipe/item_filter_smelt.json similarity index 100% rename from src/main/resources/data/basepower/recipes/item_filter_smelt.json rename to src/main/resources/data/basepower/recipe/item_filter_smelt.json diff --git a/src/main/resources/data/basepower/recipes/rotator_tool.json b/src/main/resources/data/basepower/recipe/rotator_tool.json similarity index 100% rename from src/main/resources/data/basepower/recipes/rotator_tool.json rename to src/main/resources/data/basepower/recipe/rotator_tool.json diff --git a/src/main/resources/data/basepower/recipes/sprocket_bronze_nugget_tech_no_rod.json b/src/main/resources/data/basepower/recipe/sprocket_bronze_nugget_tech_no_rod.json similarity index 100% rename from src/main/resources/data/basepower/recipes/sprocket_bronze_nugget_tech_no_rod.json rename to src/main/resources/data/basepower/recipe/sprocket_bronze_nugget_tech_no_rod.json diff --git a/src/main/resources/data/basepower/recipes/sprocket_rod_normal.json b/src/main/resources/data/basepower/recipe/sprocket_rod_normal.json similarity index 100% rename from src/main/resources/data/basepower/recipes/sprocket_rod_normal.json rename to src/main/resources/data/basepower/recipe/sprocket_rod_normal.json diff --git a/src/main/resources/data/basepower/recipes/sprocket_rod_tech.json b/src/main/resources/data/basepower/recipe/sprocket_rod_tech.json similarity index 100% rename from src/main/resources/data/basepower/recipes/sprocket_rod_tech.json rename to src/main/resources/data/basepower/recipe/sprocket_rod_tech.json diff --git a/src/main/resources/data/basepower/recipes/sprocket_stickwood_normal.json b/src/main/resources/data/basepower/recipe/sprocket_stickwood_normal.json similarity index 100% rename from src/main/resources/data/basepower/recipes/sprocket_stickwood_normal.json rename to src/main/resources/data/basepower/recipe/sprocket_stickwood_normal.json diff --git a/src/main/resources/data/basepower/recipes/starch_from_potato.json b/src/main/resources/data/basepower/recipe/starch_from_potato.json similarity index 100% rename from src/main/resources/data/basepower/recipes/starch_from_potato.json rename to src/main/resources/data/basepower/recipe/starch_from_potato.json diff --git a/src/main/resources/data/basepower/recipes/steel_frame.json b/src/main/resources/data/basepower/recipe/steel_frame.json similarity index 100% rename from src/main/resources/data/basepower/recipes/steel_frame.json rename to src/main/resources/data/basepower/recipe/steel_frame.json diff --git a/src/main/resources/data/basepower/recipes/still.json b/src/main/resources/data/basepower/recipe/still.json similarity index 100% rename from src/main/resources/data/basepower/recipes/still.json rename to src/main/resources/data/basepower/recipe/still.json diff --git a/src/main/resources/data/forge/tags/items/bars.json b/src/main/resources/data/forge/tags/item/bars.json similarity index 100% rename from src/main/resources/data/forge/tags/items/bars.json rename to src/main/resources/data/forge/tags/item/bars.json diff --git a/src/main/resources/data/forge/tags/items/bars/steel.json b/src/main/resources/data/forge/tags/item/bars/steel.json similarity index 100% rename from src/main/resources/data/forge/tags/items/bars/steel.json rename to src/main/resources/data/forge/tags/item/bars/steel.json diff --git a/src/main/resources/data/forge/tags/items/dusts/charcoal.json b/src/main/resources/data/forge/tags/item/dusts/charcoal.json similarity index 100% rename from src/main/resources/data/forge/tags/items/dusts/charcoal.json rename to src/main/resources/data/forge/tags/item/dusts/charcoal.json diff --git a/src/main/resources/data/forge/tags/items/dusts/coal.json b/src/main/resources/data/forge/tags/item/dusts/coal.json similarity index 100% rename from src/main/resources/data/forge/tags/items/dusts/coal.json rename to src/main/resources/data/forge/tags/item/dusts/coal.json diff --git a/src/main/resources/data/forge/tags/items/dusts/lead.json b/src/main/resources/data/forge/tags/item/dusts/lead.json similarity index 100% rename from src/main/resources/data/forge/tags/items/dusts/lead.json rename to src/main/resources/data/forge/tags/item/dusts/lead.json diff --git a/src/main/resources/data/forge/tags/items/dusts/redstone.json b/src/main/resources/data/forge/tags/item/dusts/redstone.json similarity index 100% rename from src/main/resources/data/forge/tags/items/dusts/redstone.json rename to src/main/resources/data/forge/tags/item/dusts/redstone.json diff --git a/src/main/resources/data/forge/tags/items/dusts/silver.json b/src/main/resources/data/forge/tags/item/dusts/silver.json similarity index 100% rename from src/main/resources/data/forge/tags/items/dusts/silver.json rename to src/main/resources/data/forge/tags/item/dusts/silver.json diff --git a/src/main/resources/data/forge/tags/items/dusts/tin.json b/src/main/resources/data/forge/tags/item/dusts/tin.json similarity index 100% rename from src/main/resources/data/forge/tags/items/dusts/tin.json rename to src/main/resources/data/forge/tags/item/dusts/tin.json diff --git a/src/main/resources/data/forge/tags/items/frames/steel.json b/src/main/resources/data/forge/tags/item/frames/steel.json similarity index 100% rename from src/main/resources/data/forge/tags/items/frames/steel.json rename to src/main/resources/data/forge/tags/item/frames/steel.json diff --git a/src/main/resources/data/forge/tags/items/gears.json b/src/main/resources/data/forge/tags/item/gears.json similarity index 100% rename from src/main/resources/data/forge/tags/items/gears.json rename to src/main/resources/data/forge/tags/item/gears.json diff --git a/src/main/resources/data/forge/tags/items/gears/steel.json b/src/main/resources/data/forge/tags/item/gears/steel.json similarity index 100% rename from src/main/resources/data/forge/tags/items/gears/steel.json rename to src/main/resources/data/forge/tags/item/gears/steel.json diff --git a/src/main/resources/data/forge/tags/items/gems/diamond.json b/src/main/resources/data/forge/tags/item/gems/diamond.json similarity index 100% rename from src/main/resources/data/forge/tags/items/gems/diamond.json rename to src/main/resources/data/forge/tags/item/gems/diamond.json diff --git a/src/main/resources/data/forge/tags/items/gems/emerald.json b/src/main/resources/data/forge/tags/item/gems/emerald.json similarity index 100% rename from src/main/resources/data/forge/tags/items/gems/emerald.json rename to src/main/resources/data/forge/tags/item/gems/emerald.json diff --git a/src/main/resources/data/forge/tags/items/governors.json b/src/main/resources/data/forge/tags/item/governors.json similarity index 100% rename from src/main/resources/data/forge/tags/items/governors.json rename to src/main/resources/data/forge/tags/item/governors.json diff --git a/src/main/resources/data/forge/tags/items/ingots/brass.json b/src/main/resources/data/forge/tags/item/ingots/brass.json similarity index 100% rename from src/main/resources/data/forge/tags/items/ingots/brass.json rename to src/main/resources/data/forge/tags/item/ingots/brass.json diff --git a/src/main/resources/data/forge/tags/items/ingots/copper.json b/src/main/resources/data/forge/tags/item/ingots/copper.json similarity index 100% rename from src/main/resources/data/forge/tags/items/ingots/copper.json rename to src/main/resources/data/forge/tags/item/ingots/copper.json diff --git a/src/main/resources/data/forge/tags/items/ingots/gold.json b/src/main/resources/data/forge/tags/item/ingots/gold.json similarity index 100% rename from src/main/resources/data/forge/tags/items/ingots/gold.json rename to src/main/resources/data/forge/tags/item/ingots/gold.json diff --git a/src/main/resources/data/forge/tags/items/ingots/iron.json b/src/main/resources/data/forge/tags/item/ingots/iron.json similarity index 100% rename from src/main/resources/data/forge/tags/items/ingots/iron.json rename to src/main/resources/data/forge/tags/item/ingots/iron.json diff --git a/src/main/resources/data/forge/tags/items/ingots/lead.json b/src/main/resources/data/forge/tags/item/ingots/lead.json similarity index 100% rename from src/main/resources/data/forge/tags/items/ingots/lead.json rename to src/main/resources/data/forge/tags/item/ingots/lead.json diff --git a/src/main/resources/data/forge/tags/items/ingots/nickel.json b/src/main/resources/data/forge/tags/item/ingots/nickel.json similarity index 100% rename from src/main/resources/data/forge/tags/items/ingots/nickel.json rename to src/main/resources/data/forge/tags/item/ingots/nickel.json diff --git a/src/main/resources/data/forge/tags/items/ingots/plastic.json b/src/main/resources/data/forge/tags/item/ingots/plastic.json similarity index 100% rename from src/main/resources/data/forge/tags/items/ingots/plastic.json rename to src/main/resources/data/forge/tags/item/ingots/plastic.json diff --git a/src/main/resources/data/forge/tags/items/ingots/refined_iron.json b/src/main/resources/data/forge/tags/item/ingots/refined_iron.json similarity index 100% rename from src/main/resources/data/forge/tags/items/ingots/refined_iron.json rename to src/main/resources/data/forge/tags/item/ingots/refined_iron.json diff --git a/src/main/resources/data/forge/tags/items/ingots/rubber.json b/src/main/resources/data/forge/tags/item/ingots/rubber.json similarity index 100% rename from src/main/resources/data/forge/tags/items/ingots/rubber.json rename to src/main/resources/data/forge/tags/item/ingots/rubber.json diff --git a/src/main/resources/data/forge/tags/items/ingots/steel.json b/src/main/resources/data/forge/tags/item/ingots/steel.json similarity index 100% rename from src/main/resources/data/forge/tags/items/ingots/steel.json rename to src/main/resources/data/forge/tags/item/ingots/steel.json diff --git a/src/main/resources/data/forge/tags/items/ingots/zinc.json b/src/main/resources/data/forge/tags/item/ingots/zinc.json similarity index 100% rename from src/main/resources/data/forge/tags/items/ingots/zinc.json rename to src/main/resources/data/forge/tags/item/ingots/zinc.json diff --git a/src/main/resources/data/forge/tags/items/nuggets/bronze.json b/src/main/resources/data/forge/tags/item/nuggets/bronze.json similarity index 100% rename from src/main/resources/data/forge/tags/items/nuggets/bronze.json rename to src/main/resources/data/forge/tags/item/nuggets/bronze.json diff --git a/src/main/resources/data/forge/tags/items/nuggets/copper.json b/src/main/resources/data/forge/tags/item/nuggets/copper.json similarity index 100% rename from src/main/resources/data/forge/tags/items/nuggets/copper.json rename to src/main/resources/data/forge/tags/item/nuggets/copper.json diff --git a/src/main/resources/data/forge/tags/items/nuggets/gold.json b/src/main/resources/data/forge/tags/item/nuggets/gold.json similarity index 100% rename from src/main/resources/data/forge/tags/items/nuggets/gold.json rename to src/main/resources/data/forge/tags/item/nuggets/gold.json diff --git a/src/main/resources/data/forge/tags/items/nuggets/iron.json b/src/main/resources/data/forge/tags/item/nuggets/iron.json similarity index 100% rename from src/main/resources/data/forge/tags/items/nuggets/iron.json rename to src/main/resources/data/forge/tags/item/nuggets/iron.json diff --git a/src/main/resources/data/forge/tags/items/nuggets/lead.json b/src/main/resources/data/forge/tags/item/nuggets/lead.json similarity index 100% rename from src/main/resources/data/forge/tags/items/nuggets/lead.json rename to src/main/resources/data/forge/tags/item/nuggets/lead.json diff --git a/src/main/resources/data/forge/tags/items/nuggets/tin.json b/src/main/resources/data/forge/tags/item/nuggets/tin.json similarity index 100% rename from src/main/resources/data/forge/tags/items/nuggets/tin.json rename to src/main/resources/data/forge/tags/item/nuggets/tin.json diff --git a/src/main/resources/data/forge/tags/items/pipes.json b/src/main/resources/data/forge/tags/item/pipes.json similarity index 100% rename from src/main/resources/data/forge/tags/items/pipes.json rename to src/main/resources/data/forge/tags/item/pipes.json diff --git a/src/main/resources/data/forge/tags/items/plastic.json b/src/main/resources/data/forge/tags/item/plastic.json similarity index 100% rename from src/main/resources/data/forge/tags/items/plastic.json rename to src/main/resources/data/forge/tags/item/plastic.json diff --git a/src/main/resources/data/forge/tags/items/plates/copper.json b/src/main/resources/data/forge/tags/item/plates/copper.json similarity index 100% rename from src/main/resources/data/forge/tags/items/plates/copper.json rename to src/main/resources/data/forge/tags/item/plates/copper.json diff --git a/src/main/resources/data/forge/tags/items/plates/iron.json b/src/main/resources/data/forge/tags/item/plates/iron.json similarity index 100% rename from src/main/resources/data/forge/tags/items/plates/iron.json rename to src/main/resources/data/forge/tags/item/plates/iron.json diff --git a/src/main/resources/data/forge/tags/items/plates/steel.json b/src/main/resources/data/forge/tags/item/plates/steel.json similarity index 100% rename from src/main/resources/data/forge/tags/items/plates/steel.json rename to src/main/resources/data/forge/tags/item/plates/steel.json diff --git a/src/main/resources/data/forge/tags/items/power_supply_units.json b/src/main/resources/data/forge/tags/item/power_supply_units.json similarity index 100% rename from src/main/resources/data/forge/tags/items/power_supply_units.json rename to src/main/resources/data/forge/tags/item/power_supply_units.json diff --git a/src/main/resources/data/forge/tags/items/rods.json b/src/main/resources/data/forge/tags/item/rods.json similarity index 100% rename from src/main/resources/data/forge/tags/items/rods.json rename to src/main/resources/data/forge/tags/item/rods.json diff --git a/src/main/resources/data/forge/tags/items/rods/copper.json b/src/main/resources/data/forge/tags/item/rods/copper.json similarity index 100% rename from src/main/resources/data/forge/tags/items/rods/copper.json rename to src/main/resources/data/forge/tags/item/rods/copper.json diff --git a/src/main/resources/data/forge/tags/items/rods/wooden.json b/src/main/resources/data/forge/tags/item/rods/wooden.json similarity index 100% rename from src/main/resources/data/forge/tags/items/rods/wooden.json rename to src/main/resources/data/forge/tags/item/rods/wooden.json diff --git a/src/main/resources/data/forge/tags/items/rubber.json b/src/main/resources/data/forge/tags/item/rubber.json similarity index 100% rename from src/main/resources/data/forge/tags/items/rubber.json rename to src/main/resources/data/forge/tags/item/rubber.json diff --git a/src/main/resources/data/forge/tags/items/sprockets.json b/src/main/resources/data/forge/tags/item/sprockets.json similarity index 100% rename from src/main/resources/data/forge/tags/items/sprockets.json rename to src/main/resources/data/forge/tags/item/sprockets.json diff --git a/src/main/resources/data/forge/tags/items/sprockets/steel.json b/src/main/resources/data/forge/tags/item/sprockets/steel.json similarity index 100% rename from src/main/resources/data/forge/tags/items/sprockets/steel.json rename to src/main/resources/data/forge/tags/item/sprockets/steel.json diff --git a/src/main/resources/data/forge/tags/items/stone.json b/src/main/resources/data/forge/tags/item/stone.json similarity index 100% rename from src/main/resources/data/forge/tags/items/stone.json rename to src/main/resources/data/forge/tags/item/stone.json diff --git a/src/main/resources/data/forge/tags/items/storage_blocks/diamond.json b/src/main/resources/data/forge/tags/item/storage_blocks/diamond.json similarity index 100% rename from src/main/resources/data/forge/tags/items/storage_blocks/diamond.json rename to src/main/resources/data/forge/tags/item/storage_blocks/diamond.json diff --git a/src/main/resources/data/forge/tags/items/storage_blocks/steel.json b/src/main/resources/data/forge/tags/item/storage_blocks/steel.json similarity index 100% rename from src/main/resources/data/forge/tags/items/storage_blocks/steel.json rename to src/main/resources/data/forge/tags/item/storage_blocks/steel.json diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index 857da2ef..f1e3b523 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,7 +1,7 @@ { "pack": { "description": "Power Advantage resources", - "pack_format": 32, - "supported_formats": { "min_inclusive": 32, "max_inclusive": 41 } + "pack_format": 34, + "supported_formats": { "min_inclusive": 34, "max_inclusive": 48 } } } diff --git a/src/test/java/zone/moddev/mc/basepower/math/BlockPos4DTest.java b/src/test/java/zone/moddev/mc/basepower/math/BlockPos4DTest.java index d13cdea6..3b1e85f4 100644 --- a/src/test/java/zone/moddev/mc/basepower/math/BlockPos4DTest.java +++ b/src/test/java/zone/moddev/mc/basepower/math/BlockPos4DTest.java @@ -14,8 +14,8 @@ public class BlockPos4DTest { @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); } @Test public void customDimensionKeysNeverShareCoordinates() { - ResourceKey first = ResourceKey.create(Registries.DIMENSION, new ResourceLocation("test:first")); - ResourceKey second = ResourceKey.create(Registries.DIMENSION, new ResourceLocation("test:second")); + ResourceKey first = ResourceKey.create(Registries.DIMENSION, ResourceLocation.parse("test:first")); + ResourceKey second = ResourceKey.create(Registries.DIMENSION, ResourceLocation.parse("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())); diff --git a/src/test/java/zone/moddev/mc/basepower/migration/AdvantageLegacyMigrationsTest.java b/src/test/java/zone/moddev/mc/basepower/migration/AdvantageLegacyMigrationsTest.java index 6dc4828c..dfe9ba69 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/AdvantageLegacyMigrationsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/AdvantageLegacyMigrationsTest.java @@ -22,9 +22,9 @@ public class AdvantageLegacyMigrationsTest { "converter_rf_steam", "converter_rf_electricity", "converter_rf_quantum", "converter_tr_electricity")) { assertNotNull(path, LegacyMigrationApi.blockStateCodecs().get( - new ResourceLocation("basepower", path))); - assertEquals(new ResourceLocation("basepower", path), - LegacyMigrationApi.blockTarget(new ResourceLocation("poweradvantage", path))); + ResourceLocation.fromNamespaceAndPath("basepower", path))); + assertEquals(ResourceLocation.fromNamespaceAndPath("basepower", path), + LegacyMigrationApi.blockTarget(ResourceLocation.fromNamespaceAndPath("poweradvantage", path))); } } @@ -38,9 +38,9 @@ public class AdvantageLegacyMigrationsTest { LegacyMigrationApi.tileTarget("minecraft:poweradvantage.tileentity.fluid_storage_tank")); assertEquals("basepower:crude_oil", LegacyMigrationApi.fluidTarget("crude_oil")); assertEquals("basepower:crude_oil", LegacyMigrationApi.fluidTarget("poweradvantage:crude_oil")); - assertEquals(new ResourceLocation("basepower", "crude_oil_bucket"), + assertEquals(ResourceLocation.fromNamespaceAndPath("basepower", "crude_oil_bucket"), LegacyMigrationApi.bucketTarget("crude_oil")); - assertEquals(new ResourceLocation("basepower", "refined_oil_bucket"), + assertEquals(ResourceLocation.fromNamespaceAndPath("basepower", "refined_oil_bucket"), LegacyMigrationApi.bucketTarget("poweradvantage:refined_oil")); } } diff --git a/src/test/java/zone/moddev/mc/basepower/migration/DataDrivenRegistryMigrationTest.java b/src/test/java/zone/moddev/mc/basepower/migration/DataDrivenRegistryMigrationTest.java new file mode 100644 index 00000000..d49f5639 --- /dev/null +++ b/src/test/java/zone/moddev/mc/basepower/migration/DataDrivenRegistryMigrationTest.java @@ -0,0 +1,116 @@ +package zone.moddev.mc.basepower.migration; + +import java.nio.file.Files; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtAccounter; +import net.minecraft.nbt.TagParser; +import net.minecraft.resources.ResourceLocation; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import static org.junit.Assert.*; + +public class DataDrivenRegistryMigrationTest { + @Rule public TemporaryFolder temporary = new TemporaryFolder(); + + @Test public void archivesOnlyTheRetiredFlattenedIndexWithoutEditingAnyEnchantments() throws Exception { + var root = TagParser.parseTag("{Data:{DataVersion:3839,Player:{Inventory:[{id:\"basesteam:musket\",count:1," + + "components:{\"minecraft:enchantments\":{levels:{\"basesteam:recoil\":3}}}}]}}," + + "fml:{Registries:{\"minecraft:enchantment\":{ids:[{K:\"basesteam:recoil\",V:101}," + + "{K:\"other:private_enchantment\",V:102}],blocked:[I;105]},\"other:registry\":{opaque:1}}}," + + "FML:{Registries:{\"minecraft:enchantments\":{ids:[{K:\"steamadvantage:recoil\",V:77}]}}}}"); + var old = root.copy(); + assertTrue(LegacyWorldDataHook.archiveDataDrivenEnchantmentIndex(root)); + assertEquals(old.getCompound("Data"), root.getCompound("Data")); + assertEquals(old.getCompound("FML"), root.getCompound("FML")); + assertEquals(old.getCompound("fml").getCompound("Registries").get("other:registry"), + root.getCompound("fml").getCompound("Registries").get("other:registry")); + assertEquals(old.getCompound("fml").getCompound("Registries").get("minecraft:enchantment"), + root.getCompound("basepower:legacy_registry_indexes").get("minecraft:enchantment")); + assertFalse(root.getCompound("fml").getCompound("Registries").contains("minecraft:enchantment")); + var once = root.copy(); + assertFalse(LegacyWorldDataHook.archiveDataDrivenEnchantmentIndex(root)); + assertEquals(once, root); + } + + @Test public void filePreparationKeepsAnExactArchiveAcrossAnotherSaveAndRestart() throws Exception { + var world = temporary.newFolder().toPath(); + var file = world.resolve("level.dat"); + var source = TagParser.parseTag("{Data:{DataVersion:3839},fml:{Registries:" + + "{\"minecraft:enchantment\":{ids:[{K:\"basesteam:recoil\",V:101}]}}}}"); + NbtIo.writeCompressed(source, file); + LegacyWorldDataHook.prepareForgeRegistryMetadata(file.toFile()); + assertEquals(source, NbtIo.readCompressed(world.resolve("level.dat.basepower-legacy-backup"), + NbtAccounter.unlimitedHeap())); + var archive = world.resolve("basepower-retired-registry-indexes.dat"); + byte[] bytes = Files.readAllBytes(archive); + assertEquals(source.getCompound("fml").getCompound("Registries"), + NbtIo.readCompressed(archive, NbtAccounter.unlimitedHeap())); + var current = TagParser.parseTag("{Data:{DataVersion:3955},fml:{Registries:{}}}"); + NbtIo.writeCompressed(current, file); + LegacyWorldDataHook.prepareForgeRegistryMetadata(file.toFile()); + assertArrayEquals(bytes, Files.readAllBytes(archive)); + assertEquals(current, NbtIo.readCompressed(file, NbtAccounter.unlimitedHeap())); + } + + @Test public void recognizedPaintingIndexesStillResolveInTheNativeDataRegistry() { + net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); + var lookup = net.minecraft.data.registries.VanillaRegistries.createLookup() + .lookupOrThrow(net.minecraft.core.registries.Registries.PAINTING_VARIANT); + var registry = ResourceLocation.parse("minecraft:painting_variant"); + int found = 0; + for (var holder : lookup.listElements().toList()) { + if (VanillaRegistryRemaps.retiredPaintingIndex(registry, holder.key().location())) found++; + } + assertEquals(30, found); + assertFalse(VanillaRegistryRemaps.retiredPaintingIndex(registry, ResourceLocation.parse("other:alban"))); + assertFalse(VanillaRegistryRemaps.retiredPaintingIndex(registry, ResourceLocation.parse("minecraft:unknown"))); + assertFalse(VanillaRegistryRemaps.retiredPaintingIndex(ResourceLocation.parse("minecraft:item"), + ResourceLocation.parse("minecraft:alban"))); + } + + @Test public void aConflictingArchiveIsNeverReplacedOrSilentlyDiscarded() throws Exception { + var root = TagParser.parseTag("{fml:{Registries:{\"minecraft:enchantment\":{ids:[{K:\"basesteam:recoil\",V:3}]}}}," + + "\"basepower:legacy_registry_indexes\":{\"minecraft:enchantment\":{ids:[{K:\"other:different\",V:4}]}}}"); + var before = root.copy(); + try { + LegacyWorldDataHook.archiveDataDrivenEnchantmentIndex(root); + fail("Conflicting archive was accepted"); + } catch (IllegalStateException expected) { + assertEquals(before, root); + } + } + + @Test public void nativePaintingLoadStillKeepsTheSavedVariantFacingAndUuid() throws Exception { + net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); + zone.moddev.mc.advantageworks.unit.ForgeUnitRegistries.registerVanillaFluidTypes(); + var paintings = new net.minecraft.core.MappedRegistry<>( + net.minecraft.core.registries.Registries.PAINTING_VARIANT, com.mojang.serialization.Lifecycle.stable()); + net.minecraft.data.registries.VanillaRegistries.createLookup() + .lookupOrThrow(net.minecraft.core.registries.Registries.PAINTING_VARIANT).listElements().forEach(holder -> + net.minecraft.core.Registry.register(paintings, holder.key(), holder.value())); + paintings.freeze(); + var world = NativeWolfTestWorld.create(); + var field = net.minecraft.world.level.Level.class.getDeclaredField("registryAccess"); + field.setAccessible(true); + field.set(world, new net.minecraft.core.RegistryAccess.ImmutableRegistryAccess(java.util.List.of(paintings)).freeze()); + var tag = TagParser.parseTag("{id:\"minecraft:painting\",UUID:[I;1,2,3,4],variant:\"minecraft:alban\"," + + "facing:1b,Pos:[10d,80d,12d],Motion:[0d,0d,0d],Rotation:[0f,0f],TileX:10,TileY:80,TileZ:12}"); + var root = new CompoundTag(); + root.putInt("DataVersion", 3839); + var entities = new net.minecraft.nbt.ListTag(); + entities.add(tag); + root.put("Entities", entities); + LegacyEntityDataFixes.prepare(root); + LegacyEntityDataFixes.finish(root); + var converted = root.getList("Entities", 10).getCompound(0); + var entity = new net.minecraft.world.entity.decoration.Painting(net.minecraft.world.entity.EntityType.PAINTING, world); + entity.load(converted); + var saved = entity.saveWithoutId(new CompoundTag()); + assertEquals(ResourceLocation.parse("minecraft:alban"), entity.getVariant().unwrapKey().orElseThrow().location()); + assertEquals("minecraft:alban", saved.getString("variant")); + assertEquals(1, saved.getByte("facing")); + assertEquals(tag.getUUID("UUID"), entity.getUUID()); + } +} diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntityAttributeAliasesTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntityAttributeAliasesTest.java index 1b570db6..f0fc8837 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntityAttributeAliasesTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntityAttributeAliasesTest.java @@ -42,6 +42,7 @@ public class LegacyEntityAttributeAliasesTest { 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"); + expected = LegacyForgeAttributeRegistryTest.nativeAttributeRecord(expected); assertEquals(expected, actual); assertEquals(before, entity); } @@ -51,12 +52,12 @@ public class LegacyEntityAttributeAliasesTest { LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); 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); + .getList("attributes", 10).getCompound(0); + assertEquals("forge:swim_speed", attribute.getString("id")); + assertEquals(2D, attribute.getDouble("base"), 0D); assertEquals("keep-attribute-field", attribute.getString("PrivateNote")); - CompoundTag modifier = attribute.getList("Modifiers", 10).getCompound(0); - assertEquals(MODIFIER, modifier.getUUID("UUID")); + CompoundTag modifier = attribute.getList("modifiers", 10).getCompound(0); + assertEquals("minecraft:" + MODIFIER, modifier.getString("id")); assertEquals("keep-modifier-field", modifier.getString("PrivateNote")); } @@ -73,7 +74,7 @@ public class LegacyEntityAttributeAliasesTest { LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); 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")); + .getList("attributes", 10).getCompound(0).getString("id")); assertEquals(foreign, actual.getCompound("PrivateData")); assertEquals(foreign, actual.getCompound("ForgeData")); assertEquals(items, actual.getList("Items", 10)); @@ -96,6 +97,8 @@ public class LegacyEntityAttributeAliasesTest { } entity.put("Attributes", list); CompoundTag root = chunk(entity), before = root.copy(); + before.getCompound("Level").getList("Entities", 10).set(0, + LegacyForgeAttributeRegistryTest.nativeAttributeRecord(entity)); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); assertEquals(before, root); } @@ -175,10 +178,10 @@ public class LegacyEntityAttributeAliasesTest { 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")); + CompoundTag attribute = actual.getList("attributes", 10).getCompound(0); + assertEquals("forge:swim_speed", attribute.getString("id")); + assertEquals(2D, attribute.getDouble("base"), 0D); + assertEquals("minecraft:" + MODIFIER, attribute.getList("modifiers", 10).getCompound(0).getString("id")); } assertEquals(before, player); } @@ -190,12 +193,12 @@ public class LegacyEntityAttributeAliasesTest { player.put("PrivateData", foreign.copy()); 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")); + CompoundTag attribute = result.getList("attributes", 10).getCompound(0); + assertEquals("forge:swim_speed", attribute.getString("id")); + assertEquals(2D, attribute.getDouble("base"), 0D); + assertEquals("minecraft:" + MODIFIER, attribute.getList("modifiers", 10).getCompound(0).getString("id")); assertEquals("keep-attribute-field", attribute.getString("PrivateNote")); - assertEquals("keep-modifier-field", attribute.getList("Modifiers", 10).getCompound(0).getString("PrivateNote")); + assertEquals("keep-modifier-field", attribute.getList("modifiers", 10).getCompound(0).getString("PrivateNote")); assertEquals(foreign, result.getCompound("PrivateData")); assertEquals(before, player); } @@ -236,8 +239,9 @@ public class LegacyEntityAttributeAliasesTest { Wolf after = new Wolf(EntityType.WOLF, world); after.load(root.getCompound("Level").getList("Entities", 10).getCompound(0)); assertEquals(2D, after.getAttribute(swimSpeed).getBaseValue(), 0D); - assertNotNull(after.getAttribute(swimSpeed).getModifier(MODIFIER)); - assertEquals(0.75D, after.getAttribute(swimSpeed).getModifier(MODIFIER).amount(), 0D); + var modifierId = net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("minecraft", MODIFIER.toString()); + assertNotNull(after.getAttribute(swimSpeed).getModifier(modifierId)); + assertEquals(0.75D, after.getAttribute(swimSpeed).getModifier(modifierId).amount(), 0D); assertEquals(2.75D, after.getAttributeValue(swimSpeed), 0D); assertEquals(8F, after.getHealth(), 0F); } diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyForgeAttributeRegistryTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyForgeAttributeRegistryTest.java index ade93971..39c892e7 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyForgeAttributeRegistryTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyForgeAttributeRegistryTest.java @@ -51,6 +51,7 @@ static void registerForgeAttributes() throws ReflectiveOperationException { LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); CompoundTag expected = before.copy(); expected.getList("Attributes", 10).getCompound(0).putString("Name", alias[1]); + expected = nativeAttributeRecord(expected); assertEquals(alias[0], expected, fixedEntity(root)); assertEquals(before, source); } @@ -66,14 +67,14 @@ static void registerForgeAttributes() throws ReflectiveOperationException { SharedConstants.getCurrentVersion().getDataVersion().getVersion()).getValue(); Wolf negative = new Wolf(EntityType.WOLF, world); negative.load(raw); AttributeInstance old = negative.getAttribute(ForgeRegistries.ATTRIBUTES.getHolder( - new net.minecraft.resources.ResourceLocation(ALIASES[index][1])).orElseThrow()); + ResourceLocation.parse(ALIASES[index][1])).orElseThrow()); assertNotEquals(ALIASES[index][0], values[index], old.getBaseValue(), 0D); CompoundTag root = chunk(source); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); 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)).amount(), 0D); + assertEquals(0.125D, actual.getModifier(modifierId()).amount(), 0D); assertEquals(values[index] + 0.125D, actual.getValue(), 0D); } } @@ -82,16 +83,16 @@ static void registerForgeAttributes() throws ReflectiveOperationException { 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("minecraft:player.block_interaction_range", value.getString("Name")); + CompoundTag value = actual.getList("attributes", 10).getCompound(0); + assertEquals("minecraft:player.block_interaction_range", value.getString("id")); var reach = net.minecraft.world.entity.ai.attributes.Attributes.BLOCK_INTERACTION_RANGE; assertSame(reach.value(), ForgeRegistries.ATTRIBUTES.getValue( - new net.minecraft.resources.ResourceLocation(value.getString("Name")))); + ResourceLocation.parse(value.getString("id")))); AttributeInstance nativeAttribute = new AttributeInstance(reach, unused -> {}); nativeAttribute.load(value); assertEquals(7.5D, nativeAttribute.getBaseValue(), 0D); assertEquals(7.625D, nativeAttribute.getValue(), 0D); - assertEquals(0.125D, nativeAttribute.getModifier(new UUID(17L, 19L)).amount(), 0D); + assertEquals(0.125D, nativeAttribute.getModifier(modifierId()).amount(), 0D); assertEquals(before, source); } @@ -121,9 +122,9 @@ static void registerForgeAttributes() throws ReflectiveOperationException { 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) - .getList("Modifiers", 10).getCompound(0).getDouble("Amount"), 0D); + assertEquals(alias[1], item.getList("attributes", 10).getCompound(0).getString("id")); + assertEquals(0.125D, item.getList("attributes", 10).getCompound(0) + .getList("modifiers", 10).getCompound(0).getDouble("amount"), 0D); } assertEquals(before.getCompound("PrivateData"), actual.getCompound("PrivateData")); assertEquals(before, source); @@ -146,10 +147,43 @@ static void registerForgeAttributes() throws ReflectiveOperationException { root.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion()); CompoundTag before = root.copy(); LegacyEntityDataFixes.prepare(root); assertEquals(before, root); root = chunk(entity(alias[0] + ".private", 7D)); before = root.copy(); + before.getCompound("Level").getList("Entities", 10).set(0, + nativeAttributeRecord(before.getCompound("Level").getList("Entities", 10).getCompound(0))); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); assertEquals(before, root); } } + private static ResourceLocation modifierId() { + return ResourceLocation.fromNamespaceAndPath("minecraft", new UUID(17L, 19L).toString()); + } + + static CompoundTag nativeAttributeRecord(CompoundTag source) { + CompoundTag fields = new CompoundTag(); + fields.put("Attributes", source.get("Attributes").copy()); + CompoundTag nativeFields = DataFixTypes.PLAYER.updateToCurrentVersion( + DataFixers.getDataFixer(), fields, 1343); + CompoundTag expected = source.copy(); + expected.remove("Attributes"); + expected.put("attributes", nativeFields.get("attributes").copy()); + return expected; + } + + @Test public void unknownModEntityRetainsItsAttributesInTheNativeSaveFormat() throws Exception { + CompoundTag source = entity("forge.swimSpeed", 2D), original = source.copy(); + source.put("PrivateData", original.copy()); + CompoundTag root = chunk(source); + LegacyEntityDataFixes.prepare(root); + LegacyEntityDataFixes.finish(root); + CompoundTag converted = fixedEntity(root); + Wolf nativeReader = new Wolf(EntityType.WOLF, NativeWolfTestWorld.create()); + nativeReader.load(converted); + AttributeInstance speed = nativeReader.getAttribute(ForgeMod.SWIM_SPEED.getHolder().orElseThrow()); + assertEquals(2D, speed.getBaseValue(), 0D); + assertNotNull(speed.getModifier(modifierId())); + assertEquals(2.125D, speed.getValue(), 0D); + assertEquals(original, converted.getCompound("PrivateData")); + } + 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); diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java index 463532dd..c05cd17a 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java @@ -29,21 +29,30 @@ import static org.junit.Assert.*; public class LegacyMachineItemsTest { + private static net.minecraft.core.HolderLookup.Provider registryLookup; + @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); + net.minecraftforge.registries.GameData.unfreezeData(); + registryLookup = net.minecraft.data.registries.VanillaRegistries.createLookup(); AdvantageLegacyMigrations.registerPowerAdvantage(); } @Test public void colouredWoolSurvivesAnActualConveyorInventoryLoad() { CompoundTag tile = convertedTile(stack("minecraft:wool", 14, 7)); - TileEntityConveyor conveyor = new TileEntityConveyor(net.minecraft.core.BlockPos.ZERO, Blocks.HOPPER.defaultBlockState()); - conveyor.loadWithComponents(tile, registries()); - assertSame(Items.RED_WOOL, conveyor.getItem(0).getItem()); - assertEquals(7, conveyor.getItem(0).getCount()); + var previous = zone.moddev.mc.basepower.init.Entities.CONVEYOR; + try { + zone.moddev.mc.basepower.init.Entities.CONVEYOR = net.minecraft.world.level.block.entity.BlockEntityType.Builder + .of(TileEntityConveyor::new, Blocks.HOPPER).build(null); + TileEntityConveyor conveyor = new TileEntityConveyor(net.minecraft.core.BlockPos.ZERO, Blocks.HOPPER.defaultBlockState()); + conveyor.loadWithComponents(tile, registries()); + assertSame(Items.RED_WOOL, conveyor.getItem(0).getItem()); + assertEquals(7, conveyor.getItem(0).getCount()); + } finally { zone.moddev.mc.basepower.init.Entities.CONVEYOR = previous; } } - private static net.minecraft.core.RegistryAccess registries() { - return net.minecraft.core.RegistryAccess.fromRegistryOfRegistries(net.minecraft.core.registries.BuiltInRegistries.REGISTRY); + private static net.minecraft.core.HolderLookup.Provider registries() { + return registryLookup; } @Test public void preservesVanillaMetadataVariantsInMachineSlots() { @@ -80,6 +89,9 @@ private static net.minecraft.core.RegistryAccess registries() { assertEquals(1, loaded.getCount()); assertEquals(17, loaded.getDamageValue()); assertEquals("Survey Pick", loaded.getHoverName().getString()); + var efficiency = registries().lookupOrThrow(net.minecraft.core.registries.Registries.ENCHANTMENT) + .getOrThrow(net.minecraft.world.item.enchantment.Enchantments.EFFICIENCY); + assertEquals(3, net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(efficiency, loaded)); } @Test public void nestedStoredItemsAreConvertedWithoutChangingMachineState() { diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java index 4e05859f..d97d7d1e 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java @@ -206,7 +206,7 @@ private static MethodNode nativePlayerMethod(ClassNode node, PlayerHook target) List methods = node.methods.stream().filter(method -> target.nativeCalls(method).size() == 1 && Arrays.stream(method.instructions.toArray()).anyMatch( LegacyPlayerTransformerTest::loadsPlayerType)).toList(); - assertEquals("Expected one Forge 50 PLAYER reader in " + node.name, 1, methods.size()); + assertEquals("Expected one Forge 52 PLAYER reader in " + node.name, 1, methods.size()); return methods.getFirst(); } diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyRegistryArchiveTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyRegistryArchiveTest.java index 982a2ec7..a21c421b 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyRegistryArchiveTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyRegistryArchiveTest.java @@ -49,13 +49,13 @@ public class LegacyRegistryArchiveTest { Files.write(region, header); try { assertEquals(1, LegacyWorldDataHook.indexLegacyChunks(world.toFile())); - var nether = new ResourceLocation("the_nether"); + var nether = ResourceLocation.parse("the_nether"); assertTrue(LegacyWorldDataHook.isProtectedChunk(nether, new BlockPos(0, 0, 0))); assertTrue(LegacyWorldDataHook.isProtectedChunk(nether, new BlockPos(0, 255, 0))); assertFalse(LegacyWorldDataHook.isProtectedChunk(nether, new BlockPos(0, -1, 0))); assertFalse(LegacyWorldDataHook.isProtectedChunk(nether, new BlockPos(0, 256, 0))); assertFalse(LegacyWorldDataHook.isProtectedChunk(nether, new BlockPos(16, 64, 0))); - assertFalse(LegacyWorldDataHook.isProtectedChunk(new ResourceLocation("overworld"), new BlockPos(0, 64, 0))); + assertFalse(LegacyWorldDataHook.isProtectedChunk(ResourceLocation.parse("overworld"), new BlockPos(0, 64, 0))); } finally { LegacyWorldDataHook.indexLegacyChunks(temporary.newFolder()); } diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyTransformerOrderTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyTransformerOrderTest.java index f1c84674..b03f1c23 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyTransformerOrderTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyTransformerOrderTest.java @@ -20,7 +20,7 @@ import org.objectweb.asm.tree.MethodNode; import static org.junit.Assert.*; -/** Applies both real migration scripts to the exact native Forge 50 classes. */ +/** Applies both real migration scripts to the exact native Forge 52 classes. */ public class LegacyTransformerOrderTest { private static final String WORLD = "net/minecraft/world/level/storage/LevelStorageSource"; private static final String CHUNK = "net/minecraft/world/level/chunk/storage/ChunkStorage"; @@ -87,7 +87,11 @@ private static void check(boolean baseFirst, boolean renamedChunk) throws Except } applyBoth(chunk, "legacy_chunk_status", baseFirst); assertBoth(chunk, "prepareLegacyChunk", "(" + NBT + ")V"); - assertBoth(chunk, "finalizeLegacyChunk", "(" + NBT + ")" + NBT); + int returns = (int) Arrays.stream(upgrade.instructions.toArray()) + .filter(instruction -> instruction.getOpcode() == Opcodes.ARETURN).count(); + assertEquals("Review each native Forge 52 chunk-upgrade return", 2, returns); + assertEquals(returns, calls(upgrade, POWER, "finalizeLegacyChunk", "(" + NBT + ")" + NBT)); + assertEquals(returns, calls(upgrade, METALS, "finalizeLegacyChunk", "(" + NBT + ")" + NBT)); assertEquals(1, calls(upgrade, POWER, "prepareLegacyEntities", "(" + NBT + ")V")); assertEquals(1, calls(read, POWER, "copyChunkReadForMigration", "(Ljava/util/concurrent/CompletableFuture;)Ljava/util/concurrent/CompletableFuture;")); diff --git a/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryMappingEventTest.java b/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryMappingEventTest.java index fae60cd4..e18bcdf9 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryMappingEventTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryMappingEventTest.java @@ -11,8 +11,8 @@ import static org.junit.Assert.*; public class VanillaRegistryMappingEventTest { - private static final ResourceLocation SOURCE = new ResourceLocation("minecraft:zombie_pigman_spawn_egg"); - private static final ResourceLocation TARGET = new ResourceLocation("minecraft:zombified_piglin_spawn_egg"); + private static final ResourceLocation SOURCE = ResourceLocation.parse("minecraft:zombie_pigman_spawn_egg"); + private static final ResourceLocation TARGET = ResourceLocation.parse("minecraft:zombified_piglin_spawn_egg"); @Test public void resolvesTheTargetFromActiveNotTheIncompleteStagingRegistry() { Entry target = new Entry(); @@ -40,7 +40,7 @@ public class VanillaRegistryMappingEventTest { @Test public void leavesUnrecognizedEntriesUnresolvedForForgeToReport() { IForgeRegistry active = registry(new Entry(), true); MissingMappingsEvent.Mapping mapping = new MissingMappingsEvent.Mapping<>(active, active, - new ResourceLocation("othermod:zombie_pigman_spawn_egg"), 42); + ResourceLocation.parse("othermod:zombie_pigman_spawn_egg"), 42); VanillaRegistryRemaps.remap(event(active, mapping), active); assertEquals(MissingMappingsEvent.Action.DEFAULT, property(mapping, "action")); } @@ -60,7 +60,7 @@ public class VanillaRegistryMappingEventTest { IForgeRegistry active = registry(null, false, "minecraft:memory_module_type"); for (String name : new String[] {"golem_last_seen_time", "opened_doors"}) { MissingMappingsEvent.Mapping mapping = new MissingMappingsEvent.Mapping<>(active, active, - new ResourceLocation("minecraft", name), 42); + ResourceLocation.fromNamespaceAndPath("minecraft", name), 42); MissingMappingsEvent event = new MissingMappingsEvent(key("minecraft:memory_module_type"), active, Collections.singletonList(mapping)); VanillaRegistryRemaps.remap(event, active); @@ -70,17 +70,17 @@ public class VanillaRegistryMappingEventTest { } @Test public void neverRetiresOtherModsOrCurrentMemoryTypes() { - ResourceLocation registry = new ResourceLocation("minecraft:memory_module_type"); - assertFalse(VanillaRegistryRemaps.retiredTransientMemory(registry, new ResourceLocation("othermod:opened_doors"))); - assertFalse(VanillaRegistryRemaps.retiredTransientMemory(registry, new ResourceLocation("minecraft:golem_detected_recently"))); - assertFalse(VanillaRegistryRemaps.retiredTransientMemory(new ResourceLocation("minecraft:item"), - new ResourceLocation("minecraft:opened_doors"))); + ResourceLocation registry = ResourceLocation.parse("minecraft:memory_module_type"); + assertFalse(VanillaRegistryRemaps.retiredTransientMemory(registry, ResourceLocation.parse("othermod:opened_doors"))); + assertFalse(VanillaRegistryRemaps.retiredTransientMemory(registry, ResourceLocation.parse("minecraft:golem_detected_recently"))); + assertFalse(VanillaRegistryRemaps.retiredTransientMemory(ResourceLocation.parse("minecraft:item"), + ResourceLocation.parse("minecraft:opened_doors"))); } @Test public void retiresOnlyTheTwoPointsOfInterestRemovedByVanilla() { IForgeRegistry active = registry(null, false, "minecraft:point_of_interest_type"); for (String id : new String[] {"minecraft:nitwit", "minecraft:unemployed", "minecraft:home", "othermod:nitwit"}) { - var mapping = new MissingMappingsEvent.Mapping<>(active, active, new ResourceLocation(id), 42); + var mapping = new MissingMappingsEvent.Mapping<>(active, active, ResourceLocation.parse(id), 42); VanillaRegistryRemaps.remap(event(active, mapping), active); boolean retired = id.equals("minecraft:nitwit") || id.equals("minecraft:unemployed"); assertEquals(id, retired ? MissingMappingsEvent.Action.IGNORE : MissingMappingsEvent.Action.DEFAULT, @@ -100,12 +100,23 @@ public class VanillaRegistryMappingEventTest { } } + @Test public void movedPaintingsRetireOnlyTheirKnownStaticIndexes() { + IForgeRegistry active = registry(null, false, "minecraft:painting_variant"); + for (String id : new String[] {"minecraft:alban", "minecraft:aztec", "minecraft:unknown", "othermod:alban"}) { + var mapping = new MissingMappingsEvent.Mapping<>(active, active, ResourceLocation.parse(id), 42); + VanillaRegistryRemaps.remap(event(active, mapping), active); + assertEquals(id, id.equals("minecraft:alban") || id.equals("minecraft:aztec") + ? MissingMappingsEvent.Action.IGNORE : MissingMappingsEvent.Action.DEFAULT, property(mapping, "action")); + assertNull(property(mapping, "target")); + } + } + private static MissingMappingsEvent event(IForgeRegistry staging, MissingMappingsEvent.Mapping mapping) { return new MissingMappingsEvent(staging.getRegistryKey(), staging, Collections.singletonList(mapping)); } private static ResourceKey> key(String id) { - return ResourceKey.createRegistryKey(new ResourceLocation(id)); + return ResourceKey.createRegistryKey(ResourceLocation.parse(id)); } // Forge keeps the result fields package-private for its snapshot loader. @@ -129,7 +140,7 @@ private static IForgeRegistry registry(Entry target, boolean contains, St new Class[] {IForgeRegistry.class}, (proxy, method, args) -> { switch (method.getName()) { case "getRegistryKey": return key(id); - case "getRegistryName": return new ResourceLocation(id); + case "getRegistryName": return ResourceLocation.parse(id); case "containsKey": return contains && TARGET.equals(args[0]); case "getValue": return target; case "getKey": return args[0] == target ? TARGET : null; diff --git a/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemapsTest.java b/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemapsTest.java index aab126b2..7ec556d0 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemapsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemapsTest.java @@ -12,13 +12,28 @@ import static org.junit.Assert.*; public class VanillaRegistryRemapsTest { + @org.junit.Test public void trialSpawnerRenamesResolveToActualNativeSounds() { + net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); + var registry = net.minecraft.resources.ResourceLocation.parse("minecraft:sound_event"); + for (String[] pair : new String[][] { + {"block.trial_spawner.ambient_charged", "block.trial_spawner.ambient_ominous"}, + {"block.trial_spawner.charge_activate", "block.trial_spawner.ominous_activate"}}) { + var actual = VanillaRegistryRemaps.replacement(registry, + net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("minecraft", pair[0])); + assertEquals(net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("minecraft", pair[1]), actual); + assertTrue(net.minecraftforge.registries.ForgeRegistries.SOUND_EVENTS.containsKey(actual)); + } + assertNull(VanillaRegistryRemaps.replacement(registry, + net.minecraft.resources.ResourceLocation.parse("othermod:block.trial_spawner.charge_activate"))); + } + @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 retainsVanillaBlockItemAndEnchantmentRenamesThroughForge50() { + @Test public void retainsVanillaBlockItemAndEnchantmentRenamesThroughForge52() { expect("block", "grass", "short_grass"); expect("item", "grass", "short_grass"); expect("item", "scute", "turtle_scute"); @@ -88,7 +103,7 @@ public class VanillaRegistryRemapsTest { assertEquals(id("warm_ocean"), VanillaRegistryRemaps.vanillaBiomeFixTarget(biomes, id("deep_warm_ocean"))); assertNull(VanillaRegistryRemaps.vanillaBiomeFixTarget(biomes, id("plains"))); assertNull(VanillaRegistryRemaps.vanillaBiomeFixTarget(biomes, id("unknown_biome"))); - assertNull(VanillaRegistryRemaps.vanillaBiomeFixTarget(biomes, new ResourceLocation("example:desert_hills"))); + assertNull(VanillaRegistryRemaps.vanillaBiomeFixTarget(biomes, ResourceLocation.parse("example:desert_hills"))); assertNull(VanillaRegistryRemaps.vanillaBiomeFixTarget(id("block"), id("desert_hills"))); } @@ -106,7 +121,7 @@ private static void assertBiomeFix(String from, String to) { } @Test public void leavesOtherModsAndUnrecognizedVanillaEntriesAlone() { - assertNull(VanillaRegistryRemaps.replacement(id("item"), new ResourceLocation("othermod:zombie_pigman_spawn_egg"))); + assertNull(VanillaRegistryRemaps.replacement(id("item"), ResourceLocation.parse("othermod:zombie_pigman_spawn_egg"))); assertNull(VanillaRegistryRemaps.replacement(id("item"), id("unknown_item"))); assertNull(VanillaRegistryRemaps.replacement(id("sound_event"), id("music.unknown"))); } @@ -120,5 +135,5 @@ private static void expect(String registry, String from, String to) { assertEquals(id(to), VanillaRegistryRemaps.replacement(id(registry), id(from))); } - private static ResourceLocation id(String name) { return new ResourceLocation("minecraft", name); } + private static ResourceLocation id(String name) { return ResourceLocation.fromNamespaceAndPath("minecraft", name); } } diff --git a/src/test/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPackTest.java b/src/test/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPackTest.java index 492e85fa..d36c6e05 100644 --- a/src/test/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPackTest.java +++ b/src/test/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPackTest.java @@ -41,24 +41,24 @@ public class MaterialPolicyPackTest { @Test public void policyOnlySuppliesServerDataAndHonoursResourceDiscovery() { MaterialPolicyPack pack = new MaterialPolicyPack(true, false); - ResourceLocation rubber = new ResourceLocation("forge", "tags/items/rubber.json"); + ResourceLocation rubber = ResourceLocation.fromNamespaceAndPath("forge", "tags/item/rubber.json"); assertNotNull(pack.getResource(PackType.SERVER_DATA, rubber)); assertNull(pack.getResource(PackType.CLIENT_RESOURCES, rubber)); - assertEquals(Set.of(rubber, new ResourceLocation("forge:tags/items/ingots/rubber.json"), - new ResourceLocation("forge:tags/items/solder.json")), list(pack, PackType.SERVER_DATA, "forge")); + assertEquals(Set.of(rubber, ResourceLocation.parse("forge:tags/item/ingots/rubber.json"), + ResourceLocation.parse("forge:tags/item/solder.json")), list(pack, PackType.SERVER_DATA, "forge")); assertTrue(list(pack, PackType.SERVER_DATA, "basepower").isEmpty()); assertTrue(list(pack, PackType.CLIENT_RESOURCES, "forge").isEmpty()); } private static Set list(MaterialPolicyPack pack, PackType type, String namespace) { Set entries = new HashSet<>(); - pack.listResources(type, namespace, "tags/items", (id, supplier) -> entries.add(id)); + pack.listResources(type, namespace, "tags/item", (id, supplier) -> entries.add(id)); return entries; } 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")).get()) { + ResourceLocation.fromNamespaceAndPath("forge", "tags/item/" + path + ".json")).get()) { return new JsonParser().parse(new String(stream.readAllBytes(), StandardCharsets.UTF_8)).getAsJsonObject(); } } diff --git a/src/test/java/zone/moddev/mc/basepower/recipe/MaterialTagsTest.java b/src/test/java/zone/moddev/mc/basepower/recipe/MaterialTagsTest.java index 5dfe0aa0..d3ec729a 100644 --- a/src/test/java/zone/moddev/mc/basepower/recipe/MaterialTagsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/recipe/MaterialTagsTest.java @@ -35,7 +35,7 @@ public class MaterialTagsTest { } private static Set resolve(MaterialPolicyPack pack, String path, String userEntries) throws Exception { - var resource = new ResourceLocation("forge", "tags/items/" + path + ".json"); + var resource = ResourceLocation.fromNamespaceAndPath("forge", "tags/item/" + path + ".json"); JsonObject policy; try (var reader = new InputStreamReader(pack.getResource(PackType.SERVER_DATA, resource).get(), StandardCharsets.UTF_8)) { diff --git a/src/test/java/zone/moddev/mc/basepower/resources/CreativeTabInitializationTest.java b/src/test/java/zone/moddev/mc/basepower/resources/CreativeTabInitializationTest.java index af7943fc..76933538 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/CreativeTabInitializationTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/CreativeTabInitializationTest.java @@ -40,7 +40,7 @@ public class CreativeTabInitializationTest { event.newInstance(Registries.CREATIVE_MODE_TAB, null, registry)); ItemGroups.show(Items.IRON_INGOT); ItemGroups.show(Items.STICK); - var id = new ResourceLocation("basepower:machines"); + var id = ResourceLocation.parse("basepower:machines"); assertEquals(Set.of(id), registry.keySet()); var tab = ItemGroups.tab_powerAdvantage; assertSame(tab, registry.get(id)); diff --git a/src/test/java/zone/moddev/mc/basepower/resources/NativeLootContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativeLootContractTest.java index 3c52aa1f..1efa0062 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/NativeLootContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativeLootContractTest.java @@ -16,7 +16,7 @@ public class NativeLootContractTest { 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/basepower/loot_tables/blocks").resolve(id)); + JsonObject table = read(ROOT.resolve("data/basepower/loot_table/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,11 +25,11 @@ public class NativeLootContractTest { } @Test public void portableTankCopiesTheExistingItemKeysAndTerminalReturnsAPlainPipe() throws Exception { - JsonObject table = read(ROOT.resolve("data/basepower/loot_tables/blocks/fluid_storage_tank.json")); + JsonObject table = read(ROOT.resolve("data/basepower/loot_table/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/basepower/loot_tables/blocks/fluid_pipe_terminal.json")); + JsonObject pipe = read(ROOT.resolve("data/basepower/loot_table/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/NativeTagReaderContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativeTagReaderContractTest.java index a57a27ce..f78867cf 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/NativeTagReaderContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativeTagReaderContractTest.java @@ -23,11 +23,11 @@ public class NativeTagReaderContractTest { @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(); + "src/main/resources/data/forge/tags/item/bars/steel.json")), StandardCharsets.UTF_8)).getAsJsonObject(); assertFalse("Do not silently ignore an obsolete optional array", json.has("optional")); assertTrue(NativeTestTags.resolve(id -> null, id -> null, json).isEmpty()); var present = NativeTestTags.resolve(id -> - id.equals(new ResourceLocation("basemetals:steel_bars")) ? Items.IRON_BARS : null, id -> null, json); + id.equals(ResourceLocation.parse("basemetals:steel_bars")) ? Items.IRON_BARS : null, id -> null, json); assertEquals(java.util.Set.of(Items.IRON_BARS), present); } diff --git a/src/test/java/zone/moddev/mc/basepower/resources/RecipeUnlockContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/RecipeUnlockContractTest.java index cd1dab0a..a57b11ca 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/RecipeUnlockContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/RecipeUnlockContractTest.java @@ -28,20 +28,20 @@ public class RecipeUnlockContractTest { @Test public void everyCraftingRecipeHasExactlyOneUnlockAndProcessingHasNone() throws IOException { Set expected = new LinkedHashSet<>(); - for (Path file : files(DATA.resolve("recipes"))) { + for (Path file : files(DATA.resolve("recipe"))) { if (crafting(read(file))) expected.add(file.getFileName().toString()); } - Set actual = files(DATA.resolve("advancements/recipes")).stream() + Set actual = files(DATA.resolve("advancement/recipes")).stream() .map(file -> file.getFileName().toString()).collect(Collectors.toSet()); assertFalse("No crafting recipes found", expected.isEmpty()); assertEquals("Missing or orphaned recipe unlocks", expected, actual); } @Test public void discoveriesAreSingleIngredientAlternativesNotAnEntireCraftingGrid() throws IOException { - for (Path file : files(DATA.resolve("recipes"))) { + for (Path file : files(DATA.resolve("recipe"))) { JsonObject recipe = read(file); if (!crafting(recipe)) continue; - JsonObject unlock = read(DATA.resolve("advancements/recipes").resolve(file.getFileName())); + JsonObject unlock = read(DATA.resolve("advancement/recipes").resolve(file.getFileName())); assertEquals("minecraft:recipes/root", unlock.get("parent").getAsString()); assertFalse("Recipe unlocks should not create visible advancement tabs", unlock.has("display")); JsonObject criteria = unlock.getAsJsonObject("criteria"); @@ -74,7 +74,7 @@ public class RecipeUnlockContractTest { } @Test public void rewardAndRecipeCommandCriterionReferToTheExactVariant() throws IOException { - for (Path file : files(DATA.resolve("advancements/recipes"))) { + for (Path file : files(DATA.resolve("advancement/recipes"))) { String id = MOD + ":" + file.getFileName().toString().replace(".json", ""); JsonObject unlock = read(file); JsonArray recipes = unlock.getAsJsonObject("rewards").getAsJsonArray("recipes"); @@ -88,7 +88,7 @@ public class RecipeUnlockContractTest { } @Test public void recipeBookGroupsIdentifyTheOutputNotABroadMachineCategory() throws IOException { - for (Path file : files(DATA.resolve("recipes"))) { + for (Path file : files(DATA.resolve("recipe"))) { JsonObject recipe = read(file); if (!crafting(recipe)) continue; JsonElement result = recipe.get("result"); @@ -126,7 +126,7 @@ private static String unlockPredicateKey(JsonObject predicate) { @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")) + JsonObject criterion = read(DATA.resolve("advancement/recipes/starch_from_potato.json")) .getAsJsonObject("criteria").getAsJsonObject("has_ingredient_1"); JsonObject predicate = criterion.getAsJsonObject("conditions").getAsJsonArray("items").get(0).getAsJsonObject(); var registries = net.minecraft.core.RegistryAccess.fromRegistryOfRegistries(net.minecraft.core.registries.BuiltInRegistries.REGISTRY); diff --git a/src/test/java/zone/moddev/mc/basepower/resources/StarchRecipeTest.java b/src/test/java/zone/moddev/mc/basepower/resources/StarchRecipeTest.java index 66f6e2de..087c79cc 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/StarchRecipeTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/StarchRecipeTest.java @@ -33,7 +33,7 @@ public class StarchRecipeTest { private static JsonObject read(String name) throws Exception { return new JsonParser().parse(new String(Files.readAllBytes(Paths.get( - "src/main/resources/data/basepower/recipes/" + name + ".json")), StandardCharsets.UTF_8)).getAsJsonObject(); + "src/main/resources/data/basepower/recipe/" + 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/basepower/util/MachineCapabilitiesLifecycleTest.java b/src/test/java/zone/moddev/mc/basepower/util/MachineCapabilitiesLifecycleTest.java index abb38278..a4c4c2b5 100644 --- a/src/test/java/zone/moddev/mc/basepower/util/MachineCapabilitiesLifecycleTest.java +++ b/src/test/java/zone/moddev/mc/basepower/util/MachineCapabilitiesLifecycleTest.java @@ -6,6 +6,10 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.Tag; import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.common.capabilities.ForgeCapabilities; @@ -16,14 +20,28 @@ import org.junit.BeforeClass; import org.junit.Test; import zone.moddev.mc.basepower.machines.fluidmachines.MetalTankTileEntity; +import zone.moddev.mc.basepower.machines.conveyors.TileEntityConveyor; +import zone.moddev.mc.basepower.init.Entities; import static org.junit.Assert.*; public class MachineCapabilitiesLifecycleTest { + private static BlockEntityType previousTank; + private static BlockEntityType previousConveyor; + @BeforeClass public static void nativeCapabilityTokens() throws Exception { net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); assertNotNull(ForgeCapabilities.FLUID_HANDLER); assertNotNull(ForgeCapabilities.ITEM_HANDLER); assertNotNull(ForgeCapabilities.ENERGY); + previousTank = Entities.METAL_TANK; + previousConveyor = Entities.CONVEYOR; + Entities.METAL_TANK = BlockEntityType.Builder.of(MetalTankTileEntity::new, Blocks.IRON_BLOCK).build(null); + Entities.CONVEYOR = BlockEntityType.Builder.of(TileEntityConveyor::new, Blocks.HOPPER).build(null); + } + + @AfterClass public static void restoreNativeTypes() { + Entities.METAL_TANK = previousTank; + Entities.CONVEYOR = previousConveyor; } @Test public void chunkUnloadCannotRecreateAdaptersOnAnUnremovedTile() { @@ -138,7 +156,12 @@ private static final class TestTank extends MetalTankTileEntity { } private static final class TestConverter extends cyano.poweradvantage.api.modsupport.rf.TileEntityRFConverter { - 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"); } + private static final BlockEntityType TYPE = + BlockEntityType.Builder.of(TestConverter::new, Blocks.IRON_BLOCK).build(null); + TestConverter() { this(BlockPos.ZERO, Blocks.IRON_BLOCK.defaultBlockState()); } + TestConverter(BlockPos pos, BlockState state) { + super(TYPE, pos, state, new cyano.poweradvantage.api.ConduitType("test-power"), 100, 1000, "test"); + } void reviveNow() { reviveCaps(); } } } diff --git a/test-rig/README.md b/test-rig/README.md index 3af98db2..5a562498 100644 --- a/test-rig/README.md +++ b/test-rig/README.md @@ -1,4 +1,15 @@ -# Advantage Works 1.18.2 Test Rig +# Advantage Works 1.21.1 Test Rig + +The current target is Minecraft 1.21.1, Forge 52.1.0 and Java 21. Start with +the [native test instructions](profiles/native1211/README.md) and the +[port record](../docs/1.21.1/PORT-STATUS.md). Use only the current hash-pinned +inputs in disposable worlds. Earlier results do not qualify a rebuilt jar. + +The rig checks real machine operation, saved state, migration, recipes, +partial network unloading, moving drills and endurance. Client rendering +checks are separate. Manual gameplay and visual acceptance remain required. + +## Retained 1.18.2 Reference Use Java 17, Forge 40.3.0 and the exact jars in [`profiles/native118/`](profiles/native118/). These profiles exercise diff --git a/test-rig/build.gradle b/test-rig/build.gradle index a2ba6656..b5bfae6e 100644 --- a/test-rig/build.gradle +++ b/test-rig/build.gradle @@ -5,8 +5,8 @@ plugins { id 'net.minecraftforge.gradle' version '7.0.34' } -ext.forge50MavenizerDefault = '../../../../MinecraftMineralogy/OS1206/OreSpawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar' -apply from: '../gradle/forge50-mavenizer.gradle' +ext.forge52MavenizerDefault = '../../../../MinecraftMineralogy/OS1211/OreSpawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar' +apply from: '../gradle/forge52-mavenizer.gradle' // Saves, pinned inputs and evidence are siblings, never Gradle cleanup targets. layout.buildDirectory = layout.projectDirectory.dir('build/gradle') @@ -70,7 +70,7 @@ repositories { } def powerCompileJar = file(project.findProperty('powerAdvantageReleaseJar') - ?: '../build/libs/basePower-2.4.13.120061.jar') + ?: '../build/libs/basePower-2.4.14.121011.jar') def exploratoryPowerSha = providers.gradleProperty('worksExploratoryPowerSha256') def powerCompileSha = exploratoryPowerSha.orElse(providers.gradleProperty('works_power_sha256')) @@ -106,7 +106,7 @@ tasks.register('verifyWorksCompileInputPin') { inputs.file(powerCompileJar) inputs.property('powerSha256', powerCompileSha.orElse('')) doLast { - if (!powerCompileJar.isFile()) throw new GradleException("Missing 1.20.6 Power compile input: ${powerCompileJar}") + if (!powerCompileJar.isFile()) throw new GradleException("Missing 1.21.1 Power compile input: ${powerCompileJar}") if (!(powerCompileSha.getOrElse('') ==~ /[A-Fa-f0-9]{64}/)) { throw new GradleException('Missing or invalid Power SHA-256 pin') } @@ -196,7 +196,7 @@ def registerWorksRunner = { String taskName, String profileName, boolean automat if (!automated) standardInput = System.in doFirst { JavaExec task -> String pin = providers.gradleProperty('worksProfileFile').orNull - if (!pin) throw new GradleException("Generate the pinned ${profileName} profile with migration/native1206.py profile and set -PworksProfileFile") + if (!pin) throw new GradleException("Generate the pinned ${profileName} profile with migration/native1211.py profile and set -PworksProfileFile") File profile = file(pin) if (new groovy.json.JsonSlurper().parse(profile).name != profileName) { throw new GradleException("Expected profile ${profileName}: ${profile}") @@ -218,7 +218,7 @@ def registerWorksRunner = { String taskName, String profileName, boolean automat } registerWorksRunner('runWorksDev', 'dev-core', false, - 'Builds and launches a disposable interactive 1.20.6 Advantage Works development server.') + 'Builds and launches a disposable interactive 1.21.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, @@ -242,15 +242,15 @@ registerWorksRunner('verifyWorksWorldgenLegacyImport', 'worldgen-legacy-import', registerWorksRunner('runWorksPackagedCurrent', 'packaged-current', false, 'Launches the complete packaged works on localhost for manual inspection.') -tasks.register('verifyWorksNative1206Inputs', JavaExec) { +tasks.register('verifyWorksNative1211Inputs', JavaExec) { group = 'verification' - description = 'Checks 1.20.6 candidate identities and hashes without creating or opening a world.' + description = 'Checks 1.21.1 candidate identities and hashes without creating or opening a world.' dependsOn tasks.named('runnerClasses'), tasks.named('jar') classpath = sourceSets.runner.runtimeClasspath mainClass = 'zone.moddev.mc.advantageworks.runner.WorksRunner' doFirst { JavaExec task -> String pin = providers.gradleProperty('worksProfileFile').orNull - if (!pin) throw new GradleException('Set -PworksProfileFile to a generated Forge 50 profile') + if (!pin) throw new GradleException('Set -PworksProfileFile to a generated Forge 52 profile') args '--profile', file(pin).absolutePath, '--validate-only' configurePreparedSeedArgs(task) } @@ -287,11 +287,11 @@ def compileUnitSupport = tasks.register('compileUnitSupport', JavaCompile) { classpath = nativeControlClasspath destinationDirectory = layout.buildDirectory.dir('unit-support/classes') } -tasks.register('verifyForge50UnitBootstrap', JavaExec) { +tasks.register('verifyForge52UnitBootstrap', JavaExec) { group = 'verification' dependsOn compileUnitSupport, unitBootstrapJar classpath = files(compileUnitSupport.flatMap { it.destinationDirectory }) + nativeControlClasspath - mainClass = 'zone.moddev.mc.advantageworks.unit.Forge50UnitControl' + mainClass = 'zone.moddev.mc.advantageworks.unit.Forge52UnitControl' javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(21) } doFirst { jvmArgs "-javaagent:${unitBootstrapJar.get().archiveFile.get().asFile}" } } diff --git a/test-rig/client-resources/README.md b/test-rig/client-resources/README.md index b476b629..0331a8f3 100644 --- a/test-rig/client-resources/README.md +++ b/test-rig/client-resources/README.md @@ -1,26 +1,26 @@ # Native Client Resource Check -This disposable Minecraft 1.20.6 / Forge 50.2.0 client checks models, textures, +This disposable Minecraft 1.21.1 / Forge 52.1.0 client checks models, textures, translations, recipe-book cells and machine screens after Minecraft loads them. It registers no gameplay content, opens no world and closes when finished. The probe is excluded from release jars and ordinary Eclipse launches. -See [port status](../../docs/1.20.6/PORT-STATUS.md) for current qualification. +See [port status](../../docs/1.21.1/PORT-STATUS.md) for current qualification. Earlier-version results do not qualify this version. Use Java 21 for Gradle, compilation and the client and server runners. The -target is official 1.20.6 mappings with MCP `20240429.135109`. +target is official 1.21.1 mappings with MCP `20240808.132146`. ## Build And Run ```powershell ./gradlew.bat check clientResourceProbeJar verifyEclipseProductionClasspath prepareCombinedEclipseLaunch ./test-rig/client-resources/test-launcher.ps1 -./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -MachineGuiLayouts -ChildRendererGeometry -RecipeBookFixtures '/unlock' -PreflightOnly -./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -MachineGuiLayouts -ChildRendererGeometry -RecipeBookFixtures '/unlock' +./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -MachineGuiLayouts -ChildRendererGeometry -RecipeBookFixtures '/unlock' -PreflightOnly +./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -MachineGuiLayouts -ChildRendererGeometry -RecipeBookFixtures '/unlock' ``` -The combined launch requires Power `2.4.13.120061`, Steam `2.2.14.120061` and -Electric `2.2.14.120061`. Pin the exact rebuilt jars in `profiles/native1206` +The combined launch requires Power `2.4.14.121011`, Steam `2.2.15.121011` and +Electric `2.2.15.121011`. Pin the exact rebuilt jars in `profiles/native1211` before regenerating it. Omitting the combined profile retains Power-only scope. Normal Eclipse launches contain production code only. @@ -39,10 +39,10 @@ is provenance, not proof that the selected loose output equals that jar. ## Recipe Fixtures -Generate fresh exports from the packaged 1.20.6 candidates, not a historical run: +Generate fresh exports from the packaged 1.21.1 candidates, not a historical run: ```powershell - -B test-rig/migration/native1206_recipes.py --profile '' --seed '' --inventory '' --inventory-sha256 '' --java '/bin/java.exe' --port 25577 --output test-rig/build/native1206-recipes-new + -B test-rig/migration/native1211_recipes.py --profile '' --seed '' --inventory '' --inventory-sha256 '' --java '/bin/java.exe' --port 25577 --output test-rig/build/native1211-recipes-new ``` Use an existing Python 3.11+ environment with `migration/requirements.txt`. 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 972310d8..2bce5b5b 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 @@ -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.ENTITY_TYPES.getValue(new ResourceLocation("baseelectric:hydroturbine")); + EntityType type = ForgeRegistries.ENTITY_TYPES.getValue(ResourceLocation.parse("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("baseelectric:textures/entity/hydroturbine.png").equals(texture), "Wrong hydroturbine texture"); + require(ResourceLocation.parse("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."); @@ -175,7 +175,7 @@ && close(min(vertices, 1), 0.5F) && close(max(vertices, 1), 1F) } private static BlockEntity tile(String id) { - BlockEntityType type = ForgeRegistries.BLOCK_ENTITY_TYPES.getValue(new ResourceLocation(id)); + BlockEntityType type = ForgeRegistries.BLOCK_ENTITY_TYPES.getValue(ResourceLocation.parse(id)); require(type != null, "Missing registered tile type " + id); net.minecraft.world.level.block.state.BlockState state = ForgeRegistries.BLOCKS.getValues().stream() .map(net.minecraft.world.level.block.Block::defaultBlockState).filter(type::isValid).findFirst() @@ -195,7 +195,7 @@ private static List render(Minecraft minecraft, BlockEntity tile, Strin PoseStack.Pose original = matrices.last(); org.joml.Matrix4f position = new org.joml.Matrix4f(original.pose()); org.joml.Matrix3f normal = new org.joml.Matrix3f(original.normal()); - Capture buffers = new Capture(minecraft, new ResourceLocation(expectedTexture), row); + Capture buffers = new Capture(minecraft, ResourceLocation.parse(expectedTexture), row); boolean balanced; Throwable renderFailure = null; try { @@ -226,6 +226,7 @@ private static final class Capture implements MultiBufferSource { private final ResourceLocation expectedTexture; private final JsonArray batches = new JsonArray(); private final Map buffers = new LinkedHashMap<>(); + private final List storage = new ArrayList<>(); private final List vertices = new ArrayList<>(); Capture(Minecraft minecraft, ResourceLocation expectedTexture, JsonObject row) { @@ -239,17 +240,19 @@ private static final class Capture implements MultiBufferSource { BufferBuilder buffer = buffers.get(type); if (buffer == null) { require(buffers.size() < 8, "Renderer requested too many buffers"); - buffer = new BufferBuilder(4096); - buffer.begin(type.mode(), type.format()); + var allocation = new com.mojang.blaze3d.vertex.ByteBufferBuilder(4096); + storage.add(allocation); + buffer = new BufferBuilder(allocation, type.mode(), type.format()); buffers.put(type, buffer); } return buffer; } void finish() throws Exception { + try { for (Map.Entry entry : buffers.entrySet()) { BufferBuilder buffer = entry.getValue(); - BufferBuilder.RenderedBuffer built = buffer.end(); + try (var built = buffer.buildOrThrow()) { int count = built.drawState().vertexCount(); JsonObject batch = new JsonObject(); batches.add(batch); @@ -285,8 +288,9 @@ void finish() throws Exception { batch.addProperty("texture", selectedTexture.toString()); require(expectedTexture.equals(selectedTexture), "Production renderer selected the wrong texture"); texture(minecraft, selectedTexture, batch); - built.release(); + } } + } finally { storage.forEach(com.mojang.blaze3d.vertex.ByteBufferBuilder::close); } } } @@ -360,7 +364,7 @@ private static void set(Object object, String name, Object value) throws Reflect private static int offset(VertexFormat format, VertexFormatElement.Usage usage, int index) { for (int i = 0; i < format.getElements().size(); i++) { VertexFormatElement element = format.getElements().get(i); - if (element.getUsage() == usage && element.getIndex() == index) return format.getOffset(i); + if (element.usage() == usage && element.index() == index) return format.getOffset(i); } throw new IllegalStateException("Native vertex format lacks " + usage + " " + index); } 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 9eb8b018..a7d21f87 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 @@ -47,8 +47,8 @@ public final class ClientResourceProbe { try { result.addProperty("minecraft", net.minecraftforge.versions.mcp.MCPVersion.getMCVersion()); result.addProperty("forge", net.minecraftforge.versions.forge.ForgeVersion.getVersion()); - require("1.20.6".equals(result.get("minecraft").getAsString()) - && "50.2.0".equals(result.get("forge").getAsString()), "Wrong native client target"); + require("1.21.1".equals(result.get("minecraft").getAsString()) + && "52.1.0".equals(result.get("forge").getAsString()), "Wrong native client target"); require(minecraft.level == null && minecraft.player == null && !minecraft.hasSingleplayerServer(), "Resource probe must not open a world or connect to a server"); verifyNetworkBootstrap(result); @@ -67,9 +67,9 @@ public final class ClientResourceProbe { result.add("mod_versions", versions); require(versions.has("basepower") && zone.moddev.mc.basepower.PowerAdvantage.VERSION .equals(versions.get("basepower").getAsString()), "Power metadata does not match the compiled version"); - require("2.4.13.120061".equals(versions.get("basepower").getAsString()), "Wrong native Power candidate"); + require("2.4.14.121011".equals(versions.get("basepower").getAsString()), "Wrong native Power candidate"); for (String id : new String[]{"basesteam", "baseelectric"}) { - if (versions.has(id)) require("2.2.14.120061".equals(versions.get(id).getAsString()), "Wrong native expansion candidate: " + id); + if (versions.has(id)) require("2.2.15.121011".equals(versions.get(id).getAsString()), "Wrong native expansion candidate: " + id); } require(minecraft.getModelManager().getMissingModel() != null, "Timed out before model baking"); require(minecraft.getOverlay() == null, "Timed out before native resource loading completed"); diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java index 9987ce9a..03cc4dc0 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 @@ -105,13 +105,13 @@ private static boolean check(Minecraft minecraft, FluidState state, String textu return 0x3F76E4; // Vanilla plains water tint for this read-only witness. } }; - BufferBuilder buffer = new BufferBuilder(4096); - buffer.begin(com.mojang.blaze3d.vertex.VertexFormat.Mode.QUADS, DefaultVertexFormat.BLOCK); + try (var storage = new com.mojang.blaze3d.vertex.ByteBufferBuilder(4096)) { + BufferBuilder buffer = new BufferBuilder(storage, com.mojang.blaze3d.vertex.VertexFormat.Mode.QUADS, DefaultVertexFormat.BLOCK); minecraft.getBlockRenderer().renderLiquid( BlockPos.ZERO, world, buffer, world.getBlockState(BlockPos.ZERO), state); - BufferBuilder.RenderedBuffer built = buffer.end(); + try (var built = buffer.buildOrThrow()) { int vertices = built.drawState().vertexCount(); - TextureAtlasSprite sprite = minecraft.getTextureAtlas(TextureAtlas.LOCATION_BLOCKS).apply(new ResourceLocation(texture)); + TextureAtlasSprite sprite = minecraft.getTextureAtlas(TextureAtlas.LOCATION_BLOCKS).apply(ResourceLocation.parse(texture)); require(vertices >= 4, "Native fluid renderer emitted no top face for " + state); ByteBuffer data = built.vertexBuffer(); int uvOffset = elementOffset(VertexFormatElement.Usage.UV, 0); @@ -130,14 +130,15 @@ private static boolean check(Minecraft minecraft, FluidState state, String textu entry.addProperty("vertex_rgb", String.format("%06X", rgb)); entry.addProperty("vertices", vertices); cases.add(entry); - built.release(); return correctSprite && correctTint; + } + } } private static int elementOffset(VertexFormatElement.Usage usage, int index) { 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); + if (element.usage() == usage && element.index() == index) return DefaultVertexFormat.BLOCK.getOffset(i); } throw new IllegalStateException("Native block vertex format lacks " + usage + " " + index); } 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 7d5e163d..353ada2b 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 @@ -62,15 +62,15 @@ static void verify(Minecraft minecraft, JsonObject report) { // Import only the generated server fixture's ID snapshot, never a user world. Map registry = RegistryManager.ACTIVE.takeSnapshot(false); CompoundTag level = NbtIo.readCompressed(root.resolve("server-registries.dat"), NbtAccounter.unlimitedHeap()); - require("1.20.6".equals(level.getCompound("Data").getCompound("Version").getString("Name")), - "Recipe fixture is not a native 1.20.6 server export"); + require("1.21.1".equals(level.getCompound("Data").getCompound("Version").getString("Name")), + "Recipe fixture is not a native 1.21.1 server export"); ListTag mods = level.getCompound("fml").getList("LoadingModList", Tag.TAG_COMPOUND); boolean nativeForge = false; for (int i = 0; i < mods.size(); i++) { CompoundTag mod = mods.getCompound(i); - if ("forge".equals(mod.getString("ModId"))) nativeForge = "50.2.0".equals(mod.getString("ModVersion")); + if ("forge".equals(mod.getString("ModId"))) nativeForge = "52.1.0".equals(mod.getString("ModVersion")); } - require(nativeForge, "Recipe fixture was not saved by Forge 50.2.0"); + require(nativeForge, "Recipe fixture was not saved by Forge 52.1.0"); CompoundTag saved = level.getCompound("fml").getCompound("Registries"); String itemRegistry = ForgeRegistries.ITEMS.getRegistryName().toString(); String blockRegistry = ForgeRegistries.BLOCKS.getRegistryName().toString(); diff --git a/test-rig/client-resources/resources/META-INF/mods.toml b/test-rig/client-resources/resources/META-INF/mods.toml index 224f4b95..d6f0ca8d 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="[50,51)" +loaderVersion="[52,53)" license="GPL-3.0-only" [[mods]] @@ -11,20 +11,20 @@ description='''Build-only model, texture and translation checks. Never distribut [[dependencies.advantageclientresources]] modId="basepower" mandatory=true -versionRange="[2.4.13.120061]" +versionRange="[2.4.14.121011]" ordering="AFTER" side="CLIENT" [[dependencies.advantageclientresources]] modId="forge" mandatory=true -versionRange="[50.2.0]" +versionRange="[52.1.0]" ordering="NONE" side="CLIENT" [[dependencies.advantageclientresources]] modId="minecraft" mandatory=true -versionRange="[1.20.6]" +versionRange="[1.21.1]" ordering="NONE" side="CLIENT" diff --git a/test-rig/client-resources/run.ps1 b/test-rig/client-resources/run.ps1 index 011d1ca4..54886e09 100644 --- a/test-rig/client-resources/run.ps1 +++ b/test-rig/client-resources/run.ps1 @@ -30,19 +30,19 @@ 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]*\.120061"[ \t]*(?:#[^\r\n]*)?\r?$' -or - $Metadata -notmatch '(?m)^loaderVersion\s*=\s*"\[50,') { throw "Not a native Forge 50 candidate: $Id" } - $expected = @{ basepower = '2.4.13.120061'; basesteam = '2.2.14.120061'; baseelectric = '2.2.14.120061' } + $header -notmatch '(?m)^version\s*=\s*"[^"\r\n]*\.121011"[ \t]*(?:#[^\r\n]*)?\r?$' -or + $Metadata -notmatch '(?m)^loaderVersion\s*=\s*"\[52,') { throw "Not a native Forge 52 candidate: $Id" } + $expected = @{ basepower = '2.4.14.121011'; basesteam = '2.2.15.121011'; baseelectric = '2.2.15.121011' } 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.20.6' -or $Environment.FORGE_VERSION -ne '50.2.0' -or - $Environment.MCP_MAPPINGS -ne 'official_1.20.6' -or $Environment.MCP_VERSION -ne '20240429.135109' -or - $Environment.target -ne 'fmluserdevclient') { throw 'Regenerate the native 1.20.6 / Forge 50.2.0 official client launch.' } + if ($Environment.MC_VERSION -ne '1.21.1' -or $Environment.FORGE_VERSION -ne '52.1.0' -or + $Environment.MCP_MAPPINGS -ne 'official_1.21.1' -or $Environment.MCP_VERSION -ne '20240808.132146' -or + $Environment.target -ne 'fmluserdevclient') { throw 'Regenerate the native 1.21.1 / Forge 52.1.0 official client launch.' } $program = $Attributes['org.eclipse.jdt.launching.PROGRAM_ARGUMENTS'] - if ($program -notmatch '1\.20\.6-50\.2\.0' -or + if ($program -notmatch '1\.21\.1-52\.1\.0' -or $program -notmatch '--main net\.minecraftforge\.bootstrap\.ForgeBootstrap\b' -or $program -notmatch '--launchTarget forge_userdev_client\b' -or $program -match '1\.(?:13\.2|14\.4|15\.2|16\.5|17\.1|18\.2|19\.4|20\.1)|--(?:server|port|world)\b' -or @@ -60,7 +60,7 @@ function Quote-Argument([string]$Value) { } $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.20.6'; forge = '50.2.0'; mappings = 'official_1.20.6'; mcp = '20240429.135109'; qualification = 'UNQUALIFIED'; output_source = $OutputSource; launch_profile = $LaunchProfile; require_extensions = [bool]$RequireExtensions; child_renderer_geometry = [bool]$ChildRendererGeometry } +$inputReport = [ordered]@{ minecraft = '1.21.1'; forge = '52.1.0'; mappings = 'official_1.21.1'; mcp = '20240808.132146'; 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 @@ -76,8 +76,8 @@ $inputReport.java = Get-Record $java $inputReport.launch = Get-Record (Join-Path $project $LaunchProfile) $inputReport.probe = Get-Record $probe $probeMetadata = Read-JarMetadata $probe -if ($probeMetadata -notmatch 'loaderVersion="\[50,51\)"' -or $probeMetadata -notmatch 'versionRange="\[1\.20\.6\]"' -or - $probeMetadata -notmatch 'versionRange="\[50\.2\.0\]"' -or $probeMetadata -notmatch 'versionRange="\[2\.4\.13\.120061\]"') { throw 'Rebuild the native 1.20.6 probe; do not substitute a legacy probe.' } +if ($probeMetadata -notmatch 'loaderVersion="\[52,53\)"' -or $probeMetadata -notmatch 'versionRange="\[1\.21\.1\]"' -or + $probeMetadata -notmatch 'versionRange="\[52\.1\.0\]"' -or $probeMetadata -notmatch 'versionRange="\[2\.4\.14\.121011\]"') { throw 'Rebuild the native 1.21.1 probe; do not substitute a legacy probe.' } Copy-Item -LiteralPath (Join-Path $project $LaunchProfile) -Destination (Join-Path $runtime 'launch.xml') $environment = @{} foreach ($map in $launch.launchConfiguration.mapAttribute) { @@ -108,7 +108,7 @@ $outputFiles = @(Get-ChildItem -LiteralPath $modOutput -Recurse -File | Sort-Obj if (@($outputFiles | Where-Object { $_.path -like '*.class' }).Count -eq 0) { throw 'Selected output has no compiled production classes.' } $outputFiles | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $runtime 'production-files.json') -Encoding utf8 $inputReport.production_files = Get-Record (Join-Path $runtime 'production-files.json') -if (-not $PowerCandidate) { $PowerCandidate = Join-Path $project 'build/libs/basePower-2.4.13.120061.jar' } +if (-not $PowerCandidate) { $PowerCandidate = Join-Path $project 'build/libs/basePower-2.4.14.121011.jar' } $PowerCandidate = (Resolve-Path -LiteralPath $PowerCandidate).Path Assert-NativeMetadata (Read-JarMetadata $PowerCandidate) 'basepower' $inputReport.power_candidate = Get-Record $PowerCandidate @@ -174,7 +174,7 @@ foreach ($archive in $archives) { $archives = $launchArchives $inputReport.archives = $archiveRecords $inputReport.runtime_candidates = $runtimeCandidates -if (-not ($archives -match 'forge-1\.20\.6-50\.2\.0\.jar$')) { throw 'Missing actual native mapped Forge runtime.' } +if (-not ($archives -match 'forge-1\.21\.1-52\.1\.0\.jar$')) { throw 'Missing actual native mapped Forge runtime.' } if ($RequireExtensions -and (-not $runtimeCandidates.ContainsKey('basesteam') -or -not $runtimeCandidates.ContainsKey('baseelectric'))) { throw 'Combined launch must explicitly contain both native expansion candidates.' } $arguments = @([regex]::Matches($attributes['org.eclipse.jdt.launching.PROGRAM_ARGUMENTS'], '"[^"]*"|\S+') | ForEach-Object { $_.Value.Trim('"') }) $vmArguments = @([regex]::Matches($attributes['org.eclipse.jdt.launching.VM_ARGUMENTS'], '"[^"]*"|\S+') | ForEach-Object { $_.Value.Trim('"') }) @@ -214,7 +214,7 @@ $resultPath = Join-Path $runtime 'client-resources.json' if (-not (Test-Path -LiteralPath $resultPath)) { throw "Client did not produce a result; inspect $runtime/logs" } $result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json if ($result.status -ne 'PASS') { throw "Client resource check failed: $($result.error); inspect $runtime" } -if ($result.minecraft -ne '1.20.6' -or $result.forge -ne '50.2.0') { throw 'Result is not native 1.20.6 / Forge 50.2.0 evidence.' } +if ($result.minecraft -ne '1.21.1' -or $result.forge -ne '52.1.0') { throw 'Result is not native 1.21.1 / Forge 52.1.0 evidence.' } if ($result.tank_gauge_pixel_status -ne 'PASS' -or @($result.tank_gauge_pixels).Count -ne 26) { throw 'Full fluid-gauge pixel checks did not pass; a single-pixel probe is not acceptable.' } diff --git a/test-rig/client-resources/test-launcher.ps1 b/test-rig/client-resources/test-launcher.ps1 index f8e06339..586030d2 100644 --- a/test-rig/client-resources/test-launcher.ps1 +++ b/test-rig/client-resources/test-launcher.ps1 @@ -76,33 +76,33 @@ if ($CrossVersionEvidenceRoots) { $native = @' modLoader="javafml" -loaderVersion="[50,)" # native Forge +loaderVersion="[52,)" # native Forge [[mods]] # mandatory header modId="basepower" # inline comment -version="2.4.13.120061" # native candidate +version="2.4.14.121011" # native candidate [[dependencies.basepower]] modId="minecraft" mandatory=true -versionRange="[1.20.6]" +versionRange="[1.21.1]" '@ Assert-NativeMetadata $native 'basepower' $checks++ -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.13.120061','2.4.4.113021')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.13.120061','2.4.5.114041')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.13.120061','2.4.5.115021')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('[50,)','[45,)')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.13.120061','2.4.7.116051')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.13.120061','${version}')) 'basepower' } +Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.14.121011','2.4.4.113021')) 'basepower' } +Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.14.121011','2.4.5.114041')) 'basepower' } +Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.14.121011','2.4.5.115021')) 'basepower' } +Assert-Throws { Assert-NativeMetadata ($native.Replace('[52,)','[50,)')) 'basepower' } +Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.14.121011','2.4.7.116051')) 'basepower' } +Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.14.121011','${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.13.120061','2.2.14.120061') + $expansion = $native.Replace('basepower', $id).Replace('2.4.14.121011','2.2.15.121011') Assert-NativeMetadata $expansion $id $checks++ - Assert-Throws { Assert-NativeMetadata ($expansion.Replace('2.2.14.120061','2.2.6.115021')) $id } + Assert-Throws { Assert-NativeMetadata ($expansion.Replace('2.2.15.121011','2.2.6.115021')) $id } } -$environment = @{ MC_VERSION = '1.20.6'; FORGE_VERSION = '50.2.0'; MCP_MAPPINGS = 'official_1.20.6'; MCP_VERSION = '20240429.135109'; target = 'fmluserdevclient' } -$attributes = @{ 'org.eclipse.jdt.launching.PROGRAM_ARGUMENTS' = '--main net.minecraftforge.bootstrap.ForgeBootstrap --cache D:\native\1.20.6-50.2.0 -- --launchTarget forge_userdev_client --gameDir .'; 'org.eclipse.jdt.launching.MAIN_TYPE' = 'net.minecraftforge.launcher.Main' } +$environment = @{ MC_VERSION = '1.21.1'; FORGE_VERSION = '52.1.0'; MCP_MAPPINGS = 'official_1.21.1'; MCP_VERSION = '20240808.132146'; target = 'fmluserdevclient' } +$attributes = @{ 'org.eclipse.jdt.launching.PROGRAM_ARGUMENTS' = '--main net.minecraftforge.bootstrap.ForgeBootstrap --cache D:\native\1.21.1-52.1.0 -- --launchTarget forge_userdev_client --gameDir .'; 'org.eclipse.jdt.launching.MAIN_TYPE' = 'net.minecraftforge.launcher.Main' } Assert-NativeLaunch $environment $attributes $checks++ foreach ($key in 'MC_VERSION','FORGE_VERSION','MCP_MAPPINGS','MCP_VERSION','target') { @@ -118,7 +118,7 @@ foreach ($argument in '--server localhost','--port 25565','--gameDir D:\protecte $invalid = $attributes.Clone() $invalid['org.eclipse.jdt.launching.MAIN_TYPE'] = 'net.minecraft.client.main.Main' Assert-Throws { Assert-NativeLaunch $environment $invalid } -Assert-NativeArchive 'D:\native\forge-1.20.6-50.2.0.jar' +Assert-NativeArchive 'D:\native\forge-1.21.1-52.1.0.jar' $checks++ foreach ($path in 'D:\PA114\client.jar','D:\PA113\client.jar','D:\PA116\client.jar','D:\native\forge-1.16.5-36.2.34.jar','D:\native\forge-1.14.4-28.2.26.jar','D:\native\client-extra-1.13.2.jar') { Assert-Throws { Assert-NativeArchive $path } @@ -135,7 +135,7 @@ if ($StaticOnly) { return } $project = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path -$power = Join-Path $project 'build/libs/basePower-2.4.13.120061.jar' +$power = Join-Path $project 'build/libs/basePower-2.4.14.121011.jar' Assert-NativeMetadata (Read-JarMetadata $power) 'basepower' $checks++ [xml]$launch = Get-Content -LiteralPath (Join-Path $project 'runClientAllAdvantage.launch') -Raw diff --git a/test-rig/gradle.properties b/test-rig/gradle.properties index 3be35f94..1920ad42 100644 --- a/test-rig/gradle.properties +++ b/test-rig/gradle.properties @@ -1,10 +1,10 @@ org.gradle.jvmargs=-Xmx2G org.gradle.daemon=false -minecraft_version=1.20.6 -forge_version=50.2.0 +minecraft_version=1.21.1 +forge_version=52.1.0 mapping_channel=official -mapping_version=1.20.6 -mcp_version=20240429.135109 -harness_version=1.1.4.120061 -works_power_sha256=E53BDD64776741B2EBAE36A646049DC98487E1BAD1CF69F1E3FCD95188F907F1 +mapping_version=1.21.1 +mcp_version=20240808.132146 +harness_version=1.1.5.121011 +works_power_sha256=F91A613EB5FAA218AAD2DC3C881DEC7B897A5EE874215282A439E34B915C1AE6 diff --git a/test-rig/migration/audit_native1211_endurance.py b/test-rig/migration/audit_native1211_endurance.py new file mode 100644 index 00000000..7b63f640 --- /dev/null +++ b/test-rig/migration/audit_native1211_endurance.py @@ -0,0 +1,34 @@ +"""Independently replay each saved checkpoint from the full Forge 52 endurance run.""" + +import endurance +import native1211 +from audit_native1201_endurance import main +from native117_broad import read_json +from native1211_endurance import measure_batch + + +class Forge52: + MINECRAFT, FORGE = native1211.MINECRAFT, native1211.FORGE + verify_runtime = staticmethod(native1211.audit_staged) + parse_snapshot = staticmethod(native1211.parse_endurance_snapshot) + + @staticmethod + def require_native_boot(path): + native1211.require_clean_log(path) + text = path.read_text(encoding="utf-8") + for marker in (f"Starting minecraft server version {native1211.MINECRAFT}", + f"Forge mod loading, version {native1211.FORGE}, for MC {native1211.MINECRAFT} " + f"with MCP {native1211.MCP}", "Done ("): + endurance.require(marker in text, f"Missing native startup witness in {path.name}: {marker}") + + @staticmethod + def verify_batch(runtime, label, documents, definitions): + stations = ("S-04", "E-03", "W-04", "W-03") + measured = measure_batch({station: documents[station] for station in stations}, definitions) + recorded = read_json(runtime / f"semantic-batch-{label}.json") + endurance.require(measured["status"] == "PASS" and measured == recorded, + f"{runtime.name}/{label}: replay disagrees with saved item and fluid observations") + + +if __name__ == "__main__": + main(adapter=Forge52) diff --git a/test-rig/migration/native117_machine_operations.py b/test-rig/migration/native117_machine_operations.py index 08ebe480..1158e7a7 100644 --- a/test-rig/migration/native117_machine_operations.py +++ b/test-rig/migration/native117_machine_operations.py @@ -118,15 +118,19 @@ def saved_actor_nbt(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"] + for field, identity in (("Attributes", "Name"), ("attributes", "id")): + if field not in entity: + continue + if "Attributes" in entity and "attributes" in entity: + raise AssertionError("Ambiguous native actor attribute representations") + attributes = entity[field] if not isinstance(attributes, nbtlib.List) or any( - not isinstance(row, nbtlib.Compound) or not isinstance(row.get("Name"), nbtlib.String) + not isinstance(row, nbtlib.Compound) or not isinstance(row.get(identity), nbtlib.String) for row in attributes): raise AssertionError("Malformed native actor attributes") - names = [str(row["Name"]) for row in attributes] + names = [str(row[identity]) 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"]))) + entity[field] = nbtlib.List[nbtlib.Compound](sorted(attributes, key=lambda row: str(row[identity]))) for passenger in entity.get("Passengers", []): visit(passenger) visit(value) return typed_nbt(value) diff --git a/test-rig/migration/native117_recipes.py b/test-rig/migration/native117_recipes.py index fb9b9d4c..4902ed53 100644 --- a/test-rig/migration/native117_recipes.py +++ b/test-rig/migration/native117_recipes.py @@ -41,13 +41,15 @@ def read_json(path): return json.loads(path.read_text(encoding="utf-8-sig")) -def recipe_fixture(jars, *, component_predicates=False): +def recipe_fixture(jars, *, component_predicates=False, singular_directories=False): recipes = [] sources = {} for path in jars: with zipfile.ZipFile(path) as jar: for mod in MOD_COUNTS: - prefix = f"data/{mod}/recipes/" + recipe_directory = "recipe" if singular_directories else "recipes" + advancement_directory = "advancement" if singular_directories else "advancements" + prefix = f"data/{mod}/{recipe_directory}/" 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: @@ -55,7 +57,7 @@ def recipe_fixture(jars, *, component_predicates=False): if recipe["type"] not in RECIPE_TYPES: continue stem = Path(name).stem - advancement_name = f"data/{mod}/advancements/recipes/{stem}.json" + advancement_name = f"data/{mod}/{advancement_directory}/recipes/{stem}.json" advancement = json.loads(jar.read(advancement_name)) predicates = {} for key, criterion in advancement["criteria"].items(): @@ -286,7 +288,8 @@ def progression_campaign(root, target, entry, java, port, report, *, engine=None write_json(runtime / "progression-fixture.json", definition) pack = runtime / WORLD / "datapacks/progression-override" write_json(pack / "pack.mcmeta", {"pack": {"pack_format": pack_format, "description": "Disposable loot-override witness"}}) - write_json(pack / "data/minecraft/loot_tables/chests/simple_dungeon.json", { + loot_directory = "loot_tables" if engine is None else getattr(engine, "loot_directory", "loot_tables") + write_json(pack / f"data/minecraft/{loot_directory}/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): diff --git a/test-rig/migration/native1211.py b/test-rig/migration/native1211.py new file mode 100644 index 00000000..9f2a4766 --- /dev/null +++ b/test-rig/migration/native1211.py @@ -0,0 +1,380 @@ +"""Run the Forge 52 Power/Base Metals interop witnesses in a fresh disposable world.""" + +import argparse +import base64 +import gzip +import io +import json +import os +from pathlib import Path +import re +import subprocess +import traceback +from zipfile import ZipFile + +import nbtlib + +from matrix import digest, fingerprint, stage as stage_server, write_json +from native117 import jar_identity, output_path, require_disjoint +from native117_code import NativeLaunch, code_files +from native117_interop import mark_runtime, require_checks, require_probe_order +from native117_readiness import await_ready +from native1201 import FLAT_SETTINGS, require_clean_log + + +RIG = Path(__file__).resolve().parents[1] +MINECRAFT = "1.21.1" +FORGE = "52.1.0" +MCP = "20240808.132146" +FORGE_VERSION = f"{MINECRAFT}-{FORGE}" +SHIM = f"forge-{FORGE_VERSION}-shim.jar" +SHIM_SHA = "F7B706C79896728723DA0E4E8C5A6ECDB6A617AD4DF9D7EA1DDC6367187BC291" +LAUNCHER = f"libraries/net/minecraftforge/forge/{FORGE_VERSION}/" + ( + "win_args.txt" if os.name == "nt" else "unix_args.txt") +INVENTORY = "native1211-code-inventory.json" +EXPLORATORY = "EXPLORATORY_UNQUALIFIED" +BASE_PROFILE = RIG / "profiles/native1211/power-interop.json" + + +def read_json(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def compat_tile_nbt(row): + if "nbtBase64" in row: + data = nbtlib.File.parse(io.BytesIO(gzip.decompress(base64.b64decode(row["nbtBase64"], validate=True)))) + else: + data = nbtlib.parse_nbt(row.get("nbt", "{}")) + # Project component data for existing inventory oracles without discarding + # the original component maps that their full-stack comparisons also use. + for stack in data.get("Items", []): + count = stack.get("count") + if not isinstance(count, (nbtlib.Byte, nbtlib.Short, nbtlib.Int, nbtlib.Long)) or "Count" in stack or "tag" in stack: + raise AssertionError(f"Expected a native 1.21.1 stack in checkpoint: {stack}") + stack["Count"] = count + custom = stack.get("components", {}).get("minecraft:custom_data") + if custom is not None: + stack["tag"] = custom + return data + + +def compat_tile_data(row): + return compat_tile_nbt(row).unpack() + + +def parse_endurance_snapshot(document): + return {tuple(row["pos"]): {**row, "data": compat_tile_data(row)} for row in document["snapshot"]} + + +def audit_staged(runtime): + expected = {entry["file"]: entry["sha256"] for entry in read_json(runtime / "input-jars.json")} + verify_runtime(runtime, read_json(runtime / INVENTORY), expected) + + +def validate_hash(path, expected): + if not re.fullmatch(r"[A-Fa-f0-9]{64}", expected) or digest(path) != expected.upper(): + raise AssertionError(f"Wrong or missing artifact hash: {path}") + + +def java21(executable): + java = executable.resolve(strict=True) + result = subprocess.run([str(java), "-version"], capture_output=True, text=True, check=True, timeout=30) + version = (result.stderr or result.stdout).strip() + if not re.search(r'version "21(?:\.|")', version): + raise ValueError(f"Forge 52 test requires Java 21: {version}") + return java, version + + +def inspect_seed(seed): + validate_hash(seed / SHIM, SHIM_SHA) + command = (seed / LAUNCHER).read_text(encoding="utf-8").split() + if command != ["-Djava.net.preferIPv6Addresses=system", "-jar", SHIM]: + raise AssertionError("Forge 52 launcher is not the official shim command") + return {"forge": FORGE_VERSION, "files": code_files(seed)} + + +def seal(seed, inventory): + require_disjoint(inventory, seed) + if inventory.exists() or not inventory.is_relative_to(RIG / "build"): + raise ValueError("Inventory must be a new path under test-rig/build") + write_json(inventory, inspect_seed(seed)) + + +def read_mods(args): + data = read_json(args.profile) + if (data.get("mode"), data.get("minecraftVersion"), data.get("forgeVersion"), + data.get("qualificationStatus")) != ("packaged", MINECRAFT, FORGE, EXPLORATORY): + raise ValueError("Use the exploratory Forge 52 packaged profile") + mods = { + "basepower": ("2.4.14.121011", args.power, args.power_sha256), + "advantageworkstest": ("1.1.5.121011", args.harness, args.harness_sha256), + } + for entry in data["mods"]: + identity = entry["id"] + if identity in mods: + raise ValueError(f"Duplicate mod identity: {identity}") + mods[identity] = (entry["version"], (RIG / entry["path"]).resolve(), entry["sha256"]) + if set(mods) != {"basepower", "advantageworkstest", "basemetals", "orespawn"}: + raise ValueError("Power interop requires exactly Power, Base Metals, OreSpawn, and the harness") + for identity, (version, path, sha) in mods.items(): + if not version.endswith(".121011") or not path.name.endswith(f"-{version}.jar"): + raise ValueError(f"Expected an ordinary Forge 52 jar: {path}") + validate_hash(path, sha) + jar_identity(path, identity, version) + verify_compile_pin(args.harness, args.power_sha256) + return mods + + +def verify_compile_pin(harness, power_sha256): + with ZipFile(harness) as jar: + manifest = dict(line.split(": ", 1) for line in + jar.read("META-INF/MANIFEST.MF").decode().replace("\r\n ", "").splitlines() + if ": " in line) + if (manifest.get("Built-On") != FORGE_VERSION + or manifest.get("Power-Input-Status") != EXPLORATORY + or manifest.get("Power-Input-SHA256") != power_sha256.upper()): + raise AssertionError("Harness compile pin and staged ordinary Power jar differ") + + +def read_family_target(profile, seed, inventory_file, inventory_sha256): + validate_hash(inventory_file, inventory_sha256) + inventory = read_json(inventory_file) + if inspect_seed(seed) != inventory: + raise AssertionError("Official Forge server differs from its sealed code inventory") + data = read_json(profile) + if (data.get("mode"), data.get("minecraftVersion"), data.get("forge", {}).get("version"), + data.get("qualificationStatus")) != ("packaged", MINECRAFT, FORGE_VERSION, EXPLORATORY): + raise ValueError("Expected a materialized Forge 52 packaged profile") + mods = {} + for item in data["mods"]: + identity = item["id"] + path = (RIG / item["path"]).resolve(strict=True) + version = item["version"] + if identity in mods or not version.endswith(".121011") or not path.name.endswith(f"-{version}.jar") or path.name != item["file"]: + raise ValueError(f"Invalid ordinary jar pin: {identity}") + validate_hash(path, item["sha256"]) + jar_identity(path, identity, version) + mods[identity] = (version, path, item["sha256"]) + if not {"basepower", "advantageworkstest", "basesteam", "baseelectric", "orespawn"} <= mods.keys(): + raise ValueError("A full family profile must pin Power, Steam, Electric, OreSpawn and Advantage Works") + verify_compile_pin(mods["advantageworkstest"][1], mods["basepower"][2]) + return seed, inventory, mods + + +def materialize_profile(args): + runtime_profile = args.output.resolve() + if not runtime_profile.is_relative_to(RIG / "build") or runtime_profile.exists(): + raise ValueError("Write the pinned profile to a new file under test-rig/build") + mods = read_mods(args) + if args.name == "power-interop": + base = {"name": args.name, "levelType": "minecraft:flat", "buildWorks": False, "lifecycleStations": []} + origin = args.profile + required = set(mods) + else: + if not re.fullmatch(r"[a-z0-9-]+", args.name): + raise ValueError("Invalid profile name") + origin = RIG / "profiles/native1201" / (args.name + ".json") + base = read_json(origin) + template_version = base.get("minecraftVersion", base.get("forge", {}).get("version", "").split("-")[0]) + if (base.get("name") != args.name or template_version != "1.20.1" + or base.get("forge", {}).get("version") != "1.20.1-47.4.10"): + raise ValueError("Profile template must be a named native1201 profile") + required = {entry["id"] for entry in base["mods"]} | {"basepower", "orespawn"} + for identity, path, sha in (("basesteam", args.steam, args.steam_sha256), + ("baseelectric", args.electric, args.electric_sha256), + ("mineralogy", args.mineralogy, args.mineralogy_sha256)): + if identity not in required: + continue + if path is None or sha is None: + raise ValueError(f"Need an ordinary 1.21.1 jar and exact SHA-256 for {identity}") + version = path.stem.rsplit("-", 1)[-1] + if not version.endswith(".121011"): + raise ValueError(f"Wrong Minecraft/loader target for {identity}: {version}") + validate_hash(path, sha) + jar_identity(path, identity, version) + mods[identity] = (version, path, sha) + for legacy in ("powerDeobfSha256", "sourceCommit"): + base.pop(legacy, None) + base.update(mode="packaged", runtimeBase="build/works-native1211", minecraftVersion=MINECRAFT, + forge={"version": FORGE_VERSION}, portStatus="PORTED_UNQUALIFIED", + qualificationStatus=EXPLORATORY, sourceVersion=mods["basepower"][0], + sourceProfileSha256=digest(origin), mods=[ + dict(id=key, version=version, path=str(path.resolve()), file=path.name, sha256=sha.upper()) + for key, (version, path, sha) in mods.items() if key in required + ]) + if {entry["id"] for entry in base["mods"]} != required: + raise ValueError("Profile template references unpinned mods") + write_json(runtime_profile, base) + + +def verify_runtime(runtime, inventory, expected): + if inspect_seed(runtime) != inventory: + raise AssertionError("Staged Forge code changed") + if {p.name: digest(p) for p in (runtime / "mods").glob("*.jar")} != expected: + raise AssertionError("Staged mod jars changed") + + +def stage(runtime, seed, inventory, mods, port): + stage_server(runtime, (seed, LAUNCHER, [mod[1] for mod in mods.values()])) + expected = {path.name: sha.upper() for _, path, sha in mods.values()} + write_json(runtime / INVENTORY, inventory) + settings = runtime / "server.properties" + overrides = { + "level-type": "minecraft:flat", "generator-settings": json.dumps(FLAT_SETTINGS, separators=(",", ":")), + "server-port": port, "generate-structures": "false", "simulation-distance": 4, "allow-nether": "true", + } + existing = [line for line in settings.read_text().splitlines() if line.partition("=")[0] not in overrides] + settings.write_text("\n".join(existing + [f"{key}={value}" for key, value in overrides.items()]) + "\n") + config = runtime / "config/fml.toml" + config.parent.mkdir() + config.write_text("versionCheck = false\n", encoding="utf-8") + mark_runtime(runtime) + verify_runtime(runtime, inventory, expected) + return expected + + +class Server(NativeLaunch): + forge_version = FORGE_VERSION + + def validate_native_log(self, path): + diagnostics = require_clean_log(path) + if diagnostics: + write_json(path.with_suffix(".platform-diagnostics.json"), diagnostics) + + def close(self): + super().close() + console = (self.native_runtime / f"console-{self.native_label}.log").read_text(encoding="utf-8") + for marker in (f"Starting minecraft server version {MINECRAFT}", + f"Forge mod loading, version {FORGE}, for MC {MINECRAFT} with MCP {MCP}", "Done ("): + if marker not in console: + raise AssertionError(f"Missing Forge 52 startup witness: {marker}") + + +def require_shared_crushing(document): + checks = document.get("sharedChecks", []) + if len(checks) != 1 or checks[0].get("id") != "basemetals/native-power-crushing" or checks[0].get("status") != "PASS": + raise AssertionError(f"Missing or failed Base Metals shared crushing witness: {checks}") + + +def require_oil_buckets(document, readiness): + require_probe_order(document, readiness) + expected = {f"{oil}/{check}" for oil in ("crude_oil", "refined_oil") + for check in ("world-roundtrip", "machine-roundtrip", "machine-below-bucket", + "machine-insufficient-room", "machine-full")} + checks = document.get("checks", []) + if document.get("gameMode") != "survival" or len(checks) != len(expected) or {row["id"] for row in checks} != expected: + raise AssertionError("Incomplete survival oil-bucket witness set") + failures = [row for row in checks if row.get("status") != "PASS"] + if failures: + raise AssertionError(f"Survival oil bucket failures: {[(row['id'], row.get('failure')) for row in failures]}") + + +def interop(args): + if not 1024 <= args.port <= 65535: + raise ValueError("An unprivileged localhost port is required") + runtime = output_path(args.output) + for path in (args.profile, args.seed, args.inventory, args.java, args.harness, args.power): + require_disjoint(runtime, path) + validate_hash(args.inventory, args.inventory_sha256) + inventory = read_json(args.inventory) + if inspect_seed(args.seed) != inventory: + raise AssertionError("Official Forge server differs from its sealed code inventory") + mods = read_mods(args) + for _, path, _ in mods.values(): + require_disjoint(runtime, path) + java, java_version = java21(args.java) + protected = fingerprint(args.seed) + expected = stage(runtime, args.seed, inventory, mods, args.port) + if args.oil_buckets: + (runtime / "advantage-native-oil-bucket.allow").touch() + receipt = runtime / "native1211-interop-results.json" + report = dict(status="RUNNING", qualificationStatus=EXPLORATORY, phases={}, minecraft=MINECRAFT, forge=FORGE, + profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), + javaSha256=digest(java), javaVersion=java_version, jars=expected) + write_json(receipt, report) + try: + for phase in ("exercise", "restored"): + verify_runtime(runtime, inventory, expected) + server = Server(runtime, java, LAUNCHER, phase) + try: + server.send("forceload add 1023 1023 1100 1030") + ready = await_ready(server, runtime, (63, 63, 68, 64), f"interop-{phase}", minecraft=MINECRAFT) + server.send(f"advinterop {phase}") + server.wait(f"ADVINTEROP COMPLETE {phase}", 120) + document = read_json(runtime / "advantage-works-results" / f"native-interop-{phase}.json") + report["phases"][phase] = dict(readiness=ready, observation=document) + require_probe_order(document, ready) + require_checks(document, phase) + require_shared_crushing(document) + if args.oil_buckets and phase == "exercise": + server.send("advoilbuckets") + server.wait("ADVOILBUCKETS COMPLETE", 120) + buckets = read_json(runtime / "advantage-works-results/native-oil-buckets.json") + report["phases"][phase]["oilBuckets"] = buckets + require_oil_buckets(buckets, ready) + server.send("save-all flush") + server.wait("Saved the game", 30) + except BaseException as failure: + try: + server.close() + except BaseException as shutdown: + failure.add_note(f"Shutdown/log audit also failed: {shutdown!r}") + raise + else: + server.close() + write_json(receipt, report) + report["status"] = "PASS" + except BaseException as failure: + report.update(status="FAIL", failure=repr(failure), traceback=traceback.format_exc()) + raise + finally: + try: + verify_runtime(runtime, inventory, expected) + report["seedUnchanged"] = fingerprint(args.seed) == protected + if not report["seedUnchanged"]: + raise AssertionError("Protected Forge seed changed during the test") + except Exception as audit: + report.update(status="FAIL", auditError=repr(audit)) + write_json(receipt, report) + return int(report["status"] != "PASS") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + for command in ("seal", "interop", "profile"): + sub = commands.add_parser(command) + if command in ("seal", "interop"): + sub.add_argument("--seed", type=Path, required=True) + sub.add_argument("--inventory", type=Path, required=True) + if command in ("interop", "profile"): + sub.add_argument("--profile", type=Path, default=BASE_PROFILE) + for key in ("output", "power", "harness"): + sub.add_argument("--" + key, type=Path, required=True) + for key in ("power", "harness"): + sub.add_argument("--" + key + "-sha256", required=True) + if command == "interop": + sub.add_argument("--java", type=Path, required=True) + sub.add_argument("--inventory-sha256", required=True) + sub.add_argument("--port", type=int, required=True) + sub.add_argument("--oil-buckets", action="store_true") + elif command == "profile": + sub.add_argument("--name", required=True) + for mod in ("steam", "electric", "mineralogy"): + sub.add_argument("--" + mod, type=Path) + sub.add_argument("--" + mod + "-sha256") + args = parser.parse_args() + if args.command == "profile": + materialize_profile(args) + return 0 + args.seed = args.seed.resolve(strict=True) + args.inventory = args.inventory.resolve() + if args.command == "seal": + seal(args.seed, args.inventory) + print(digest(args.inventory)) + return 0 + return interop(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native1211_actors.py b/test-rig/migration/native1211_actors.py new file mode 100644 index 00000000..0b5229c5 --- /dev/null +++ b/test-rig/migration/native1211_actors.py @@ -0,0 +1,379 @@ +"""Check sealed historical player and dimension fixtures on Forge 52.""" + +import argparse +import builtins +import copy +from functools import partial +from pathlib import Path +import shutil +import socket +import traceback +from types import SimpleNamespace + +import nbtlib + +import actors +import dimensions +from matrix import WORLD, digest, fingerprint, write_json +from native117 import output_path, require_disjoint +from native117_actor_io import bind, capture_actors, capture_dimensions +import native117_actors as historical +from native117_dimension_io import parse_dimension_nbt, project_entity +import native117_entity_storage as entity_storage +import native117_helper_contract as helper_contract +import native117_stopped_entities as stopped +import native1211 as native + + +DATA_VERSION = 3955 +ENCHANTMENTS = { + "Enchantments": "minecraft:enchantments", + "StoredEnchantments": "minecraft:stored_enchantments", +} + + +def expected_components(value): + """Convert the historical expectation, never normalize a native observation away.""" + if isinstance(value, list): + return [expected_components(item) for item in value] + if not isinstance(value, dict): + return value + if "id" not in value or "Count" not in value: + return {key: expected_components(item) for key, item in value.items()} + if type(value["Count"]) is not int or value["Count"] <= 0 or "count" in value or "components" in value: + raise AssertionError("Malformed or ambiguous historical stack") + result = {key: item for key, item in value.items() if key not in ("Count", "tag")} + result["count"] = value["Count"] + custom = dict(value.get("tag", {})) + if "BlockEntityTag" in custom: + raise AssertionError("This actor adapter does not translate nested block-entity stack data") + components = {} + if "Damage" in custom: + components["minecraft:damage"] = custom.pop("Damage") + for key, component in ENCHANTMENTS.items(): + if key in custom: + enchantments = custom.pop(key) + levels = {item["id"]: item["lvl"] for item in enchantments} + if len(levels) != len(enchantments) or any(not isinstance(key, str) for key in levels): + raise AssertionError("Historical enchantments must have unique registry names") + components[component] = {"levels": levels} + if custom: + components["minecraft:custom_data"] = custom + if components: + result["components"] = components + return result + + +def exact_items(expected, observed, path=""): + if isinstance(expected, dict) and isinstance(observed, dict): + if "id" in expected and "count" in expected: + if expected != observed: + return [dict(path=path + ".exact-item", expected=expected, actual=observed)] + else: + return [finding for key in sorted(expected.keys() & observed.keys()) + for finding in exact_items(expected[key], observed[key], f"{path}.{key}")] + elif isinstance(expected, list) and isinstance(observed, list): + return [finding for index, (before, after) in enumerate(zip(expected, observed)) + for finding in exact_items(before, after, f"{path}[{index}]")] + return [] + + +def baseline(source, kind): + state, label = historical.baseline(source, kind) + return expected_components(state), label + + +def leash_position(value): + if (len(value) != 3 or any(type(coordinate) is not int or not -(1 << 31) <= coordinate < (1 << 31) + for coordinate in value)): + raise ValueError("Expected the native leash knot's block-position int[3]") + return dict(zip(("X", "Y", "Z"), value)) + + +def dimension_entity_view(value): + if value.get("id") == "minecraft:horse": + if "leash" in value: + if "Leash" in value: + raise ValueError("Ambiguous saved horse leash") + value["Leash"] = leash_position(value.pop("leash")) + if "body_armor_item" in value: + if "ArmorItem" in value: + raise ValueError("Ambiguous saved horse armor") + value["ArmorItem"] = value.pop("body_armor_item") + for attribute in value.get("Attributes", ()): + if attribute["Name"] == "minecraft:generic.jump_strength": + if any(other["Name"] == "horse.jumpStrength" for other in value["Attributes"]): + raise ValueError("Ambiguous saved horse jump strength") + attribute["Name"] = "horse.jumpStrength" + for passenger in value.get("Passengers", ()): + dimension_entity_view(passenger) + return value + + +def native_dimension_nbt(entry): + parser = bind(parse_dimension_nbt, project_entity=lambda value, contexts: + project_entity(native_attribute_view(value), contexts)) + return dimension_entity_view(parser(entry)) + + +def native_attribute_view(value): + """Expose the new field spelling to the retained semantic oracle without changing values.""" + result = copy.deepcopy(value) + + def visit(entity): + if "attributes" in entity: + if "Attributes" in entity or not isinstance(entity["attributes"], list): + raise ValueError("Ambiguous native attribute list") + attributes, names = [], set() + for original in entity.pop("attributes"): + if not isinstance(original, dict) or not isinstance(original.get("id"), str): + raise ValueError("Malformed native attribute") + row = dict(original) + name = row.pop("id") + if name in names or "Name" in row or "Base" in row or "Modifiers" in row: + raise ValueError("Duplicate or ambiguous native attribute") + names.add(name) + row["Name"] = name + if "base" in row: + row["Base"] = row.pop("base") + if "modifiers" in row: + row["Modifiers"] = row.pop("modifiers") + attributes.append(row) + entity["Attributes"] = attributes + for passenger in entity.get("Passengers", ()): + visit(passenger) + visit(result) + return result + + +def capture_player_version(capture, runtime, label, evidence): + state = capture(runtime, label, evidence) + version = int(nbtlib.load(evidence / "player.dat").get("DataVersion", -1)) + if version != DATA_VERSION: + if label != "converted" or version >= DATA_VERSION: + raise AssertionError(f"{label}: native player file was not serialized as data version {DATA_VERSION}: {version}") + # The unopened loose file has not gone through PlayerDataStorage yet. + # Compare the retained source representation but keep its original file and version as evidence. + state["file"] = expected_components(state["file"]) + write_json(evidence / "player-format.json", dict(dataVersion=version, native=version == DATA_VERSION)) + return state + + +retain = bind(historical.retain, + capture_actors=partial(capture_player_version, capture_actors), + capture_dimensions=partial(capture_player_version, bind(capture_dimensions, parse_nbt=native_dimension_nbt))) +command = bind(historical.command, retain=retain) +release = bind(historical.release, command=command) + + +def helper_snapshot(record): + expected, rows, roots = entity_storage._snapshots(record) + for row in rows.values(): + if "leash" not in row: + continue + position = row["leash"] + if "Leash" in row or not isinstance(position, nbtlib.IntArray): + raise ValueError("Malformed or ambiguous native stopped leash") + if len(position) == 3: + row["Leash"] = nbtlib.Compound({key: nbtlib.Int(number) + for key, number in leash_position(list(map(int, position))).items()}) + elif len(position) != 4: + raise ValueError("Unexpected native stopped leash array length") + return expected, rows, roots + + +validate_helpers = bind(helper_contract.validate, native=SimpleNamespace( + _snapshots=helper_snapshot, _nbt=entity_storage._nbt, _decompress=entity_storage._decompress, + _identity=entity_storage._identity), CONTRACT_SHA="83990C70FA3F6AD0CDC2803CA956B6CF1227EA5B5701D95175874B7D9EEA0AB8", + POLICY="native1211-leash-knot-nosave-v1") +helper_adapter = SimpleNamespace(CONTRACT="native1211-helper-contract.json", LIFECYCLE=helper_contract.LIFECYCLE, + validate=validate_helpers, raw_helpers_absent=helper_contract.raw_helpers_absent) + + +def stop_audit_import(name, globals=None, locals=None, fromlist=(), level=0): + # Only this function's local helper import is adapted. The raw region audit + # still uses the original on-disk NBT and the module is never monkey-patched. + if level == 0 and name == "native117_helper_contract": + return helper_adapter + return builtins.__import__(name, globals, locals, fromlist, level) + + +audit_stopped = bind(stopped.audit_stopped, + __builtins__=dict(vars(builtins), __import__=stop_audit_import)) + + +def verdict(oracle, before, after): + result = oracle(before, after) + findings = exact_items(before, after) + result["differences"].extend(findings) + if findings: + result["status"] = "FAIL" + if "entity_status" in result and any(row["path"].startswith(".entities.") for row in findings): + result["entity_status"] = "FAIL" + if "player_status" in result and any(not row["path"].startswith(".entities.") for row in findings): + result["player_status"] = "FAIL" + return result + + +def assess_native(before, after, strict, proof, scope, adapter): + result = dict(status=strict["status"], rawStrictStatus=strict["status"], + rawStrictResult=copy.deepcopy(strict), nativeDifferences=[]) + if strict["status"] == "PASS" or proof is None: + return result + expected = copy.deepcopy(before) + wolf = expected.get("entities", {}).get("passenger-wolf", {}) + actual = after.get("entities", {}).get("passenger-wolf", {}) + maximum = wolf.get("Attributes", {}).get("generic.maxHealth", {}) + if (wolf.get("id") != "minecraft:wolf" or actual.get("id") != wolf["id"] + or wolf.get("Health") != 8 or maximum.get("Base") != 20 + or any(key not in wolf or actual.get(key) != wolf[key] + for key in ("UUIDMost", "UUIDLeast", "OwnerUUID"))): + return result + # These exact changes were independently reproduced by vanilla DFU and native + # Wolf load/save on a classpath that contains no Advantage classes. + controls = {} + for version, checksum in ((512, "39770FAE00166CFC3F107E1990D13699383D5C5797640DF60F9241455D8C40C7"), + (1343, "456850C8BC2F43931750A6CBAC4A8020C7007B10A70E51CB77DF0FD7464AA38E")): + path = Path(proof) / f"legacy-{version}-receipt.json" + native.validate_hash(path, checksum) + controls[str(version)] = dict(path=str(path), sha256=checksum, observation=native.read_json(path)) + wolf["Health"] = 16 + maximum["Base"] = 40 + checked = verdict(dimensions.verdict, expected, after) + if checked["status"] == "PASS": + result.update(status="PASS_WITH_NATIVE_DIFFERENCE", allOtherFieldsStrictMatch=True, + controlledProof=controls, nativeDifferences=copy.deepcopy(strict["differences"])) + return result + + +def summarize_native(checks, assessments): + failures, native_changes = [], [] + for key, check in checks.items(): + if check["status"] == "PASS": + continue + assessment = assessments.get(key) + if (assessment is None or assessment.get("rawStrictResult") != check + or assessment.get("status") != "PASS_WITH_NATIVE_DIFFERENCE" + or assessment.get("allOtherFieldsStrictMatch") is not True): + failures.append(key) + else: + native_changes.append(dict(check=key, assessment=assessment)) + return dict(status="FAIL" if not checks or failures else + "PASS_WITH_NATIVE_DIFFERENCE" if native_changes else "PASS", + rawStrictStatus="FAIL" if native_changes or failures else "PASS", + failures=failures, nativeDifferences=native_changes) + + +def copy_fixture(source, runtime): + shutil.copytree(source / WORLD, runtime / WORLD) + shutil.copy2(source / "migration-fixture.json", runtime / "migration-fixture.json") + config = source / "config" + if config.is_dir(): + for item in config.rglob("*"): + destination = runtime / "config" / item.relative_to(config) + if item.is_file() and destination.exists(): + raise FileExistsError(f"Retained fixture config conflicts with native runtime: {destination}") + shutil.copytree(config, runtime / "config", dirs_exist_ok=True) + + +run_route = bind(historical.run_route, baseline=baseline, command=command, release=release, + copy_fixture=copy_fixture, assess=assess_native, + summarize_checks=summarize_native, + native117_dimension_assessment=SimpleNamespace(__file__=__file__), + actors=SimpleNamespace(compare=partial(verdict, actors.compare), freeze_result=actors.freeze_result, + tick_result=actors.tick_result, backup_state=actors.backup_state), + dimensions=SimpleNamespace(verdict=partial(verdict, dimensions.verdict), + tick_result=dimensions.tick_result)) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("fixture", "fixture-receipt", "profile", "seed", "inventory", "java", "output"): + parser.add_argument("--" + name, type=lambda value: Path(value).resolve(), required=True) + parser.add_argument("--fixture-receipt-sha256", required=True) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--kind", choices=("actors", "dimensions"), required=True) + parser.add_argument("--native-wolf-proof", type=lambda value: Path(value).resolve(strict=True)) + parser.add_argument("--port", type=int, required=True) + args = parser.parse_args() + if not 26450 <= args.port <= 65535: + parser.error("Use a disposable localhost port at or above 26450") + + output = output_path(args.output) + source = args.fixture.resolve(strict=True) + seed, inventory, mods = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + java, version = native.java21(args.java) + for path in (source, args.fixture_receipt, args.profile, seed, args.inventory, java, + *(entry[1] for entry in mods.values())): + require_disjoint(output, path) + native.validate_hash(args.fixture_receipt, args.fixture_receipt_sha256) + approved = native.read_json(args.fixture_receipt) + before = fingerprint(source) + if not before or Path(approved["source"]).resolve() != source or approved["sourceFingerprint"] != before: + raise AssertionError("Historical fixture no longer matches its approved receipt") + with socket.socket() as listener: + listener.bind(("127.0.0.1", args.port)) + seed_before = fingerprint(seed) + expected_jars = {path.name: sha.upper() for _, path, sha in mods.values()} + runtime = output / "server" + report = output / "native1211-actors-results.json" + runner = Path(__file__) + result = dict(status="RUNNING", kind=args.kind, source=str(source), realClientLogin="NOT_TESTED", + qualificationStatus=native.EXPLORATORY, minecraft=native.MINECRAFT, forge=native.FORGE, + fixtureReceiptSha256=digest(args.fixture_receipt), profileSha256=digest(args.profile), + inventorySha256=digest(args.inventory), runnerSha256=digest(runner), + historicalOracleSha256=digest(Path(historical.__file__)), javaSha256=digest(java), + javaVersion=version, port=args.port, jars=expected_jars, checks={}) + output.mkdir() + write_json(report, result) + + def stage(destination, target, port): + native.stage(destination, *target, port) + return native.LAUNCHER + + class Server(native.Server): + def __init__(self, *values): + native.verify_runtime(runtime, inventory, expected_jars) + super().__init__(*values) + + def close(self): + try: + super().close() + finally: + native.verify_runtime(runtime, inventory, expected_jars) + + try: + args.target_manifest, args.java17 = args.profile, java + result["checks"] = run_route( + source, runtime, (seed, inventory, mods), args, stage=stage, server_type=Server, + stop_audit=partial(audit_stopped, minecraft_version=native.MINECRAFT, + data_version=DATA_VERSION, stopping_phase="ServerStoppingEvent"), + check_log=native.require_clean_log, minecraft=native.MINECRAFT) + checks = result["checks"] + result["status"] = "PASS" if checks and all(c["status"] == "PASS" for c in checks.values()) else "FAIL" + if args.kind == "dimensions": + result["nativeAssessment"] = native.read_json(runtime / "native117-platform-assessment.json") + result["status"] = result["nativeAssessment"]["assessment"]["status"] + except BaseException as failure: + result.update(status="FAIL", error=repr(failure), traceback=traceback.format_exc()) + checkpoint = runtime / "native117-comparison.json" + if checkpoint.is_file(): + result["checks"] = native.read_json(checkpoint) + raise + finally: + result["sourceUnchanged"] = fingerprint(source) == before + result["seedUnchanged"] = fingerprint(seed) == seed_before + result["inputsUnchanged"] = all(digest(path) == sha for path, sha in ( + (args.profile, result["profileSha256"]), (args.inventory, result["inventorySha256"]), + (args.fixture_receipt, result["fixtureReceiptSha256"]), (java, result["javaSha256"]), + (runner, result["runnerSha256"]), (Path(historical.__file__), result["historicalOracleSha256"]), + *((path, sha.upper()) for _, path, sha in mods.values()), + )) + if not all(result[key] for key in ("sourceUnchanged", "seedUnchanged", "inputsUnchanged")): + result["status"] = "FAIL" + write_json(report, result) + return int(result["status"] not in ("PASS", "PASS_WITH_NATIVE_DIFFERENCE")) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native1211_broad.py b/test-rig/migration/native1211_broad.py new file mode 100644 index 00000000..85c22e3d --- /dev/null +++ b/test-rig/migration/native1211_broad.py @@ -0,0 +1,118 @@ +"""Commission all 21 bays and prove twelve native drill witnesses across restart.""" + +import argparse +from pathlib import Path +from types import SimpleNamespace +import traceback + +from matrix import digest, fingerprint, write_json +from native117 import output_path, require_disjoint +from native117_broad import DRILL_BAYS, campaign_document, drill_cases, run_profile +from native117_readiness import await_ready +import native1211 as native + + +class Adapter: + Server = native.Server + MINECRAFT, FORGE, FLAT_SETTINGS = native.MINECRAFT, native.FORGE, native.FLAT_SETTINGS + verify_runtime = staticmethod(native.audit_staged) + require_clean_log = staticmethod(native.require_clean_log) + + @staticmethod + def stage(runtime, target, port): + seed, inventory, entries = target + native.stage(runtime, seed, inventory, { + key: (item["version"], item["path"], item["sha256"]) for key, item in entries.items() + }, port) + return native.LAUNCHER + + @staticmethod + def drill_tag(marker): + return ('{components:{"minecraft:custom_data":{AdvBayWitness:' + marker + + '}},Items:[{Slot:0b,id:"minecraft:diamond",count:7,' + 'components:{"minecraft:custom_data":{AdvBayStock:' + marker + '}}}]}') + + @staticmethod + def drill_data(row): + data = native.compat_tile_data(row) + if "AdvBayWitness" in data.get("ForgeData", {}): + raise AssertionError("Broad witness must come from the native tile component") + marker = data.get("components", {}).get("minecraft:custom_data", {}).get("AdvBayWitness") + if marker is not None: + data["ForgeData"] = {"AdvBayWitness": marker} + return data + + @staticmethod + def drill_tickets(server, runtime, stations, *, add): + for station in stations: + if station["id"] not in DRILL_BAYS: + continue + cells = [case[cell] for case in drill_cases(station) for cell in ("origin", "rear")] + bounds = (min(x for x, _, _ in cells), min(z for _, _, z in cells), + max(x for x, _, _ in cells), max(z for _, _, z in cells)) + verb = "add" if add else "remove" + server.send(f"forceload {verb} " + " ".join(map(str, bounds))) + if add: + await_ready(server, runtime, tuple(value >> 4 for value in bounds), + f"broad-seed-{station['id']}", minecraft=native.MINECRAFT) + else: + server.wait("Unmarked", 30) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for key in ("profile", "seed", "inventory", "java", "output"): + parser.add_argument("--" + key, type=lambda value: Path(value).resolve(), required=True) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--port", type=int, required=True) + parser.add_argument("--run-seconds", type=int, default=8) + parser.add_argument("--mode", 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("Use a bounded duration and consecutive unprivileged localhost ports") + output = output_path(args.output) + seed, inventory, mods = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + if "basemetals" not in mods: + raise ValueError("The shared profile is the source of both shared and decoupled pins") + java, version = native.java21(args.java) + for path in (args.profile, seed, args.inventory, java, *(item[1] for item in mods.values())): + require_disjoint(output, path) + protected = fingerprint(seed) + entries = {key: dict(id=key, version=value[0], path=value[1], file=value[1].name, sha256=value[2]) + for key, value in mods.items()} + spec = native.RIG / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json" + stations = native.read_json(spec)["stations"] + if len(stations) != 21: + raise AssertionError("The complete 21-bay definition is required") + names = ("packaged-current", "packaged-decoupled") if args.mode == "both" else (args.mode,) + proof = dict(qualificationStatus=native.EXPLORATORY, minecraft=native.MINECRAFT, forge=native.FORGE, + profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), + stationDefinitionSha256=digest(spec), javaVersion=version, javaSha256=digest(java), + jars={key: item["sha256"] for key, item in entries.items()}) + receipt = output / "native1211-broad-results.json" + results = {} + for index, name in enumerate(names): + write_json(receipt, campaign_document(names, results, proof, finished=False)) + try: + results[name] = run_profile(SimpleNamespace(java17=java, run_seconds=args.run_seconds), None, + output, name, args.port + index, stations, + native118_target=(seed, inventory, entries), native_adapter=Adapter) + except Exception: + results[name] = dict(status="FAIL", profile=name, error=traceback.format_exc()) + try: + expected = {item["file"]: item["sha256"] for key, item in entries.items() + if name != "packaged-decoupled" or key != "basemetals"} + native.verify_runtime(output / name, inventory, expected) + except Exception: + results[name].update(status="FAIL", custodyError=traceback.format_exc()) + print(name, results[name]["status"], flush=True) + final = campaign_document(names, results, proof, finished=True) + final["seedUnchanged"] = fingerprint(seed) == protected + if not final["seedUnchanged"]: + final["status"] = "FAIL" + write_json(receipt, final) + return int(final["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native1211_endurance.py b/test-rig/migration/native1211_endurance.py new file mode 100644 index 00000000..dfe55016 --- /dev/null +++ b/test-rig/migration/native1211_endurance.py @@ -0,0 +1,243 @@ +"""Run the measured Works feed, conservation and cold-restart protocol on Forge 52.""" + +import argparse +from collections import Counter +import json +from pathlib import Path +import time + +import endurance +from matrix import WORLD, digest, fingerprint, write_json +from native117 import output_path, require_disjoint +from native117_broad import read_json +from native117_endurance import NativeEnduranceRig, configure_new_fresh_fixture, prove_fresh_fixture_before_build +from native117_identity import target_id +import native1211 as native +from native1211_upgrade import comparison, copy_source +import semantic_machine_audit + + +class EnduranceHold(Exception): + pass + + +def check_hold(root): + if (root / "hold-endurance.request").is_file(): + raise EnduranceHold("Operator requested hold") + + +def measure_batch(documents, definitions): + # Use a derived view for the frozen oracle; raw checkpoint documents stay intact. + def project(row): + if "nbt" not in row: + return row + view = dict(row, nbt=native.compat_tile_nbt(row).snbt()) + view.pop("nbtBase64", None) + return view + + try: + views = { + station: dict(document, snapshot=[project(row) for row in document["snapshot"]]) + if document.get("snapshot") else document + for station, document in documents.items() + } + except (AssertionError, KeyError, ValueError, TypeError) as error: + return dict(status="FAIL", failures=[str(error)]) + return semantic_machine_audit.endurance_batch(views, definitions, identity=target_id) + + +class Rig(NativeEnduranceRig): + server_type = native.Server + + def __init__(self, name, runtime, java, launcher, definitions, inventory, expected): + super().__init__(name, runtime, java, launcher, definitions) + self.code_inventory, self.expected_jars = inventory, expected + + def start(self, build=False): + # Prime an inert rig. Otherwise old start-action stock can enter a conveyor + # while console commands clear and replace the finite measured inputs. + self.command("tick freeze", "The game is frozen") + try: + return super().start(build=build) + finally: + self.command("tick unfreeze", "The game is running normally") + + def seed(self, initial=False): + if initial: + return super().seed(initial=True) # start() already holds the native tick gate. + self.command("tick freeze", "The game is frozen") + try: + return super().seed(initial=False) + finally: + self.command("tick unfreeze", "The game is running normally") + + def checkpoint(self, label): + for station in endurance.CHECKED: + self.command(f"advworks checkpoint {station} {label}", "Checkpoint written for " + station) + document = read_json(self.runtime / "advantage-works-results" / f"{station}-checkpoint-{label}.json") + self.documents[station], self.snapshots[station] = document, native.parse_endurance_snapshot(document) + + def batch(self, label): + measured = measure_batch( + {station: self.documents[station] for station in ("S-04", "E-03", "W-04", "W-03")}, self.definitions) + with (self.runtime / f"semantic-batch-{label}.json").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 endurance.Rig.batch(self, label, power="basepower") + + def boot(self, phase): + native.verify_runtime(self.runtime, self.code_inventory, self.expected_jars) + super().boot(phase) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("profile", "seed", "inventory", "output", "java"): + parser.add_argument("--" + name, required=True, type=lambda value: Path(value).resolve()) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--seconds", type=int, default=1800) + parser.add_argument("--retained", type=lambda value: Path(value).resolve(strict=True)) + parser.add_argument("--retained-receipt-sha256") + parser.add_argument("--port", type=int, default=25658) + args = parser.parse_args() + endurance.require(args.seconds >= 60 and args.seconds % 60 == 0, "Use whole one-minute batches") + if not 1024 <= args.port < 65535: + parser.error("Use unprivileged consecutive localhost ports") + root = output_path(args.output) + target = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + java, version = native.java21(args.java) + seed_before = fingerprint(args.seed) + for protected in (args.profile, args.seed, args.inventory, java, *(item[1] for item in target[2].values())): + require_disjoint(root, protected) + source_before = None + if args.retained: + require_disjoint(root, args.retained) + receipts = list(args.retained.glob("native1211-*-upgrade-results.json")) + if len(receipts) != 1 or args.retained_receipt_sha256 is None: + raise ValueError("Pin the exact passing retained migration receipt") + native.validate_hash(receipts[0], args.retained_receipt_sha256) + prior = read_json(receipts[0]) + if (prior["status"] != "PASS" or prior["profileSha256"] != digest(args.profile) + or prior["inventorySha256"] != digest(args.inventory) + or digest(args.retained / "migration-restarted.json") != prior["phases"]["restarted"]["snapshotSha256"]): + raise AssertionError("Retained input does not match the passing pinned migration") + source_before = fingerprint(args.retained) + spec = native.RIG / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json" + definitions = {row["id"]: row for row in read_json(spec)["stations"]} + report = dict(status="RUNNING", minecraft=native.MINECRAFT, forge=native.FORGE, + qualificationStatus=native.EXPLORATORY, javaVersion=version, javaSha256=digest(java), + jars={key: item[2] for key, item in target[2].items()}, duration_seconds=args.seconds, + interval_seconds=60, profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), + stationDefinitionSha256=digest(spec), active_stations=endurance.ACTIVE, + checked_stations=endurance.CHECKED, profiles={}, retained=str(args.retained) if args.retained else None) + receipt = root / "native1211-endurance-results.json" + write_json(receipt, report) + rigs = [] + try: + cases = (("fresh1211", None), ("retained-upgrade-native1211", args.retained)) if args.retained else (("fresh1211", None),) + for index, (name, source) in enumerate(cases): + check_hold(root) + runtime = root / name + expected = native.stage(runtime, *target, args.port + index) + rig = Rig(name, runtime, java, native.LAUNCHER, definitions, target[1], expected) + rigs.append(rig) + if source: + copy_source(source, runtime) + write_json(runtime / "migration-fixture.json", read_json(source / "migration-fixture.json")) + rig.boot("retained-before-build") + rig.server.snapshot("retained-before-build") + rig.shutdown() + preservation = comparison(read_json(source / "migration-restarted.json"), + read_json(runtime / "migration-retained-before-build.json"), modern=False) + write_json(runtime / "retained-comparison.json", preservation) + endurance.require(preservation["status"] == "PASS", "Retained fixture changed before operation") + else: + creation = configure_new_fresh_fixture(runtime) + (runtime / "advantage-works.allow").touch() + (runtime / "migration-operate.allow").touch() + rig.boot("endurance") + if source is None: + report["fresh_fixture_proof"] = prove_fresh_fixture_before_build(rig, creation) + rig.start(build=True) + initial = {rig.name: rig.documents["W-01"]["worldTime"] for rig in rigs} + started = time.monotonic() + cycles = args.seconds // 60 + for cycle in range(1, cycles + 1): + deadline = started + cycle * 60 + while time.monotonic() < deadline: + check_hold(root) + 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: + check_hold(root) + label = f"cycle-{cycle:03}" + rig.checkpoint(label) + rig.batch(label) + if cycle < cycles: + rig.seed() + report["profiles"][rig.name] = dict(status="RUNNING", completed_batches=len(rig.ledger), + latest_world_time=rig.documents["W-01"]["worldTime"]) + write_json(receipt, report) + print(f"{cycle * 60}s: all requested profiles passed batch {cycle}", flush=True) + elapsed = time.monotonic() - started + ticks = {rig.name: rig.documents["W-01"]["worldTime"] - initial[rig.name] for rig in rigs} + for rig in rigs: + check_hold(root) + endurance.require(ticks[rig.name] >= args.seconds * 18, rig.name + ": fewer than 18 game ticks per second") + rig.structure() + rig.checkpoint("before-restart") + rig.batch("before-restart") + rig.command("save-all flush", "Saved the game") + rig.shutdown() + rig.boot("endurance-restart") + rig.checkpoint("after-restart") + rig.batch("after-restart") + rig.structure() + rig.seed() + resume_deadline = time.monotonic() + 60 + while time.monotonic() < resume_deadline: + check_hold(root) + time.sleep(min(1, max(0, resume_deadline - time.monotonic()))) + for rig in rigs: + check_hold(root) + rig.checkpoint("restart-resumed") + rig.batch("restart-resumed") + statuses = rig.structure() + rig.command("save-all flush", "Saved the game") + rig.shutdown() + totals = Counter() + for batch in rig.ledger: + if batch["label"].startswith("cycle-"): + totals.update(batch["outputs"]) + report["profiles"][rig.name] = dict(status="PASS", elapsed_seconds=elapsed, endurance_batches=cycles, + game_ticks=ticks[rig.name], verified_checkpoints=len(rig.ledger), + endurance_outputs=dict(totals), station_statuses=statuses, + restart="PASS", resumed_processing="PASS") + report["status"] = "PASS" if args.seconds >= 1800 else "SMOKE_PASS_NOT_ENDURANCE" + except EnduranceHold as hold: + report.update(status="HELD", holdReason=str(hold)) + except BaseException as error: + report.update(status="FAIL", failure=repr(error)) + raise + finally: + for rig in rigs: + try: + rig.shutdown() + native.verify_runtime(rig.runtime, rig.code_inventory, rig.expected_jars) + except Exception as error: + report.update(status="FAIL", shutdown_or_audit_error=repr(error)) + report["seedUnchanged"] = fingerprint(args.seed) == seed_before + if not report["seedUnchanged"]: + report["status"] = "FAIL" + if args.retained: + report["retained_unchanged"] = fingerprint(args.retained) == source_before + if not report["retained_unchanged"]: + report["status"] = "FAIL" + write_json(receipt, report) + print(report["status"] + ": " + str(receipt), flush=True) + return 2 if report["status"] == "HELD" else int(report["status"] == "FAIL") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native1211_machines.py b/test-rig/migration/native1211_machines.py new file mode 100644 index 00000000..3fd6c77b --- /dev/null +++ b/test-rig/migration/native1211_machines.py @@ -0,0 +1,139 @@ +"""Run native harvest, lifecycle and cross-mod oil checks without altering the old oracles.""" + +import argparse +from pathlib import Path +import traceback + +import lifecycle +from matrix import digest, fingerprint, write_json +from native117 import output_path, require_disjoint +from native117_actor_io import bind +from native117_identity import fixture_ids +from native117_lifecycle import NativeLifecycleRun, authorize_lifecycle, exercise +from native117_oil import NativeOilRun, exercise as exercise_oil, fixtures as oil_fixtures +from native1211_networks import compatibility_view, native_inventory_item +import native1201_machines +import native1211 as native + + +MINERALOGY = dict(id="mineralogy", version="6.1.4.121011", + sourceRef="87d2d1bb2d1e7898f602f8ae9284f43e543c6e01", + file="Mineralogy-6.1.4.121011.jar", + sha256="DD1537FF6279BBB8BAFEFFA3FBE6BA799B124125FD5C383FF59F9DB0184F358D") + + +def oil_pins(entries): + pins = {"jars": [dict(id=key, version=value[0], path=str(value[1]), sha256=value[2]) + for key, value in entries.items()]} + for pin in pins["jars"]: + if pin["id"] == "mineralogy": + if pin["version"] != MINERALOGY["version"] or pin["sha256"].upper() != MINERALOGY["sha256"]: + raise ValueError("Wrong Mineralogy oil input") + pin["sourceRef"] = MINERALOGY["sourceRef"] + return pins + + +def native_boot(path): + native.require_clean_log(path) + content = path.read_text(encoding="utf-8") + for marker in (f"Starting minecraft server version {native.MINECRAFT}", + f"Forge mod loading, version {native.FORGE}, for MC {native.MINECRAFT} with MCP {native.MCP}"): + if marker not in content: + raise AssertionError("Missing native machine startup proof: " + marker) + + +class LifecycleRun(NativeLifecycleRun): + minecraft = native.MINECRAFT + max_ticket_distance = 11 + require_boot = staticmethod(native_boot) + + def open_server(self, label): + native.audit_staged(self.runtime) + return native.Server(self.runtime, self.java, self.launcher, label) + + def inventory(self, pos, slot, item, count): + coordinates = " ".join(map(str, pos)) + self.command(f"item replace block {coordinates} container.{slot} with {native_inventory_item(item)} {count}") + + def probe(self, operation, label): + return compatibility_view(super().probe(operation, label)) + + +class OilRun(NativeOilRun): + expected_launcher = native.LAUNCHER + max_ticket_distance = 11 + require_boot = staticmethod(native_boot) + + def open_server(self, label): + native.audit_staged(self.runtime) + return native.Server(self.runtime, self.java, self.launcher, label) + + +harvest = bind(native1201_machines.harvest, native1201=native) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("profile", "seed", "inventory", "java", "output"): + parser.add_argument("--" + name, required=True, type=lambda value: Path(value).resolve()) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--port", type=int, required=True) + parser.add_argument("--campaign", choices=("harvest", "oil", "lifecycle"), required=True) + args = parser.parse_args() + target = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + java, version = native.java21(args.java) + output = output_path(args.output) + for path in (args.profile, args.seed, args.inventory, java, *(entry[1] for entry in target[2].values())): + require_disjoint(output, path) + seed_before = fingerprint(args.seed) + expected = native.stage(output, *target, args.port) + receipt = output / f"native1211-{args.campaign}-results.json" + report = dict(status="RUNNING", minecraft=native.MINECRAFT, forge=native.FORGE, jars=expected, + profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), + javaSha256=digest(java), javaVersion=version) + + def progress(observations): + report["observations"] = observations + write_json(receipt, report) + + runner = None + try: + if args.campaign == "harvest": + result = harvest(output, java, native.LAUNCHER, {}, progress) + elif args.campaign == "oil": + pins = oil_pins(target[2]) + definition = oil_fixtures(pins, mineralogy_version=MINERALOGY["version"], + mineralogy_source=MINERALOGY["sourceRef"]) + write_json(output / "lifecycle-fixture.json", definition) + authorize_lifecycle(output) + runner = OilRun(output, java, native.LAUNCHER, definition, pins, seed_before) + exercise_oil(runner, 8, progress) + result = dict(report["observations"], laneCount=len(definition), status="PASS") + else: + definition = fixture_ids(lifecycle.fixtures()) + write_json(output / "lifecycle-fixture.json", definition) + authorize_lifecycle(output) + runner = LifecycleRun(output, java, native.LAUNCHER, definition) + result = exercise(runner, "fresh1211", progress) + report.update(status=result["status"], observations=result) + except BaseException as failure: + report.update(status="FAIL", failure=repr(failure), traceback=traceback.format_exc()) + raise + finally: + try: + if runner is not None: + runner.close() + native.verify_runtime(output, target[1], expected) + report["seedUnchanged"] = fingerprint(args.seed) == seed_before + if not report["seedUnchanged"]: + raise AssertionError("Protected server seed changed") + except BaseException as failure: + report.update(status="FAIL", custodyError=repr(failure)) + raise + finally: + write_json(receipt, report) + return int(report["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native1211_networks.py b/test-rig/migration/native1211_networks.py new file mode 100644 index 00000000..a972feeb --- /dev/null +++ b/test-rig/migration/native1211_networks.py @@ -0,0 +1,204 @@ +"""Run physical chunk unload, bridge, drill and interop oracles on Forge 52.""" + +import argparse +from pathlib import Path +import socket +import traceback + +import nbtlib + +from matrix import digest, fingerprint, write_json +from native117 import output_path, require_disjoint +import native117_extended +from native117_geometry import FORCED_LEVEL, FULL_LEVEL +from native117_holder_networks import bridge_fixtures, extended_fixtures, linear_fixtures +from native117_interop import require_checks, require_probe_order +from native117_lifecycle import authorize_lifecycle +from native117_partial import exercise_bridge, exercise_partial +from native117_readiness import await_ready +from native117_ticket_networks import NativeTicketExtendedRun +import native1211 as native + + +# Forge 52's ChunkPyramid has radius 11 (ChunkLevel.MAX_LEVEL = 44). +# Keep the finite fixtures outside that actual loaded-holder apron. +MAX_TICKET_DISTANCE = 11 +HOLDER_RADIUS = FULL_LEVEL + MAX_TICKET_DISTANCE - FORCED_LEVEL + + +def native_inventory_item(item): + identity, separator, payload = item.partition("{") + if not separator: + return item + custom = nbtlib.parse_nbt(separator + payload) + if not isinstance(custom, nbtlib.Compound): + raise AssertionError("Historical drill fixture item data must be a compound") + return identity + "[minecraft:custom_data=" + custom.snbt(compact=True) + "]" + + +def compatibility_view(value): + """Supply the old oracle's accessors while retaining the full native item components.""" + if isinstance(value, list): + return [compatibility_view(item) for item in value] + if not isinstance(value, dict): + return value + result = {key: compatibility_view(item) for key, item in value.items()} + if isinstance(value.get("nbt"), str): + compound = nbtlib.parse_nbt(value["nbt"]) + if not isinstance(compound, nbtlib.Compound): + raise AssertionError("Native lifecycle witness NBT must be a compound") + for stack in compound.get("Items", ()): + if (not isinstance(stack, nbtlib.Compound) or not isinstance(stack.get("id"), nbtlib.String) + or not isinstance(stack.get("count"), nbtlib.Int) or int(stack["count"]) <= 0 + or "Count" in stack or "tag" in stack): + raise AssertionError(f"Expected native 1.21.1 inventory stack: {stack}") + stack["Count"] = stack["count"] + custom = stack.get("components", {}).get("minecraft:custom_data") + if custom is not None: + if not isinstance(custom, nbtlib.Compound): + raise AssertionError("Native inventory custom_data must be a compound") + stack["tag"] = custom + result["nbt"] = compound.snbt(compact=True) + return result + + +class PartialRun(NativeTicketExtendedRun): + minecraft = native.MINECRAFT + max_ticket_distance = MAX_TICKET_DISTANCE + require_boot = staticmethod(native.require_clean_log) + + def open_server(self, label): + return Server(self.runtime, self.java, self.launcher, label) + + def inventory(self, pos, slot, item, count): + coordinates = " ".join(map(str, pos)) + self.command(f"item replace block {coordinates} container.{slot} with {native_inventory_item(item)} {count}") + + def probe(self, operation, label): + return compatibility_view(super().probe(operation, label)) + + +class Server(native.Server): + def __init__(self, runtime, *args): + native.audit_staged(runtime) + super().__init__(runtime, *args) + + def close(self): + try: + super().close() + finally: + native.audit_staged(self.native_runtime) + + +def interop(runtime, java, progress): + observations = {} + for phase in ("exercise", "restored"): + server = Server(runtime, java, native.LAUNCHER, phase) + try: + server.send("forceload add 1023 1023 1100 1030") + ready = await_ready(server, runtime, (63, 63, 68, 64), "interop-" + phase, + minecraft=native.MINECRAFT) + server.send("advinterop " + phase) + server.wait("ADVINTEROP COMPLETE " + phase, 120) + report = native.read_json(runtime / "advantage-works-results" / f"native-interop-{phase}.json") + observations[phase] = dict(observation=report, readiness=ready) + progress(observations) + require_probe_order(report, ready) + require_checks(report, phase) + if "basemetals" in {item["id"] for item in native.read_json(runtime / "network-inputs.json")}: + native.require_shared_crushing(report) + server.send("save-all flush") + server.wait("Saved the game", 30) + finally: + server.close() + return dict(observations=observations, status="PASS") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("profile", "seed", "inventory", "output", "java"): + parser.add_argument("--" + name, type=lambda value: Path(value).resolve(), required=True) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--port", type=int, required=True) + parser.add_argument("--campaign", choices=("partial", "bridge", "extended", "interop"), required=True) + args = parser.parse_args() + if not 26450 <= args.port <= 65535: + parser.error("Use a disposable localhost port at or above 26450") + + runtime = output_path(args.output) + seed, inventory, mods = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + java, version = native.java21(args.java) + for path in (args.profile, seed, args.inventory, java, *(item[1] for item in mods.values())): + require_disjoint(runtime, path) + with socket.socket() as listener: + listener.bind(("127.0.0.1", args.port)) + protected = fingerprint(seed) + expected = {path.name: sha.upper() for _, path, sha in mods.values()} + helpers = [Path(__file__), *(Path(__file__).with_name(name) for name in ( + "native117_partial.py", "native117_ticket_networks.py", "native117_holder_networks.py", + "native117_extended.py", "extended_lifecycle.py", "lifecycle.py", "audit_pipe_only.py", + ))] + source_hashes = {path.name: digest(path) for path in helpers} + report = dict(status="RUNNING", campaign=args.campaign, qualificationStatus=native.EXPLORATORY, + minecraft=native.MINECRAFT, forge=native.FORGE, port=args.port, + profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), + javaSha256=digest(java), javaVersion=version, jars=expected, oracleSources=source_hashes) + receipt = runtime / f"native1211-{args.campaign}-results.json" + + def progress(value): + report["observations"] = value + write_json(receipt, report) + + runner = None + staged = False + try: + native.stage(runtime, seed, inventory, mods, args.port) + staged = True + write_json(receipt, report) + write_json(runtime / "network-inputs.json", [ + dict(id=key, version=mod_version, file=path.name, sha256=sha.upper()) + for key, (mod_version, path, sha) in mods.items() + ]) + if args.campaign == "interop": + result = interop(runtime, java, progress) + else: + layout, exercise = { + "partial": (linear_fixtures, exercise_partial), + "bridge": (bridge_fixtures, exercise_bridge), + "extended": (extended_fixtures, native117_extended.exercise), + }[args.campaign] + definition = layout(HOLDER_RADIUS) + fixture = runtime / "lifecycle-fixture.json" + write_json(fixture, definition) + report["fixtureSha256"] = digest(fixture) + authorize_lifecycle(runtime) + runner = PartialRun(runtime, java, native.LAUNCHER, definition) + result = exercise(runner, progress) + report.update(status=result["status"], observations=result) + except BaseException as failure: + report.update(status="FAIL", failure=repr(failure), traceback=traceback.format_exc()) + raise + finally: + try: + if runner is not None: + runner.close() + if staged: + native.verify_runtime(runtime, inventory, expected) + report["seedUnchanged"] = fingerprint(seed) == protected + report["oraclesUnchanged"] = all(digest(path) == source_hashes[path.name] for path in helpers) + report["inputsUnchanged"] = all(digest(path) == sha for path, sha in ( + (args.profile, report["profileSha256"]), (args.inventory, report["inventorySha256"]), + (java, report["javaSha256"]), *((path, sha.upper()) for _, path, sha in mods.values()), + )) + if not all(report[key] for key in ("seedUnchanged", "oraclesUnchanged", "inputsUnchanged")): + raise AssertionError("Protected input changed during the native network test") + except Exception as audit: + report.update(status="FAIL", auditError=repr(audit)) + finally: + if runtime.is_dir(): + write_json(receipt, report) + return int(report["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native1211_operations.py b/test-rig/migration/native1211_operations.py new file mode 100644 index 00000000..41eef4be --- /dev/null +++ b/test-rig/migration/native1211_operations.py @@ -0,0 +1,119 @@ +"""Measure complete finite machine jobs, then compare their stopped saves and cold reload.""" + +import argparse +import gzip +import io +from pathlib import Path +import traceback +import zlib + +import nbtlib + +from matrix import digest, fingerprint, write_json +from native117 import output_path, require_disjoint +from native117_actor_io import bind +from native117_machine_operations import validate_operation, validate_restart +import native117_machine_saved as saved +import native1211 as native + + +def modern_chunk(payload): + if len(payload) < 6 or int.from_bytes(payload[:4], "big") != len(payload) - 4: + raise ValueError("Invalid native terrain payload length") + decoders = {1: gzip.decompress, 2: zlib.decompress, 3: lambda value: value} + root = nbtlib.File.parse(io.BytesIO(decoders[payload[4]](payload[5:]))) + if int(root.get("DataVersion", -1)) != 3955 or "Level" in root: + raise ValueError("Machine save must be native 1.21.1 terrain") + sections = [] + for section in root["sections"]: + view = nbtlib.Compound({"Y": section["Y"]}) + if "block_states" in section: + states = section["block_states"] + view["Palette"] = states["palette"] + if "data" in states: + view["BlockStates"] = states["data"] + sections.append(view) + # Supply only field-name accessors to the unchanged, typed raw-save oracle. + return nbtlib.Compound({"xPos": root["xPos"], "zPos": root["zPos"], + "Sections": nbtlib.List[nbtlib.Compound](sections), + "TileEntities": root["block_entities"]}) + + +audit_saved = bind(saved.audit, chunk=modern_chunk) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("profile", "seed", "inventory", "java", "output"): + parser.add_argument("--" + name, required=True, type=lambda value: Path(value).resolve()) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--port", required=True, type=int) + parser.add_argument("--group", choices=("all", "pickup", "power", "steam", "electric"), default="all") + args = parser.parse_args() + target = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + java, version = native.java21(args.java) + runtime = output_path(args.output) + for path in (args.profile, args.seed, args.inventory, java, *(entry[1] for entry in target[2].values())): + require_disjoint(runtime, path) + seed_before = fingerprint(args.seed) + expected = native.stage(runtime, *target, args.port) + for marker in ("advantage-machine-operation.allow", "migration-operate.allow"): + (runtime / marker).touch() + report = dict(status="RUNNING", minecraft=native.MINECRAFT, forge=native.FORGE, group=args.group, + profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), jars=expected, + javaSha256=digest(java), javaVersion=version, phases={}) + receipt = runtime / "native1211-machine-operation-results.json" + write_json(receipt, report) + server = None + try: + server = native.Server(runtime, java, native.LAUNCHER, "operate") + for command in ("gamerule doMobSpawning false", "gamerule randomTickSpeed 0", + "gamerule doWeatherCycle false", "weather clear"): + server.send(command) + server.send("advmachines run " + args.group + " measured") + server.wait("ADVMACHINES COMPLETE measured", 1500) + catalog = native.read_json(runtime / "advantage-works-results/machine-catalog-measured.json") + document = native.read_json(runtime / "advantage-works-results/machine-operation-measured.json") + report["phases"]["operation"] = validate_operation(catalog, document, args.group) + server.send("save-all flush") + server.wait("Saved the game", 30) + server.close() + server = None + fixture = native.read_json(runtime / "machine-operation-fixture.json") + fixture_sha = report["fixtureSha256"] = digest(runtime / "machine-operation-fixture.json") + report["phases"]["saved-before-restart"] = audit_saved(runtime, fixture, runtime / "raw-before-restart") + write_json(receipt, report) + server = native.Server(runtime, java, native.LAUNCHER, "restart") + server.send("advmachines inspect restarted") + server.wait("ADVMACHINES INSPECTED restarted", 360) + observed = native.read_json(runtime / "advantage-works-results/machine-inspection-restarted.json") + report["phases"]["restart"] = validate_restart(fixture, observed) + server.send("save-all flush") + server.wait("Saved the game", 30) + server.close() + server = None + report["phases"]["saved-after-restart"] = audit_saved(runtime, observed, runtime / "raw-after-restart") + if digest(runtime / "machine-operation-fixture.json") != fixture_sha: + raise AssertionError("Operating fixture changed across restart") + report["status"] = "PASS" if all(value["status"] == "PASS" for value in report["phases"].values()) else "FAIL" + except BaseException as failure: + report.update(status="FAIL", failure=repr(failure), traceback=traceback.format_exc()) + raise + finally: + try: + if server is not None: + server.close() + native.verify_runtime(runtime, target[1], expected) + report["seedUnchanged"] = fingerprint(args.seed) == seed_before + if not report["seedUnchanged"]: + raise AssertionError("Protected server seed changed") + except BaseException as failure: + report.update(status="FAIL", custodyError=repr(failure)) + raise + finally: + write_json(receipt, report) + return int(report["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native1211_recipes.py b/test-rig/migration/native1211_recipes.py new file mode 100644 index 00000000..c3b8837b --- /dev/null +++ b/test-rig/migration/native1211_recipes.py @@ -0,0 +1,92 @@ +"""Exercise live recipe discovery and progression against pinned Forge 52 player jars.""" + +import argparse +from pathlib import Path + +from matrix import digest, fingerprint, write_json +from native117 import output_path, require_disjoint +from native117_recipes import ( + MOD_COUNTS, RecipePruning, progression_campaign, recipe_fixture, require_free_port, unlock_campaign, +) +import native1201 +import native1211 + + +class RecipeServer(RecipePruning, native1211.Server): + @staticmethod + def errors(text): + return native1201.log_findings(text)[0] + + +class Engine: + version = (native1211.MINECRAFT, native1211.FORGE) + pack_format = 48 + loot_directory = "loot_table" + server = native1211.Server + recipe_server = RecipeServer + result_name = "native1211-recipes-results.json" + + @staticmethod + def stage(runtime, target, port, campaign): + native1211.stage(runtime, *target, port) + (runtime / ("advantage-recipes.allow" if campaign == "unlock" else "advantage-progression.allow")).touch() + if campaign == "progression": + (runtime / "advantage-works.allow").touch() + (runtime / "migration-operate.allow").touch() + return native1211.LAUNCHER + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("profile", "seed", "inventory", "output", "java"): + parser.add_argument("--" + name, required=True, type=Path) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--port", required=True, type=int) + parser.add_argument("--campaign", choices=("all", "unlock", "progression"), default="all") + args = parser.parse_args() + target = native1211.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + seed, inventory, mods = target + if set(mods) != {"basepower", "basesteam", "baseelectric", "advantageworkstest", "basemetals", "orespawn"}: + raise ValueError("The recipe and processing campaign needs the complete Base Metals profile") + require_free_port(args.port) + root = output_path(args.output) + for path in (args.profile, seed, args.inventory, *(path for _, path, _ in mods.values())): + require_disjoint(root, path) + definition, sources = recipe_fixture([path for _, path, _ in mods.values()], + component_predicates=True, singular_directories=True) + entries = {"jars": [ + dict(id=mod, version=version, path=str(path), sha256=sha.upper()) + for mod, (version, path, sha) in mods.items() + ]} + expected = {path.name: sha.upper() for _, path, sha in mods.values()} + before = fingerprint(seed) + report = dict(status="RUNNING", minecraft=native1211.MINECRAFT, forge=native1211.FORGE, + campaign=args.campaign, profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), + javaSha256=digest(args.java), target_jars=entries["jars"], recipe_inventory=MOD_COUNTS, + unlock_boots=[], progression_cases=[]) + write_json(root / "packaged-recipe-sources.json", sources) + try: + if args.campaign in ("all", "unlock"): + unlock_campaign(root, target, entries, args.java, args.port, definition, report, engine=Engine) + if len(report["unlock_boots"]) != 5: + raise AssertionError("Incomplete Forge 52 unlock campaign") + native1211.verify_runtime(root / "unlock", inventory, expected) + if args.campaign in ("all", "progression"): + progression_campaign(root, target, entries, args.java, args.port, report, engine=Engine) + if len(report["progression_cases"]) != 18: + raise AssertionError("Incomplete Forge 52 progression campaign") + native1211.verify_runtime(root / "progression", inventory, expected) + report["status"] = "PASS" + except BaseException as failure: + report.update(status="FAIL", error=repr(failure)) + raise + finally: + report["seed_unchanged"] = before == fingerprint(seed) + if not report["seed_unchanged"]: + report["status"] = "FAIL" + write_json(root / Engine.result_name, report) + return int(report["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native1211_upgrade.py b/test-rig/migration/native1211_upgrade.py new file mode 100644 index 00000000..d8c5f5a7 --- /dev/null +++ b/test-rig/migration/native1211_upgrade.py @@ -0,0 +1,303 @@ +"""Strict shared family upgrade using sealed historical or modern source receipts.""" + +import argparse +import gzip +import io +from pathlib import Path +import shutil +import time +import traceback +import zlib + +import nbtlib + +from matrix import WORLD, digest, differences, fingerprint, parse_nbt, plain, write_json +from native117 import native_normalized, output_path, require_disjoint +from native117_format import decode_palette +from native117_legacy_world import turbine +from native1211 import (EXPLORATORY, FORGE, LAUNCHER, MINECRAFT, Server, java21, read_family_target, read_json, stage, + validate_hash, verify_runtime) +from transformer_order import read_region + + +DATA_VERSION = 3955 +JOBS = (("sourceChest", "destination", "dirt", "dirt", 8), + ("steamCrusher", "steamCrusher", "stone", "cobblestone", 1), + ("electricFurnace", "electricFurnace", "iron_ore", "iron_ingot", 1)) + + +def components(value): + """Translate only the 1.20.1 item representation, never a 1.21.1 observation.""" + if isinstance(value, list): + return [components(item) for item in value] + if not isinstance(value, dict): + return value + if "Count" not in value or "id" not in value: + return {key: components(item) for key, item in value.items()} + stack = {key: components(item) for key, item in value.items() if key not in ("Count", "tag")} + stack["count"] = value["Count"] + old = dict(value.get("tag", {})) + patch = {} + if "Damage" in old: + patch["minecraft:damage"] = old.pop("Damage") + for before, after in (("Enchantments", "minecraft:enchantments"), + ("StoredEnchantments", "minecraft:stored_enchantments")): + if before in old: + entries = old.pop(before) + levels = {row["id"]: row["lvl"] for row in entries} + if len(levels) != len(entries): + raise AssertionError("Duplicate enchantment witness") + patch[after] = {"levels": levels} + if old: + patch["minecraft:custom_data"] = components(old) + if patch: + stack["components"] = patch + return stack + + +def comparison(before, after, *, modern): + def index(rows): + result = {(row["name"], row["x"], row["y"], row["z"]): row for row in rows} + if len(rows) != len(result): + raise AssertionError("Duplicate machine witness") + return result + + first, second = index(before), index(after) + if first.keys() != second.keys(): + raise AssertionError("Machine witness coordinates or names changed") + findings = [] + for identity, left in first.items(): + right = second[identity] + key = str(identity) + for field in ("actualName", "properties"): + if left[field] != right[field]: + findings.append(dict(path=f"{key}.{field}", expected=left[field], actual=right[field])) + if bool(left.get("nbtBase64")) != bool(right.get("nbtBase64")): + findings.append(dict(path=f"{key}.tile", expected=bool(left.get("nbtBase64")), + actual=bool(right.get("nbtBase64")))) + elif left.get("nbtBase64"): + expected = native_normalized(plain(parse_nbt(left))) + if modern: + expected = components(expected) + actual = native_normalized(plain(parse_nbt(right))) + findings.extend(differences(expected, actual, key + ".nbt")) + for row in differences(actual, expected, key + ".nbt"): + if row["actual"] == "": + findings.append(dict(path=row["path"], expected="", actual=row["expected"])) + return dict(status="FAIL" if findings else "PASS", witnesses=len(first), differences=findings) + + +def capture(runtime, label, rows): + world = runtime / WORLD + output = runtime / "native1211-save" / label + output.mkdir(parents=True) + shutil.copy2(world / "level.dat", output / "level.dat") + level = nbtlib.load(output / "level.dat")["Data"] + if int(level["DataVersion"]) != DATA_VERSION: + raise AssertionError("Saved level did not identify Minecraft 1.21.1") + regions, chunks = {}, {} + for row in rows: + x, y, z = pos = tuple(row[axis] for axis in ("x", "y", "z")) + cx, cz = key = x // 16, z // 16 + if key not in chunks: + rx, rz = region = cx // 32, cz // 32 + if region not in regions: + _, regions[region] = read_region(world / "region" / f"r.{rx}.{rz}.mca") + payload = regions[region][(cx & 31) + (cz & 31) * 32] + (output / f"{cx}_{cz}.chunk").write_bytes(payload) + if len(payload) < 6 or int.from_bytes(payload[:4], "big") != len(payload) - 4: + raise ValueError("Malformed chunk length") + decoders = {1: gzip.decompress, 2: zlib.decompress, 3: lambda data: data} + chunk = plain(nbtlib.File.parse(io.BytesIO(decoders[payload[4]](payload[5:]))).unpack()) + if chunk["DataVersion"] != DATA_VERSION or "Level" in chunk or (chunk["xPos"], chunk["zPos"]) != key: + raise AssertionError("Saved chunk differs from its native 1.21.1 region slot") + sections, tiles = {}, {} + for section in chunk["sections"]: + section_y = section["Y"] + if section_y in sections or any(k in section for k in ("Blocks", "Palette", "BlockStates")): + raise AssertionError("Duplicate section or old palette") + states = section.get("block_states") + sections[section_y] = decode_palette(states["palette"], states.get("data", [])) if states else None + for tile in chunk.get("block_entities", ()): + location = tile["x"], tile["y"], tile["z"] + if location in tiles: + raise AssertionError("Duplicate saved tile") + tile.pop("keepPacked", None) + tiles[location] = tile + chunks[key] = sections, tiles + sections, tiles = chunks[key] + palette = sections.get(y // 16) + block = palette[((y & 15) << 8) | ((z & 15) << 4) | (x & 15)] if palette else {"Name": "minecraft:air"} + if (block["Name"], block.get("Properties", {})) != (row["actualName"], row["properties"]): + raise AssertionError(f"Saved block differs from live state at {pos}: {block}") + expected = plain(parse_nbt(row)) if row.get("nbtBase64") else None + if tiles.get(pos) != expected: + raise AssertionError(f"Saved block entity differs from live state at {pos}: {tiles.get(pos)} != {expected}") + result = dict(status="PASS", witnesses=len(rows), nativeDataVersion=DATA_VERSION, + worldGenSettings=plain(level["WorldGenSettings"].unpack()), + files={path.name: digest(path) for path in output.iterdir()}) + write_json(output / "receipt.json", result) + return result + + +def copy_source(source, runtime): + protected = fingerprint(source / WORLD) + shutil.copytree(source / WORLD, runtime / WORLD) + shutil.copytree(source / "config", runtime / "config", dirs_exist_ok=True) + if protected != fingerprint(runtime / WORLD): + raise AssertionError("Staged world is not an exact copy") + + +def with_server(runtime, java, label, action): + server = Server(runtime, java, LAUNCHER, label) + try: + action(server) + server.snapshot(label) + server.send("save-all flush") + server.wait("Saved the game", 30) + except BaseException as failure: + try: + server.close() + except BaseException as shutdown: + failure.add_note(f"Shutdown/log audit also failed: {shutdown!r}") + raise + else: + server.close() + return read_json(runtime / f"migration-{label}.json") + + +def count(rows, name, item): + tile = next(row for row in rows if row["name"] == name) + return sum(int(stack["count"]) for stack in parse_nbt(tile).get("Items", ()) if stack["id"] == "minecraft:" + item) + + +def operate(runtime, frozen, target, java, port, positions, before): + expected = stage(runtime, *target, port) + copy_source(frozen, runtime) + shutil.copy2(frozen / "migration-fixture.json", runtime / "migration-fixture.json") + (runtime / "migration-operate.allow").touch() + + def start(server): + for source, _, item, _, amount in JOBS: + x, y, z = positions[source] + server.send(f"item replace block {x} {y} {z} container.0 with minecraft:{item} {amount}") + server.wait("Replaced a slot", 30) + time.sleep(30) + + try: + rows = with_server(runtime, java, "operation", start) + finally: + verify_runtime(runtime, target[1], expected) + results = {} + for source, sink, item, product, amount in JOBS: + old, new, remaining = count(before, sink, product), count(rows, sink, product), count(rows, source, item) + if new != old + amount or remaining: + raise AssertionError(f"{sink}: expected {old + amount} {product} and no input, got {new}/{remaining}") + results[sink] = dict(before=old, after=new, item="minecraft:" + product) + return results + + +def run(args): + source = args.source.resolve(strict=True) + reference = args.legacy_reference or source + prior_path = reference / ("native1201-legacy-world-results.json" if args.legacy_reference + else getattr(args, "source_receipt_name", "native1201-modern-upgrade-results.json")) + validate_hash(prior_path, args.receipt_sha256) + prior = read_json(prior_path) + restart = prior["phases"]["restarted"] + if prior["status"] != "PASS" or not prior["sourceUnchanged"] or restart["status"] != "PASS": + raise AssertionError("Missing passing source and restart receipts") + old_snapshot = reference / "migration-restarted.json" + validate_hash(old_snapshot, restart["snapshotSha256"]) + old_rows = read_json(old_snapshot) + target = read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + java, java_version = java21(args.java) + runtime = output_path(args.output) + for protected in (source, reference, args.seed, args.profile, args.inventory, java, *(mod[1] for mod in target[2].values())): + require_disjoint(runtime, protected) + before, seed_before = fingerprint(source), fingerprint(args.seed) + if args.legacy_reference: + def portable(hashes): + return {key.replace("\\", "/"): sha.upper() for key, sha in hashes.items()} + if source != Path(prior["source"]).resolve() or portable(before) != portable(prior["sourceFingerprint"]): + raise AssertionError("Historical source changed since the independent 1.20.1 run") + expected = stage(runtime, *target, args.port) + result = dict(status="RUNNING", qualificationStatus=EXPLORATORY, minecraft=MINECRAFT, forge=FORGE, + source=str(source), reference=str(reference), sourceReceiptSha256=digest(prior_path), profileSha256=digest(args.profile), + inventorySha256=digest(args.inventory), javaSha256=digest(java), javaVersion=java_version, + jars=expected, phases={}) + route = "historical" if args.legacy_reference else "modern" + receipt = runtime / f"native1211-{route}-upgrade-results.json" + write_json(receipt, result) + try: + copy_source(source, runtime) + fixture = read_json(reference / "migration-fixture.json") + write_json(runtime / "migration-fixture.json", fixture) + positions = {row["name"]: [row["x"], row["y"], row["z"]] for row in fixture} + saved = [] + for label in ("converted", "restarted"): + snapshot = runtime / f"migration-{label}.json" + try: + rows = with_server(runtime, java, label, lambda server: None) + except Exception as failure: + if snapshot.is_file(): + try: + phase = comparison(old_rows, read_json(snapshot), modern=True) + phase.update(snapshotSha256=digest(snapshot), evidenceStatus="OBSERVATION_ONLY", + reason="Server or log validation failed") + result["phases"][label] = phase + except Exception as diagnostic: + failure.add_note(f"Snapshot comparison also failed: {diagnostic!r}") + raise + phase = comparison(old_rows, rows, modern=True) + phase["snapshotSha256"] = digest(snapshot) + result["phases"][label] = phase + write_json(receipt, result) + if phase["status"] != "PASS": + raise AssertionError(f"Strict 1.20.1 witness mismatch in {label}: {phase['differences'][:5]}") + phase["serialized"] = capture(runtime, label, rows) + phase["turbine"] = turbine(runtime / WORLD, positions["hydroGenerator"], data_version=DATA_VERSION) + if phase["turbine"] != restart["turbine"]: + raise AssertionError("Turbine UUID, parent or position changed") + saved.append((rows, phase["serialized"]["worldGenSettings"])) + if saved[0][1] != saved[1][1] or comparison(saved[0][0], saved[1][0], modern=False)["status"] != "PASS": + raise AssertionError("Native machine state or worldgen settings changed during restart") + result["phases"]["operation"] = operate(runtime / "operation", runtime, target, java, args.port, + positions, saved[1][0]) + result["status"] = "PASS" + except BaseException as error: + result.update(status="FAIL", failure=repr(error), traceback=traceback.format_exc()) + raise + finally: + try: + verify_runtime(runtime, target[1], expected) + result["sourceUnchanged"] = fingerprint(source) == before + result["seedUnchanged"] = fingerprint(args.seed) == seed_before + if not result["sourceUnchanged"] or not result["seedUnchanged"]: + raise AssertionError("Protected source or seed changed") + except Exception as audit: + result.update(status="FAIL", auditError=repr(audit)) + finally: + write_json(receipt, result) + return int(result["status"] != "PASS") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for key in ("source", "profile", "seed", "inventory", "java", "output"): + parser.add_argument("--" + key, type=lambda value: Path(value).resolve(), required=True) + parser.add_argument("--legacy-reference", type=lambda value: Path(value).resolve(strict=True), + help="Passing independent 1.20.1 reference for the exact historical source directory") + parser.add_argument("--source-receipt-name", choices=("native1201-modern-upgrade-results.json", + "native1206-modern-upgrade-results.json"), default="native1201-modern-upgrade-results.json") + for key in ("receipt", "inventory"): + parser.add_argument("--" + key + "-sha256", required=True) + parser.add_argument("--port", type=int, required=True) + args = parser.parse_args() + if not 1024 <= args.port <= 65535: + parser.error("A dedicated unprivileged localhost port is required") + return run(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/test_audit_native1211_endurance.py b/test-rig/migration/test_audit_native1211_endurance.py new file mode 100644 index 00000000..596b119f --- /dev/null +++ b/test-rig/migration/test_audit_native1211_endurance.py @@ -0,0 +1,34 @@ +import json +from pathlib import Path +from tempfile import TemporaryDirectory +import unittest +from unittest.mock import patch + +from audit_native1211_endurance import Forge52 + + +class NativeEnduranceReplayTest(unittest.TestCase): + def test_replay_requires_a_matching_positive_semantic_checkpoint(self): + documents = {station: {"station": station} for station in + ("S-04", "E-03", "W-04", "W-03", "E-02")} + definitions = {"W-03": {"id": "W-03"}} + measured = {"status": "PASS", "measurements": {"oil": 1000}} + with TemporaryDirectory() as directory: + runtime = Path(directory) + checkpoint = runtime / "semantic-batch-cycle-001.json" + checkpoint.write_text(json.dumps(measured), encoding="utf-8") + with patch("audit_native1211_endurance.measure_batch", return_value=measured) as measure: + Forge52.verify_batch(runtime, "cycle-001", documents, definitions) + measure.assert_called_once_with({key: value for key, value in documents.items() + if key != "E-02"}, definitions) + + checkpoint.write_text(json.dumps({"status": "PASS", "measurements": {"oil": 999}}), + encoding="utf-8") + with self.assertRaisesRegex(AssertionError, "replay disagrees"): + Forge52.verify_batch(runtime, "cycle-001", documents, definitions) + + failed = {"status": "FAIL", "failures": ["missing oil"]} + checkpoint.write_text(json.dumps(failed), encoding="utf-8") + measure.return_value = failed + with self.assertRaisesRegex(AssertionError, "replay disagrees"): + Forge52.verify_batch(runtime, "cycle-001", documents, definitions) diff --git a/test-rig/migration/test_native1211.py b/test-rig/migration/test_native1211.py new file mode 100644 index 00000000..1a35bc0a --- /dev/null +++ b/test-rig/migration/test_native1211.py @@ -0,0 +1,176 @@ +import copy +import json +from pathlib import Path +from types import SimpleNamespace +import tempfile +import unittest +from unittest.mock import patch + +import nbtlib + +import endurance +import native1211 as native +import semantic_machine_audit +import test_semantic_machine_audit +from native117_identity import target_id +from native1211_upgrade import comparison, components +from native1211_broad import Adapter +from native1211_endurance import EnduranceHold, Rig, check_hold, measure_batch + + +class Native1211Test(unittest.TestCase): + def test_priming_is_atomic_and_always_releases_the_native_tick_gate(self): + rig = Rig("fresh", Path("runtime"), Path("java"), native.LAUNCHER, {}, {}, {}) + for method in ("start", "seed"): + calls = [] + rig.command = lambda command, response=None: calls.append(command) + with patch("native1211_endurance.NativeEnduranceRig." + method, + side_effect=RuntimeError("deliberate priming failure")): + with self.assertRaisesRegex(RuntimeError, "priming failure"): + getattr(rig, method)() + self.assertEqual(["tick freeze", "tick unfreeze"], calls) + + def test_endurance_decoding_is_local_and_preserves_raw_checkpoint(self): + row = {"pos": [1, 2, 3], "nbt": '{Items:[{Slot:0b,id:"baseelectric:lithium_battery",count:1,' + 'components:{"minecraft:custom_data":{energy:5.5f},"minecraft:damage":2}}]}'} + document = {"snapshot": [row]} + before = copy.deepcopy(document) + native_stack = native.parse_endurance_snapshot(document)[(1, 2, 3)]["data"]["Items"][0] + self.assertEqual(1, native_stack["Count"]) + self.assertEqual({"energy": 5.5}, native_stack["tag"]) + self.assertEqual(2, native_stack["components"]["minecraft:damage"]) + self.assertEqual(before, document) + self.assertNotIn("Count", endurance.parse_snapshot(document)[(1, 2, 3)]["data"]["Items"][0]) + legacy = {"snapshot": [{"pos": [1, 2, 3], "nbt": '{Items:[{Slot:0b,id:"minecraft:dirt",Count:2b}]}'}]} + self.assertEqual(2, endurance.parse_snapshot(legacy)[(1, 2, 3)]["data"]["Items"][0]["Count"]) + with self.assertRaisesRegex(AssertionError, "native 1.21.1 stack"): + native.parse_endurance_snapshot(legacy) + + def test_native_semantic_view_keeps_historical_positive_and_negative_oracles(self): + fixture = test_semantic_machine_audit.IndependentEnduranceTest() + fixture.setUp() + legacy = fixture.documents() + definitions = fixture.rig.definitions + self.assertEqual("PASS", semantic_machine_audit.endurance_batch(legacy, definitions)["status"]) + modern = copy.deepcopy(legacy) + for document in modern.values(): + for row in document["snapshot"]: + row["block"] = target_id(row["block"]) + data = nbtlib.parse_nbt(row["nbt"]) + for stack in data.get("Items", []): + stack["count"] = nbtlib.Int(stack.pop("Count")) + for name in ("Tank", "InputTank"): + if "FluidName" in data.get(name, {}): + data[name]["FluidName"] = nbtlib.String(target_id(str(data[name]["FluidName"]))) + row["nbt"] = data.snbt() + before = copy.deepcopy(modern) + self.assertEqual("PASS", measure_batch(modern, definitions)["status"]) + self.assertEqual(before, modern) + row = modern["W-03"]["snapshot"][0] + data = nbtlib.parse_nbt(row["nbt"]) + self.assertIn("Tank", data) + del data["Tank"] + row["nbt"] = data.snbt() + result = measure_batch(modern, definitions) + self.assertEqual("FAIL", result["status"]) + self.assertIn("Missing oil tank evidence", result["failures"]) + + def test_endurance_custody_does_not_shadow_native_inventory_commands(self): + rig = Rig("fresh", Path("runtime"), Path("java"), native.LAUNCHER, {}, {"files": {}}, {}) + self.assertTrue(callable(rig.inventory)) + self.assertEqual({"files": {}}, rig.code_inventory) + + def test_endurance_hold_requires_an_explicit_request_file(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + check_hold(root) + (root / "hold-endurance.request").touch() + with self.assertRaises(EnduranceHold): + check_hold(root) + + def test_broad_witness_requires_native_tile_and_item_components(self): + marker = '"S-05:down-facing"' + row = {"nbt": Adapter.drill_tag(marker)} + decoded = Adapter.drill_data(row) + self.assertEqual("S-05:down-facing", decoded["ForgeData"]["AdvBayWitness"]) + self.assertEqual("S-05:down-facing", decoded["Items"][0]["tag"]["AdvBayStock"]) + self.assertEqual(7, decoded["Items"][0]["Count"]) + self.assertIn("components", decoded["Items"][0]) + with self.assertRaisesRegex(AssertionError, "native tile component"): + Adapter.drill_data({"nbt": '{ForgeData:{AdvBayWitness:"legacy"}}'}) + with self.assertRaisesRegex(AssertionError, "native 1.21.1 stack"): + native.compat_tile_data({"nbt": '{Items:[{id:"minecraft:diamond",Count:7b}]}'}) + + def test_java_version_gate_rejects_non_21_runtime(self): + with tempfile.TemporaryDirectory() as directory: + java = Path(directory) / "java" + java.touch() + result = SimpleNamespace(stdout="", stderr='openjdk version "17.0.1"') + with patch.object(native.subprocess, "run", return_value=result): + with self.assertRaisesRegex(ValueError, "requires Java 21"): + native.java21(java) + result.stderr = 'openjdk version "21.0.12.1"' + self.assertEqual((java, result.stderr), native.java21(java)) + + def test_modern_components_preserve_exact_enchantment_wear_and_custom_payload(self): + before = {"id": "basesteam:musket", "Count": 1, "Slot": 3, "tag": { + "Damage": 5, "Enchantments": [{"id": "basesteam:powderless", "lvl": 1}], + "Witness": "legacy"}} + after = {"id": "basesteam:musket", "count": 1, "Slot": 3, "components": { + "minecraft:damage": 5, "minecraft:enchantments": {"levels": {"basesteam:powderless": 1}}, + "minecraft:custom_data": {"Witness": "legacy"}}} + self.assertEqual(after, components(before)) + witness = {"name": "chest", "x": 1, "y": 5, "z": 1, "actualName": "minecraft:chest", + "properties": {}, "nbtBase64": "fixture"} + with patch("native1211_upgrade.parse_nbt", side_effect=[{"Items": [before]}, {"Items": [after]}]): + self.assertEqual("PASS", comparison([witness], [witness], modern=True)["status"]) + extra = dict(after, components={**after["components"], "minecraft:repair_cost": 10}) + with patch("native1211_upgrade.parse_nbt", side_effect=[{"Items": [before]}, {"Items": [extra]}]): + self.assertEqual("FAIL", comparison([witness], [witness], modern=True)["status"]) + with patch("native1211_upgrade.parse_nbt", side_effect=[{"Items": [before]}, {"Items": [dict(after, count=2)]}]): + self.assertEqual("FAIL", comparison([witness], [witness], modern=True)["status"]) + + def test_requires_one_successful_shared_crushing_witness(self): + row = {"id": "basemetals/native-power-crushing", "status": "PASS"} + native.require_shared_crushing({"sharedChecks": [row]}) + for rows in ([], [row, row], [dict(row, status="FAIL")], [dict(row, id="other")]): + with self.subTest(rows=rows), self.assertRaises(AssertionError): + native.require_shared_crushing({"sharedChecks": rows}) + + def test_inventory_rejects_modified_launcher_even_with_the_same_shim(self): + with tempfile.TemporaryDirectory() as directory: + seed = Path(directory) + launcher = seed / native.LAUNCHER + launcher.parent.mkdir(parents=True) + launcher.write_text("-Djava.net.preferIPv6Addresses=system -jar " + native.SHIM) + with patch.object(native, "validate_hash"): + self.assertIn(native.LAUNCHER, native.inspect_seed(seed)["files"]) + launcher.write_text(launcher.read_text() + " --unexpected") + with self.assertRaisesRegex(AssertionError, "official shim"): + native.inspect_seed(seed) + + def test_profile_does_not_carry_historical_artifact_pins_or_drop_assertions(self): + sha = "A" * 64 + versions = {"basepower": "2.4.14.121011", "advantageworkstest": "1.1.5.121011", "orespawn": "4.1.0.121011"} + mods = {key: (version, native.RIG / f"{key}-{version}.jar", sha) for key, version in versions.items()} + fixture = native.read_json(native.RIG / "profiles/native1201/worldgen-legacy-import.json") + output = native.RIG / "build/test-generated-profile.json" + args = SimpleNamespace(name="worldgen-legacy-import", output=output, profile=native.BASE_PROFILE, + steam=Path("baseSteam-2.2.15.121011.jar"), steam_sha256=sha, + electric=Path("baseElectric-2.2.15.121011.jar"), electric_sha256=sha, + mineralogy=None, mineralogy_sha256=None) + with (patch.object(native, "read_mods", return_value=mods), patch.object(native, "validate_hash"), + patch.object(native, "jar_identity"), patch.object(native, "write_json") as write): + native.materialize_profile(args) + _, document = write.call_args.args + for key in ("firstProfileAssertions", "restartProfileAssertions", "configFixtures", "worldgenExpectations"): + self.assertEqual(fixture.get(key), document.get(key), key) + self.assertNotIn("powerDeobfSha256", document) + self.assertNotIn("sourceCommit", document) + self.assertEqual("1.21.1", document["minecraftVersion"]) + self.assertEqual(native.EXPLORATORY, document["qualificationStatus"]) + self.assertTrue(all(entry["version"].endswith(".121011") for entry in document["mods"])) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native1211_actor_network_adapters.py b/test-rig/migration/test_native1211_actor_network_adapters.py new file mode 100644 index 00000000..0cfff957 --- /dev/null +++ b/test-rig/migration/test_native1211_actor_network_adapters.py @@ -0,0 +1,168 @@ +"""Fail-closed controls for the Forge 52 lifecycle format adapters.""" + +import copy +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import nbtlib + +import actors +import dimensions +import native1211_actors as actor_driver +import native1211_networks as network_driver +from matrix import WORLD + + +class ActorAdapters(unittest.TestCase): + def test_native_attribute_view_keeps_values_and_opaque_modifiers(self): + original = {"attributes": [{"id": "forge:swim_speed", "base": 2.5, "modifiers": [ + {"id": "other:current", "amount": 0.125, "operation": "add_value"}]}]} + before = copy.deepcopy(original) + actual = actor_driver.native_attribute_view(original) + self.assertEqual({"Attributes": [{"Name": "forge:swim_speed", "Base": 2.5, "Modifiers": [ + {"id": "other:current", "amount": 0.125, "operation": "add_value"}]}]}, actual) + self.assertEqual(before, original) + for malformed in ({"attributes": [{"id": "x:y"}, {"id": "x:y"}]}, + {"attributes": [], "Attributes": []}): + with self.assertRaises(ValueError): + actor_driver.native_attribute_view(malformed) + + def test_preserves_enchantments_damage_slot_count_and_opaque_custom_data(self): + item = {"id": "basesteam:musket", "Count": 2, "Slot": -106, "tag": { + "Damage": 5, "Enchantments": [{"id": "basesteam:recoil", "lvl": 3}], + "StoredEnchantments": [{"id": "basesteam:powderless", "lvl": 1}], + "Nested": {"id": "minecraft:dirt", "Count": 17}, + }} + original = copy.deepcopy(item) + self.assertEqual({ + "id": "basesteam:musket", "count": 2, "Slot": -106, "components": { + "minecraft:damage": 5, + "minecraft:enchantments": {"levels": {"basesteam:recoil": 3}}, + "minecraft:stored_enchantments": {"levels": {"basesteam:powderless": 1}}, + "minecraft:custom_data": {"Nested": {"id": "minecraft:dirt", "Count": 17}}, + }, + }, actor_driver.expected_components(item)) + self.assertEqual(original, item) + + def test_rejects_missing_or_added_native_stack_components(self): + expected = {"file": {"Inventory": [{"id": "minecraft:dirt", "Slot": 3, "count": 1, + "components": {"minecraft:custom_data": {"Note": "original"}}}]}, + "entities": {}} + self.assertEqual("PASS", actor_driver.verdict(actors.compare, expected, copy.deepcopy(expected))["status"]) + for components in ({}, {"minecraft:custom_data": {"Note": "original"}, "minecraft:damage": 7}): + actual = copy.deepcopy(expected) + actual["file"]["Inventory"][0]["components"] = components + result = actor_driver.verdict(actors.compare, expected, actual) + self.assertEqual("FAIL", result["status"]) + self.assertEqual("FAIL", result["player_status"]) + self.assertTrue(any(row["path"].endswith(".exact-item") for row in result["differences"])) + + def test_rejects_ambiguous_native_and_historical_stack(self): + with self.assertRaises(AssertionError): + actor_driver.expected_components({"id": "minecraft:dirt", "Count": 2, "count": 2}) + + def test_dimension_projection_preserves_actual_leash_armor_and_jump_strength(self): + armor = {"id": "minecraft:iron_horse_armor", "count": 1, + "components": {"minecraft:custom_data": {"Note": "witness"}}} + horse = {"id": "minecraft:horse", "leash": [168, 100, 8], "body_armor_item": armor, + "Attributes": [{"Name": "minecraft:generic.jump_strength", "Base": 0.7}]} + result = actor_driver.dimension_entity_view(horse) + self.assertEqual({"X": 168, "Y": 100, "Z": 8}, result["Leash"]) + self.assertEqual(armor, result["ArmorItem"]) + self.assertEqual([{"Name": "horse.jumpStrength", "Base": 0.7}], result["Attributes"]) + + def test_ambiguous_or_malformed_native_leash_is_not_normalized(self): + for tag in ({"leash": [168, 100, True]}, {"leash": [168, 100, 8], "Leash": {"X": 5}}): + with self.subTest(tag=tag), self.assertRaises(ValueError): + actor_driver.dimension_entity_view({"id": "minecraft:horse", **tag}) + + def test_fixture_copy_does_not_replace_native_forge_config(self): + with tempfile.TemporaryDirectory() as folder: + source, runtime = Path(folder) / "source", Path(folder) / "runtime" + (source / WORLD).mkdir(parents=True) + (source / "config").mkdir() + (source / "migration-fixture.json").write_text("{}", encoding="utf-8") + (source / "config/poweradvantage.cfg").write_text("historical", encoding="utf-8") + (runtime / "config").mkdir(parents=True) + (runtime / "config/fml.toml").write_text("native", encoding="utf-8") + actor_driver.copy_fixture(source, runtime) + self.assertEqual("native", (runtime / "config/fml.toml").read_text(encoding="utf-8")) + self.assertEqual("historical", (runtime / "config/poweradvantage.cfg").read_text(encoding="utf-8")) + + +class NativeDifferenceAssessment(unittest.TestCase): + def setUp(self): + self.before = {"entities": {"passenger-wolf": { + "id": "minecraft:wolf", "Health": 8, + "Attributes": {"generic.maxHealth": {"Base": 20}}, + "UUIDMost": 5, "UUIDLeast": 7, "OwnerUUID": "owner"}}, + "player": {"Inventory": [{"id": "minecraft:dirt", "Count": 17}]}} + self.after = copy.deepcopy(self.before) + self.after["entities"]["passenger-wolf"]["Health"] = 16 + self.after["entities"]["passenger-wolf"]["Attributes"]["generic.maxHealth"]["Base"] = 40 + + def assess(self, actual, proof=Path("controls")): + strict = actor_driver.verdict(dimensions.verdict, self.before, actual) + with patch.object(actor_driver.native, "validate_hash") as hashes, \ + patch.object(actor_driver.native, "read_json", return_value={"noPower": True}): + result = actor_driver.assess_native(self.before, actual, strict, proof, "scope", None) + return result, hashes + + def test_only_native_wolf_change_is_classified_and_raw_failure_is_kept(self): + result, hashes = self.assess(self.after) + self.assertEqual("PASS_WITH_NATIVE_DIFFERENCE", result["status"]) + self.assertEqual("FAIL", result["rawStrictResult"]["status"]) + self.assertEqual(2, hashes.call_count) + self.assertEqual(8, self.before["entities"]["passenger-wolf"]["Health"]) + + def test_unrelated_inventory_loss_owner_change_or_wrong_health_stays_failed(self): + variants = [] + for key, value in (("OwnerUUID", "another"), ("Health", 15), ("UUIDLeast", 9)): + changed = copy.deepcopy(self.after) + changed["entities"]["passenger-wolf"][key] = value + variants.append(changed) + changed = copy.deepcopy(self.after) + changed["player"]["Inventory"][0]["Count"] = 16 + variants.append(changed) + for actual in variants: + with self.subTest(actual=actual): + self.assertEqual("FAIL", self.assess(actual)[0]["status"]) + self.assertEqual("FAIL", self.assess(self.after, proof=None)[0]["status"]) + + def test_control_hash_failure_is_not_accepted(self): + strict = actor_driver.verdict(dimensions.verdict, self.before, self.after) + with patch.object(actor_driver.native, "validate_hash", side_effect=ValueError("changed control")): + with self.assertRaises(ValueError): + actor_driver.assess_native(self.before, self.after, strict, Path("controls"), "scope", None) + + +class NetworkAdapters(unittest.TestCase): + def test_native_witness_inventory_command(self): + item = 'minecraft:dirt{AdvWorksWitness:"S-EXT-north"}' + self.assertEqual('minecraft:dirt[minecraft:custom_data={AdvWorksWitness:"S-EXT-north"}]', + network_driver.native_inventory_item(item)) + self.assertEqual("minecraft:apple", network_driver.native_inventory_item("minecraft:apple")) + + def test_compatibility_view_retains_native_components_and_does_not_edit_observations(self): + source = {"callbacks": {"drill": {"after": {"nbt": + '{Items:[{id:"minecraft:dirt",Slot:0b,count:7,' + 'components:{"minecraft:custom_data":{AdvWorksWitness:"test"},"other:component":5}}]}'}}}} + original = copy.deepcopy(source) + projected = network_driver.compatibility_view(source) + stack = nbtlib.parse_nbt(projected["callbacks"]["drill"]["after"]["nbt"])["Items"][0] + self.assertEqual(7, stack["count"]) + self.assertEqual(7, stack["Count"]) + self.assertEqual("test", stack["tag"]["AdvWorksWitness"]) + self.assertEqual(5, stack["components"]["other:component"]) + self.assertEqual(original, source) + + def test_legacy_or_badly_typed_stack_is_not_native_evidence(self): + for count in ("Count:7b", "count:7b", "count:0"): + with self.subTest(count=count), self.assertRaises(AssertionError): + network_driver.compatibility_view({"nbt": '{Items:[{id:"minecraft:dirt",' + count + '}]}'}) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native1211_machine_adapters.py b/test-rig/migration/test_native1211_machine_adapters.py new file mode 100644 index 00000000..317d74f1 --- /dev/null +++ b/test-rig/migration/test_native1211_machine_adapters.py @@ -0,0 +1,68 @@ +"""Fail-closed checks for native saved terrain and the optional oil input pin.""" + +import io +from pathlib import Path +import unittest +from unittest.mock import Mock +import zlib + +import nbtlib + +from native1211_machines import LifecycleRun, MINERALOGY, oil_pins +from native1211_operations import modern_chunk + + +class MachineAdapters(unittest.TestCase): + def test_lifecycle_inventory_uses_native_custom_data_command(self): + runner = object.__new__(LifecycleRun) + runner.command = Mock() + runner.inventory([1, 2, 3], 0, 'minecraft:dirt{AdvWorksWitness:"S-MOVE-east"}', 7) + runner.command.assert_called_once_with( + 'item replace block 1 2 3 container.0 with ' + 'minecraft:dirt[minecraft:custom_data={AdvWorksWitness:"S-MOVE-east"}] 7') + + def test_oil_input_keeps_verified_source_and_rejects_another_build(self): + entry = (MINERALOGY["version"], Path("mineralogy.jar"), MINERALOGY["sha256"]) + pins = oil_pins({"mineralogy": entry}) + self.assertEqual(MINERALOGY["sourceRef"], pins["jars"][0]["sourceRef"]) + self.assertNotIn("sourceRef", oil_pins({"basepower": ("v", Path("p.jar"), "A" * 64)})["jars"][0]) + for version, sha in (("wrong", entry[2]), (entry[0], "B" * 64)): + with self.assertRaises(ValueError): + oil_pins({"mineralogy": (version, entry[1], sha)}) + + @staticmethod + def terrain(version=3955, legacy=False): + root = nbtlib.File({ + "DataVersion": nbtlib.Int(version), "xPos": nbtlib.Int(3), "zPos": nbtlib.Int(-2), + "sections": nbtlib.List[nbtlib.Compound]([nbtlib.Compound({ + "Y": nbtlib.Byte(4), "block_states": nbtlib.Compound({ + "palette": nbtlib.List[nbtlib.Compound]([nbtlib.Compound({ + "Name": nbtlib.String("basepower:fluid_container")})]), + "data": nbtlib.LongArray([17, -3])})})]), + "block_entities": nbtlib.List[nbtlib.Compound]([nbtlib.Compound({ + "id": nbtlib.String("basepower:fluid_container"), + "Tank": nbtlib.Compound({"Amount": nbtlib.Int(1234)})})]), + }) + if legacy: + root["Level"] = nbtlib.Compound() + raw = io.BytesIO() + root.write(raw) + compressed = b"\x02" + zlib.compress(raw.getvalue()) + return len(compressed).to_bytes(4, "big") + compressed + + def test_saved_chunk_accessors_preserve_typed_payload(self): + observed = modern_chunk(self.terrain()) + self.assertEqual(1234, int(observed["TileEntities"][0]["Tank"]["Amount"])) + self.assertIsInstance(observed["TileEntities"][0]["Tank"]["Amount"], nbtlib.Int) + self.assertEqual([17, -3], list(observed["Sections"][0]["BlockStates"])) + self.assertEqual("basepower:fluid_container", observed["Sections"][0]["Palette"][0]["Name"]) + self.assertEqual(-2, int(observed["zPos"])) + + def test_wrong_version_legacy_shape_and_broken_length_are_rejected(self): + for payload in (self.terrain(version=3839), self.terrain(legacy=True), self.terrain()[:-1]): + with self.subTest(payload=payload[:12]), self.assertRaises(ValueError): + modern_chunk(payload) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/platform-controls/NativeLeashKnotPlatformControl.java b/test-rig/platform-controls/NativeLeashKnotPlatformControl.java index 9ca7ab12..4c467a09 100644 --- a/test-rig/platform-controls/NativeLeashKnotPlatformControl.java +++ b/test-rig/platform-controls/NativeLeashKnotPlatformControl.java @@ -56,7 +56,7 @@ public static void main(String[] args) throws Exception { || 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", "native1206-leash-knot-nosave-v1"); + proof.put("format", 1); proof.put("policy", "native1211-leash-knot-nosave-v1"); proof.put("minecraftVersion", SharedConstants.getCurrentVersion().getName()); proof.put("dataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion()); proof.put("forgeVersion", net.minecraftforge.versions.forge.ForgeVersion.getVersion()); diff --git a/test-rig/platform-controls/NativeWolfPlatformControl.java b/test-rig/platform-controls/NativeWolfPlatformControl.java index dca148e7..43f9c3a9 100644 --- a/test-rig/platform-controls/NativeWolfPlatformControl.java +++ b/test-rig/platform-controls/NativeWolfPlatformControl.java @@ -28,7 +28,7 @@ import net.minecraftforge.registries.ForgeRegistries; import zone.moddev.mc.advantageworks.unit.ForgeUnitRegistries; -/** Native load/save control: no Advantage classes, server, entity ticks or DFU. */ +/** Independent native controls: no Advantage classes, server or entity ticks. */ 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"); @@ -87,6 +87,36 @@ private static void control(Path root, ServerLevel world, boolean tame, float he System.out.println("WOLF_CONTROL " + new Gson().toJson(row)); } + private static void legacyControl(Path root, ServerLevel world, int version) throws Exception { + CompoundTag before = net.minecraft.nbt.TagParser.parseTag("{id:\"" + (version == 512 ? "Wolf" : "minecraft:wolf") + + "\",Health:8f,Attributes:[{Name:\"generic.maxHealth\",Base:20d}],Pos:[0d,100d,0d]," + + "Motion:[0d,0d,0d],Rotation:[0f,0f]}"); + before.putLong("UUIDMost", ID.getMostSignificantBits()); + before.putLong("UUIDLeast", ID.getLeastSignificantBits()); + before.putString("OwnerUUID", OWNER.toString()); + var fixed = net.minecraft.util.datafix.DataFixers.getDataFixer().update( + net.minecraft.util.datafix.fixes.References.ENTITY, + new com.mojang.serialization.Dynamic<>(net.minecraft.nbt.NbtOps.INSTANCE, before.copy()), + version, SharedConstants.getCurrentVersion().getDataVersion().getVersion()); + CompoundTag nativeTag = (CompoundTag) fixed.getValue(); + Wolf loaded = wolf(world); + loaded.load(nativeTag); + CompoundTag saved = loaded.saveWithoutId(new CompoundTag()); + NbtIo.writeCompressed(before, root.resolve("legacy-" + version + "-source.dat")); + NbtIo.writeCompressed(nativeTag, root.resolve("legacy-" + version + "-dfu.dat")); + NbtIo.writeCompressed(saved, root.resolve("legacy-" + version + "-saved.dat")); + if (loaded.getHealth() != 16 || loaded.getMaxHealth() != 40 || !ID.equals(loaded.getUUID()) + || !OWNER.equals(loaded.getOwnerUUID())) throw new AssertionError("Unexpected native legacy-wolf conversion"); + Map row = new LinkedHashMap<>(); + row.put("sourceDataVersion", version); row.put("targetDataVersion", 3955); + row.put("sourceHealth", 8); row.put("savedHealth", loaded.getHealth()); + row.put("sourceMaxHealth", 20); row.put("savedMaxHealth", loaded.getMaxHealth()); + row.put("uuidPreserved", true); row.put("ownerPreserved", true); row.put("noPower", true); + String json = new Gson().toJson(row); + Files.writeString(root.resolve("legacy-" + version + "-receipt.json"), json + "\n"); + System.out.println("WOLF_LEGACY_DFU_CONTROL " + json); + } + public static void main(String[] args) throws Exception { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); ForgeUnitRegistries.registerVanillaFluidTypes(); @@ -99,6 +129,7 @@ public static void main(String[] args) throws Exception { 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); + legacyControl(root, world, 512); legacyControl(root, world, 1343); System.out.println("WOLF_CONTROL_DONE no_server=true no_ticks=true native_entity_methods_unmodified=true"); } } diff --git a/test-rig/profiles/native1211/README.md b/test-rig/profiles/native1211/README.md new file mode 100644 index 00000000..f88e9343 --- /dev/null +++ b/test-rig/profiles/native1211/README.md @@ -0,0 +1,68 @@ +# Forge 52 Test Runs + +These profiles are for Minecraft 1.21.1 and Java 21. They are not permission +to open a real world. Keep the server seed, source worlds and passing source +receipts separate from each new output directory. + +`power-interop.json` pins Base Metals and OreSpawn. `dev-core.json` pins the +two expansions for the combined Eclipse launch. Override dependency locations +through the build properties rather than copying jars into source control. +The local Base Metals migration patch is required for old-world tests; the +older public build is not an equivalent input. + +## Preparing Inputs + +Build Power and the harness, then Steam and Electric against the same Power +jar. Use `migration/native1211.py profile --help` to generate an ignored +packaged profile containing the exact player and harness jar hashes. Stage +only those jars in a disposable runtime. The harness must never enter the +player release jar or the manual client mod list. + +Install `migration/requirements.txt` in a local Python environment. From +`test-rig`, the native drivers expose their required arguments with `--help`: + +| Driver | Evidence | +| --- | --- | +| `native1211_broad.py` | All walkable stations, shared and decoupled runtimes | +| `native1211_operations.py` | Complete finite machine jobs, raw stopped saves and restart | +| `native1211_recipes.py` | Native unlock packets, all recipe modes and survival progression | +| `native1211_upgrade.py` | Hash-checked historical/retained worlds, state and connected operation | +| `native1211_actors.py` | Players, nested items, entities, dimensions and stopped entity storage | +| `native1211_networks.py` | Partial/bridge unloads, extended drills and fluid API interoperability | +| `native1211_machines.py` | Harvest, cross-mod oil transport and moving-machine lifecycle | +| `native1211_endurance.py` | Fresh and migrated 30-minute runs, restart and resumed processing | +| `audit_native1211_endurance.py` | Independent replay of every saved endurance checkpoint | + +Run the Python regression checks with: + +```text +python -B -m unittest discover -s migration -p "test_*.py" +``` + +The shared layouts and older semantic oracles deliberately remain unchanged. +The native adapters translate representation and commands, not quantities or +expected results. In particular, native item components must retain their +custom data; a test setup using old item-command syntax is a failed run, +not evidence of lost production items. + +## Evidence Rules + +Keep failed, held and short smoke runs. Do not overwrite them with a later +pass. A smoke duration below 1,800 seconds is not an endurance pass. The +full run also requires at least 18 measured game ticks per second and a +minute of successful operation after a full server-process restart. + +The independent replay uses the same immutable station specification as +the run, currently `src/main/resources/assets/advantageworkstest/works/works-1.13.2.json`. +Its historical filename records the shared layout, not the running platform. +The driver verifies the actual Minecraft/Forge identities in every checkpoint. + +Dimension reports retain the strict failure for Minecraft's independently +proven wolf-health change. Only the exact pinned native control may explain +that difference. Unrelated item loss, ownership changes or missing entities +still fail. See the [port record](../../../docs/1.21.1/PORT-STATUS.md). + +Client pixel, GUI caption and recipe-book checks are separate from packaged +server operation. See the [client probe instructions](../../client-resources/README.md). +Real player login, live animations, tooltips and human visual acceptance +remain manual checks. diff --git a/test-rig/profiles/native1211/dev-core.json b/test-rig/profiles/native1211/dev-core.json new file mode 100644 index 00000000..250a77ab --- /dev/null +++ b/test-rig/profiles/native1211/dev-core.json @@ -0,0 +1,19 @@ +{ + "name": "dev-core", + "mode": "development", + "minecraftVersion": "1.21.1", + "forgeVersion": "52.1.0", + "powerDeobfSha256": "F91A613EB5FAA218AAD2DC3C881DEC7B897A5EE874215282A439E34B915C1AE6", + "mods": [ + { + "id": "basesteam", + "version": "2.2.15.121011", + "sha256": "EA305B9060BC2A84021C7299249AA79AA593EA8A37665C18D0B3483902F1046A" + }, + { + "id": "baseelectric", + "version": "2.2.15.121011", + "sha256": "CFE7EF9CDAB3BD4DB0342562B2D9CEBAA19E78119C4D4AAD4B637B5BF0A59560" + } + ] +} diff --git a/test-rig/profiles/native1211/power-interop.json b/test-rig/profiles/native1211/power-interop.json new file mode 100644 index 00000000..1d2bed3b --- /dev/null +++ b/test-rig/profiles/native1211/power-interop.json @@ -0,0 +1,22 @@ +{ + "name": "power-interop", + "mode": "packaged", + "minecraftVersion": "1.21.1", + "forgeVersion": "52.1.0", + "qualificationStatus": "EXPLORATORY_UNQUALIFIED", + "baseMetalsSourceCommit": "b42b92b0e6f60ac5baf07a6b74042b2fa8427904", + "mods": [ + { + "id": "basemetals", + "version": "3.1.2.121011", + "path": "../../../../BaseMetals/.verification-basemetals1211/Advantage shared hook/build/libs/BaseMetals-3.1.2.121011.jar", + "sha256": "7C0B961A8D4991C9B69379518AAD700A71C1E4B2076CF5DEF292C58029408A07" + }, + { + "id": "orespawn", + "version": "4.1.0.121011", + "path": "../../../../MinecraftMineralogy/OS1211/OreSpawn/build/libs/OreSpawn-4.1.0.121011.jar", + "sha256": "EB369B7D7CE6D6C453658347A420C2F0783B7F4D1AC7383C87176AC05192B5A0" + } + ] +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java index b216b3a4..dbd87c96 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java @@ -7,7 +7,7 @@ @Mod(AdvantageWorksMod.MODID) public final class AdvantageWorksMod { public static final String MODID = "advantageworkstest"; - public static final String VERSION = "1.1.4.120061"; + public static final String VERSION = "1.1.5.121011"; public AdvantageWorksMod() { MigrationProbe.register(); 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 f35e7f3c..6dae4686 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 @@ -22,7 +22,8 @@ static void register(List cases) { }, 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"); + c.require(c.world.getBlockState(c.at(8, 4, 8)).getLightEmission(c.world, c.at(8, 4, 8)) == 15, + "battery powers the live LED at full light output"); 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()), 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 1a339af7..5582a7e3 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 @@ -334,7 +334,8 @@ private JsonObject snapshot(MinecraftServer server, ServerLevel world, JsonObjec 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(net.minecraftforge.registries.ForgeRegistries.ENTITY_TYPES.getKey(leash.getType()).toString())); - BlockPos leashPos = leash instanceof HangingEntity ? ((HangingEntity) leash).getPos() : null; + BlockPos leashPos = leash instanceof net.minecraft.world.entity.decoration.BlockAttachedEntity attached + ? attached.getPos() : null; entry.add("leashHolderPosition", leashPos == null ? JsonNull.INSTANCE : JSON.toJsonTree(new int[] {leashPos.getX(), leashPos.getY(), leashPos.getZ()})); entities.add(entry); 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 45fd4c29..8a89f04c 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 @@ -210,7 +210,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.world.level.material.Fluid oil = ForgeRegistries.FLUIDS.getValue(new ResourceLocation("basepower:crude_oil")); + net.minecraft.world.level.material.Fluid oil = ForgeRegistries.FLUIDS.getValue(ResourceLocation.parse("basepower:crude_oil")); c.require(oil != null, "Crude oil exists for faced automation"); int stored = 0; for (Direction face : Direction.values()) { @@ -229,7 +229,7 @@ private static void still(List cases) { 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("basepower:refined_oil").equals(net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(simulated.getFluid())), "Face offers the real refined product " + face); + c.require(ResourceLocation.parse("basepower:refined_oil").equals(net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(simulated.getFluid())), "Face offers the real refined product " + face); equal(c, "face." + face + ".drainSimulationUnchanged", tankAmount(c, 8, 1), 24); } })); @@ -348,8 +348,8 @@ private static void led(List cases) { c.block(22, Y, Z, EA + "led_bar"); c.record("led.constructionGameTime", c.world.getGameTime()); }, c -> { - c.require(c.world.getBlockState(c.at(8, Y, Z)).getValue(BlockStateProperties.LIT), "Powered LED is lit"); - c.require(!c.world.getBlockState(c.at(22, Y, Z)).getValue(BlockStateProperties.LIT), "Unpowered LED is dark"); + equal(c, "poweredLED.lightEmission", ledLight(c, 8, Y, Z), 15); + equal(c, "unpoweredLED.lightEmission", ledLight(c, 22, Y, Z), 0); float debit = 10 - pairEnergy(c, 8); recordSchedule(c, "led", c.activationTick, 8, Y, Z, 80); recordSchedule(c, "battery", c.activationTick, 7, Y, Z, 80); @@ -365,7 +365,7 @@ private static void led(List cases) { c.electricSupply(7, Y, Z, 0.5f); }, c -> { close(c, "exhausted.energy", pairEnergy(c, 8), 0, 0.001f); - c.require(!c.world.getBlockState(c.at(8, Y, Z)).getValue(BlockStateProperties.LIT), "Exhausted LED turns off"); + equal(c, "exhaustedLED.lightEmission", ledLight(c, 8, Y, Z), 0); })); } @@ -406,7 +406,7 @@ private static void hydro(List cases) { int turbines = 0; for (Entity actor : c.world.getEntitiesOfClass(Entity.class, WorksBlockData.bounds(c.at(0, 0, 0), c.at(31, 31, 31)))) { - if (!new ResourceLocation(EA + "hydroturbine").equals(net.minecraftforge.registries.ForgeRegistries.ENTITY_TYPES.getKey(actor.getType()))) continue; + if (!ResourceLocation.parse(EA + "hydroturbine").equals(net.minecraftforge.registries.ForgeRegistries.ENTITY_TYPES.getKey(actor.getType()))) continue; turbines++; c.record("hydro.motion/" + actor.getUUID(), actor.getDeltaMovement().toString()); c.require(actor.getDeltaMovement().lengthSqr() == 0, @@ -553,7 +553,7 @@ private static void batteries(List cases) { c.record("priority.backupStored", backup); close(c, "priority.furnaceAllocation", furnace, 99.5f, 0.001f); close(c, "priority.backupAllocation", backup, 0, 0.001f); - c.require(!c.world.getBlockState(c.at(8, Y, Z - 1)).getValue(BlockStateProperties.LIT), "One-time half-unit LED allocation expires without creating energy"); + equal(c, "finiteReserveLED.lightEmission", ledLight(c, 8, Y, Z - 1), 0); close(c, "priority.totalStored", source + furnace + led + backup, 99.5f, 0.001f); close(c, "noCycle.charged", c.energy(22, Y, Z, ELECTRICITY), 500, 0.001f); close(c, "noCycle.empty", c.energy(23, Y, Z, ELECTRICITY), 0, 0.001f); @@ -683,6 +683,14 @@ private static void turretRoom(MachineContext c) { } } + private static int ledLight(MachineContext c, int x, int y, int z) { + var pos = c.at(x, y, z); + var state = c.world.getBlockState(pos); + c.require(ResourceLocation.parse(EA + "led_bar").equals(ForgeRegistries.BLOCKS.getKey(state.getBlock())), + "Light observation targets the real LED"); + return state.getLightEmission(c.world, pos); + } + private static float pairEnergy(MachineContext c, int x) { return energyPair(c, x, Y, Z); } private static int phase(MachineContext c, int x, int y, int z) { @@ -724,7 +732,7 @@ private static int output(MachineContext c, int x, int first, int slots, String } private static int stackCount(ItemStack stack, String id) { - return stack == null || stack.isEmpty() || !new ResourceLocation(id).equals(net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(stack.getItem())) ? 0 : stack.getCount(); + return stack == null || stack.isEmpty() || !ResourceLocation.parse(id).equals(net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(stack.getItem())) ? 0 : stack.getCount(); } private static IFluidHandler fluids(MachineContext c, int x) { @@ -741,7 +749,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.world.level.material.Fluid fluid = ForgeRegistries.FLUIDS.getValue(new ResourceLocation(id)); + net.minecraft.world.level.material.Fluid fluid = ForgeRegistries.FLUIDS.getValue(ResourceLocation.parse(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(); @@ -761,11 +769,11 @@ private static int tankAmount(MachineContext c, int x, int tank) { private static void fluidIs(MachineContext c, int x, int tank, String id) { FluidStack fluid = fluids(c, x).getFluidInTank(tank); c.record("fluid." + x + "." + tank, String.valueOf(net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(fluid.getFluid()))); - c.require(new ResourceLocation(id).equals(net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(fluid.getFluid())), "Tank contains " + id); + c.require(ResourceLocation.parse(id).equals(net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(fluid.getFluid())), "Tank contains " + id); } private static void rejectFluid(MachineContext c, int x, String id, int amount) { - net.minecraft.world.level.material.Fluid fluid = ForgeRegistries.FLUIDS.getValue(new ResourceLocation(id)); + net.minecraft.world.level.material.Fluid fluid = ForgeRegistries.FLUIDS.getValue(ResourceLocation.parse(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/LifecycleProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleProbe.java index cb580ecb..da4e0e84 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 @@ -233,7 +233,7 @@ private void execute(ServerLevel world, String operation, String label) { BlockPos source = position(oil.source); if (!world.isEmptyBlock(source)) throw new IllegalStateException("Oil source is not empty: " + oil.id); net.minecraft.world.item.Item item = net.minecraftforge.registries.ForgeRegistries.ITEMS.getValue( - new net.minecraft.resources.ResourceLocation(oil.bucket)); + net.minecraft.resources.ResourceLocation.parse(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); 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 cd825eef..caa3e842 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 @@ -92,7 +92,7 @@ void requireTickingFoundation() { } void block(int x, int y, int z, String id) { - ResourceLocation name = new ResourceLocation(id); + ResourceLocation name = ResourceLocation.parse(id); if (!ForgeRegistries.BLOCKS.containsKey(name)) throw new IllegalArgumentException("Missing block " + id); mutation("block", at(x, y, z), id); world.setBlock(at(x, y, z), ForgeRegistries.BLOCKS.getValue(name).defaultBlockState(), 3); @@ -128,7 +128,7 @@ Container inventory(int x, int y, int z) { } void put(int x, int y, int z, int slot, String item, int count) { - ResourceLocation id = new ResourceLocation(item); + ResourceLocation id = ResourceLocation.parse(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); Container inventory = inventory(x, y, z); @@ -138,8 +138,8 @@ 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); + Item expected = ForgeRegistries.ITEMS.getValue(ResourceLocation.parse(item)); + if (!ForgeRegistries.ITEMS.containsKey(ResourceLocation.parse(item))) throw new IllegalArgumentException("Missing item " + item); Container inventory = inventory(x, y, z); int total = 0; for (int slot = 0; slot < inventory.getContainerSize(); slot++) { @@ -180,7 +180,7 @@ void electricSupply(int x, int y, int z, float reserve) { } void tank(int x, int y, int z, String fluid, int amount) { - ResourceLocation id = new ResourceLocation(fluid); + ResourceLocation id = ResourceLocation.parse(fluid); if (!ForgeRegistries.FLUIDS.containsKey(id) || amount < 0) throw new IllegalArgumentException("Invalid fluid reserve"); mutation("tank", at(x, y, z), fluid + ":" + amount); cyano.poweradvantage.api.fluid.FluidTank tank = ((TileEntitySimpleFluidMachine) tile(x, y, z)).getTank(); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2.java index ed2eea9c..0d2bb282 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2.java @@ -35,8 +35,8 @@ /** 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"; + static final String CONTRACT = "native1211-helper-contract.json"; + static final String POLICY = "native1211-leash-knot-nosave-v1"; private static final Map BOOTS = Collections.synchronizedMap(new WeakHashMap<>()); private Native117StoppedEntitiesSnapshotV2() {} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117WorldgenAccess.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117WorldgenAccess.java index 5f3cce3a..e9d59ba8 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117WorldgenAccess.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117WorldgenAccess.java @@ -36,7 +36,7 @@ final class Native117WorldgenAccess implements WorldgenSamplingJob.WorldAccess { return source.getChunkFuture(x, z, ChunkStatus.FULL, true).thenApply(result -> { net.minecraft.world.level.chunk.ChunkAccess access = result.orElseThrow(() -> new IllegalStateException("Native FULL chunk unavailable: " + result.getError())); - if (!(access instanceof LevelChunk) || access.getStatus() != ChunkStatus.FULL + if (!(access instanceof LevelChunk) || access.getPersistedStatus() != 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"); } 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 cf5400cc..fbf59024 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 @@ -77,7 +77,7 @@ private static void witness(ServerLevel world, ServerPlayer player, JsonArray ro JsonArray dropEvidence = new JsonArray(); row.add("dropEvidence", dropEvidence); try { - ResourceLocation key = new ResourceLocation(blockId); + ResourceLocation key = ResourceLocation.parse(blockId); require(ForgeRegistries.BLOCKS.containsKey(key), "Missing real block: " + blockId); Block block = ForgeRegistries.BLOCKS.getValue(key); player.gameMode.changeGameModeForPlayer(GameType.SURVIVAL); @@ -125,7 +125,8 @@ private static void witness(ServerLevel world, ServerPlayer player, JsonArray ro 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); + if (silk) tool.enchant(world.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.ENCHANTMENT) + .getOrThrow(Enchantments.SILK_TOUCH), 1); 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), @@ -203,7 +204,7 @@ private static void run(ServerLevel world, String label) throws Exception { JsonObject solar = new JsonObject(); solar.addProperty("id", "solar/native-solidity"); try { - Block block = ForgeRegistries.BLOCKS.getValue(new ResourceLocation("baseelectric:photovoltaic_generator")); + Block block = ForgeRegistries.BLOCKS.getValue(ResourceLocation.parse("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"); 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 30318362..b34d5ee1 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 @@ -77,7 +77,7 @@ private static void check(JsonArray checks, String id, Runnable body) { } private static Block block(String id) { - ResourceLocation key = new ResourceLocation(id); + ResourceLocation key = ResourceLocation.parse(id); require(ForgeRegistries.BLOCKS.containsKey(key), "Missing registry entry: " + id); return ForgeRegistries.BLOCKS.getValue(key); } @@ -342,21 +342,21 @@ private static void run(ServerLevel world, String phase) throws Exception { @SuppressWarnings("unchecked") private static void sharedCrushing(ServerLevel world) { - ResourceLocation recipeId = new ResourceLocation("basepower", "crushing_potato_starch"); + ResourceLocation recipeId = ResourceLocation.fromNamespaceAndPath("basepower", "crushing_potato_starch"); var holder = world.getRecipeManager().byKey(recipeId).orElseThrow( () -> new AssertionError("Missing Power recipe under Base Metals crushing: " + recipeId)); - var recipe = (net.minecraft.world.item.crafting.Recipe) holder.value(); + var recipe = (net.minecraft.world.item.crafting.Recipe) holder.value(); var type = net.minecraft.core.registries.BuiltInRegistries.RECIPE_TYPE.getKey(recipe.getType()); - require(new ResourceLocation("basemetals", "crushing").equals(type), "Wrong loaded crushing type: " + type); + require(ResourceLocation.fromNamespaceAndPath("basemetals", "crushing").equals(type), "Wrong loaded crushing type: " + type); for (var potato : List.of(net.minecraft.world.item.Items.POTATO, net.minecraft.world.item.Items.POISONOUS_POTATO)) { - var input = new net.minecraft.world.SimpleContainer(new ItemStack(potato, 3)); + var input = new net.minecraft.world.item.crafting.SingleRecipeInput(new ItemStack(potato, 3)); require(recipe.matches(input, world), "Shared crusher rejected " + potato); ItemStack result = recipe.assemble(input, world.registryAccess()); - require(result.getCount() == 1 && new ResourceLocation("basepower", "starch").equals( + require(result.getCount() == 1 && ResourceLocation.fromNamespaceAndPath("basepower", "starch").equals( ForgeRegistries.ITEMS.getKey(result.getItem())), "Incorrect shared crushing result: " + result); require(input.getItem(0).getCount() == 3, "Assembling a recipe preview mutated its input"); } - require(!recipe.matches(new net.minecraft.world.SimpleContainer(new ItemStack(net.minecraft.world.item.Items.DIRT)), world), + require(!recipe.matches(new net.minecraft.world.item.crafting.SingleRecipeInput(new ItemStack(net.minecraft.world.item.Items.DIRT)), world), "Shared crushing accepted unrelated input"); } } 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 29e058ff..dc892fc7 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 @@ -44,7 +44,7 @@ static Map observe(ServerLevel world) { Map result = new LinkedHashMap<>(); result.put("fullLevel", fullLevel); result.put("entityTickingLevel", entityTickingLevel); - result.put("maxDistance", ChunkStatus.maxDistance()); + result.put("maxDistance", ChunkLevel.RADIUS_AROUND_FULL_CHUNK); result.put("lastHolderLevel", ChunkLevel.MAX_LEVEL); result.put("holderRadius", ChunkLevel.MAX_LEVEL - entityTickingLevel); result.put("observedForcedLevels", new ArrayList<>(forcedLevels)); 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 c563a649..ebc87bac 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 @@ -181,7 +181,7 @@ private static void stills(List cases) { (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.world.level.material.Fluids.WATER - : net.minecraftforge.registries.ForgeRegistries.FLUIDS.getValue(new net.minecraft.resources.ResourceLocation("basepower:crude_oil")), 1000)); + : net.minecraftforge.registries.ForgeRegistries.FLUIDS.getValue(net.minecraft.resources.ResourceLocation.parse("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, "basepower:refined_oil", output); if (mode.equals("redstone")) c.block(9, 4, 8, "minecraft:redstone_block"); 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 517b9c9c..fd6d756b 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 @@ -124,15 +124,16 @@ private static JsonObject hammerRecipeSelection(ServerLevel world, ItemStack ham selection.addProperty("inputNbt", input.saveOptional(world.registryAccess()).toString()); selection.addProperty("recipeManagerIdentity", System.identityHashCode(world.getRecipeManager())); selection.addProperty("requestedIdPresent", - world.getRecipeManager().byKey(new ResourceLocation(requestedRecipe)).isPresent()); + world.getRecipeManager().byKey(ResourceLocation.parse(requestedRecipe)).isPresent()); try { Class hammerClass = hammer.getItem().getClass(); if (!hammerClass.getName().equals("zone.moddev.mc.basemetals.content.CrackhammerItem")) throw new IllegalStateException("Unexpected hammer class " + hammerClass.getName()); RecipeType type = net.minecraft.core.registries.BuiltInRegistries.RECIPE_TYPE - .getOptional(new ResourceLocation("basemetals", "crushing")).orElseThrow(); + .getOptional(ResourceLocation.fromNamespaceAndPath("basemetals", "crushing")).orElseThrow(); net.minecraft.world.item.crafting.RecipeHolder selected = world.getRecipeManager().getRecipeFor( - (RecipeType>) type, new SimpleContainer(input.copy()), world).orElse(null); + (RecipeType>) type, + new net.minecraft.world.item.crafting.SingleRecipeInput(input.copy()), world).orElse(null); selection.addProperty("status", selected == null ? "NO_MATCH" : "MATCH"); selection.addProperty("matched", selected != null); if (selected != null) { @@ -322,7 +323,7 @@ private static BlockEntity fresh(ServerLevel world, JsonObject fixture, BlockPos && p[i] <= bounds.getAsJsonArray("max").get(i).getAsInt(), "Machine fixture escaped its bounds"); world.setBlockAndUpdate(pos, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState()); world.removeBlockEntity(pos); - net.minecraft.world.level.block.Block material = ForgeRegistries.BLOCKS.getValue(new ResourceLocation(block)); + net.minecraft.world.level.block.Block material = ForgeRegistries.BLOCKS.getValue(ResourceLocation.parse(block)); require(material != null, "Missing machine/fluid " + block); world.setBlockAndUpdate(pos, material.defaultBlockState()); BlockEntity clean = world.getBlockEntity(pos); 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 ab98d3d8..2f18238d 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 @@ -98,7 +98,7 @@ private void audit(MinecraftServer server, ServerLevel world, String label) thro Set executed = new LinkedHashSet<>(); for (JsonElement seed : fixture.getAsJsonArray("naturalResources")) { String id = seed.getAsString(); - Item item = ForgeRegistries.ITEMS.getValue(new ResourceLocation(id)); + Item item = ForgeRegistries.ITEMS.getValue(ResourceLocation.parse(id)); if (item != null) acquire(known, player, new ItemStack(item), "natural_resource", Collections.emptyList()); } JsonArray loot = new JsonArray(); @@ -175,7 +175,7 @@ private void audit(MinecraftServer server, ServerLevel world, String label) thro } if (result.isEmpty()) continue; if (owned(holder.id()) && !crushing) { - AdvancementHolder unlock = server.getAdvancements().get(new ResourceLocation( + AdvancementHolder unlock = server.getAdvancements().get(ResourceLocation.fromNamespaceAndPath( holder.id().getNamespace(), "recipes/" + holder.id().getPath())); require(unlock != null && player.getAdvancements().getOrStartProgress(unlock).isDone() && player.getRecipeBook().contains(holder), "Manufacture lacks ingredient discovery: " + holder.id()); @@ -236,7 +236,7 @@ private void audit(MinecraftServer server, ServerLevel world, String label) thro JsonObject tags = new JsonObject(); for (String name : new String[] {"wires", "dusts/carbon", "rubber", "plastic", "circuit_boards", "solder"}) { List members = new ArrayList<>(); - for (var item : net.minecraft.core.registries.BuiltInRegistries.ITEM.getTagOrEmpty(ItemTags.create(new ResourceLocation("forge", name)))) + for (var item : net.minecraft.core.registries.BuiltInRegistries.ITEM.getTagOrEmpty(ItemTags.create(ResourceLocation.fromNamespaceAndPath("forge", name)))) members.add(net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(item.value()).toString()); Collections.sort(members); tags.add(name, JSON.toJsonTree(members)); @@ -265,13 +265,13 @@ static JsonArray inputJson(List inputs) { } static ItemStack stack(String id) { - Item item = ForgeRegistries.ITEMS.getValue(new ResourceLocation(id)); + Item item = ForgeRegistries.ITEMS.getValue(ResourceLocation.parse(id)); return item == null ? ItemStack.EMPTY : new ItemStack(item); } private static net.minecraft.world.level.storage.loot.LootTable lootTable(MinecraftServer server, String id) { return server.reloadableRegistries().getLootTable(net.minecraft.resources.ResourceKey.create( - net.minecraft.core.registries.Registries.LOOT_TABLE, new ResourceLocation(id))); + net.minecraft.core.registries.Registries.LOOT_TABLE, ResourceLocation.parse(id))); } private static LootParams chestParams(ServerLevel world) { @@ -282,8 +282,10 @@ private static LootParams chestParams(ServerLevel world) { @SuppressWarnings("unchecked") 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, world.registryAccess()) : ItemStack.EMPTY; + var input = inventory instanceof CraftingContainer grid ? grid.asCraftInput() + : new net.minecraft.world.item.crafting.SingleRecipeInput(inventory.getItem(0)); + Recipe typed = (Recipe) recipe; + return typed.matches(input, world) ? typed.assemble(input, world.registryAccess()) : ItemStack.EMPTY; } private static boolean advantage(ItemStack stack) { return !stack.isEmpty() && owned(net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(stack.getItem())); } 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 aa1f864f..cfff2d67 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 @@ -49,7 +49,7 @@ static void verify(MinecraftServer server, ServerLevel world, String label, Json buffer.readBytes(bytes); Files.write(Paths.get("recipes-" + label + ".bin"), bytes); } finally { buffer.release(); } - ResourceLocation starchId = new ResourceLocation("basepower:starch_from_potato"); + ResourceLocation starchId = ResourceLocation.parse("basepower:starch_from_potato"); Recipe starch = server.getRecipeManager().byKey(starchId).map(RecipeHolder::value).orElse(null); require(starch != null && starch.getType() == RecipeType.CRAFTING, "Missing native potato crafting recipe"); int cases = 0; @@ -59,10 +59,10 @@ static void verify(MinecraftServer server, ServerLevel world, String label, Json ItemStack input = stack(potato); input.setCount(count); grid.setItem(slot, input); - require(starch.matches(grid, world), "Potato recipe rejected slot/stack " + slot + "/" + count); - RecipeHolder selected = world.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, grid, world).orElse(null); + require(starch.matches(grid.asCraftInput(), world), "Potato recipe rejected slot/stack " + slot + "/" + count); + RecipeHolder selected = world.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, grid.asCraftInput(), world).orElse(null); require(selected != null && selected.id().equals(starchId), "Conflicting native potato crafting recipe"); - ItemStack result = starch.assemble(grid, world.registryAccess()); + ItemStack result = starch.assemble(grid.asCraftInput(), world.registryAccess()); 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); @@ -74,14 +74,14 @@ static void verify(MinecraftServer server, ServerLevel world, String label, Json for (String item : new String[] {null, "minecraft:baked_potato", "minecraft:carrot", "minecraft:dirt"}) { CraftingContainer grid = grid(3); if (item != null) grid.setItem(0, stack(item)); - require(!starch.matches(grid, world), "Invalid starch input accepted: " + item); + require(!starch.matches(grid.asCraftInput(), world), "Invalid starch input accepted: " + item); cases++; } for (String second : new String[] {"minecraft:potato", "minecraft:poisonous_potato", "minecraft:dirt"}) { 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"); + require(!starch.matches(grid.asCraftInput(), world), "Extra occupied ingredient slot accepted"); cases++; } report.addProperty("potatoCraftingCases", cases); 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 a2318bfd..6ed19674 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 @@ -112,7 +112,7 @@ private void run(MinecraftServer server, ServerLevel world, String operation, St JsonObject definition = element.getAsJsonObject(); String id = definition.get("id").getAsString(); AdvancementHolder advancement = advancement(server, id); - RecipeHolder recipe = server.getRecipeManager().byKey(new ResourceLocation(id)).orElse(null); + RecipeHolder recipe = server.getRecipeManager().byKey(ResourceLocation.parse(id)).orElse(null); JsonObject row = new JsonObject(); row.addProperty("id", id); row.addProperty("recipeActive", recipe != null); @@ -157,11 +157,11 @@ private void run(MinecraftServer server, ServerLevel world, String operation, St } else if (operation.equals("persist")) { for (JsonElement element : cases) { String id = element.getAsJsonObject().get("id").getAsString(); - reset(advancement(server, id), server.getRecipeManager().byKey(new ResourceLocation(id)).orElse(null)); + reset(advancement(server, id), server.getRecipeManager().byKey(ResourceLocation.parse(id)).orElse(null)); } JsonArray kit = new JsonArray(); for (JsonElement id : fixture.getAsJsonArray("materials")) { - ResourceLocation name = new ResourceLocation(id.getAsString()); + ResourceLocation name = ResourceLocation.parse(id.getAsString()); if (!ForgeRegistries.ITEMS.containsKey(name)) continue; discover(item(id.getAsString())); kit.add(id.getAsString()); @@ -213,7 +213,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); - RecipeHolder recipe = server.getRecipeManager().byKey(new ResourceLocation(id)).orElse(null); + RecipeHolder recipe = server.getRecipeManager().byKey(ResourceLocation.parse(id)).orElse(null); require(recipe == null || !available || player.getRecipeBook().contains(recipe), "Lost recipe reward: " + id); JsonObject row = new JsonObject(); row.addProperty("id", id); @@ -238,8 +238,8 @@ private void reset(AdvancementHolder advancement, RecipeHolder recipe) { } private static AdvancementHolder advancement(MinecraftServer server, String id) { - ResourceLocation name = new ResourceLocation(id); - return server.getAdvancements().get(new ResourceLocation(name.getNamespace(), "recipes/" + name.getPath())); + ResourceLocation name = ResourceLocation.parse(id); + return server.getAdvancements().get(ResourceLocation.fromNamespaceAndPath(name.getNamespace(), "recipes/" + name.getPath())); } private static boolean matchesInventory(InventoryChangeTrigger.TriggerInstance condition, @@ -260,13 +260,13 @@ private static List choices(JsonObject predicate) { if (predicate.has("item")) return Collections.singletonList(item(predicate.get("item").getAsString())); List items = new ArrayList<>(); for (var holder : net.minecraft.core.registries.BuiltInRegistries.ITEM.getTagOrEmpty( - ItemTags.create(new ResourceLocation(predicate.get("tag").getAsString())))) items.add(holder.value()); + ItemTags.create(ResourceLocation.parse(predicate.get("tag").getAsString())))) items.add(holder.value()); items.sort(Comparator.comparing(candidate -> net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(candidate).toString())); return items; } private static Item item(String id) { - ResourceLocation name = new ResourceLocation(id); + ResourceLocation name = ResourceLocation.parse(id); Item item = ForgeRegistries.ITEMS.getValue(name); require(item != null, "Missing test ingredient: " + id); return item; 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 cde5295c..8feb4161 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 @@ -218,7 +218,7 @@ private void construct(Witness witness) { 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 = net.minecraft.core.registries.BuiltInRegistries.BLOCK.get(new ResourceLocation("baseelectric:electric_drill")); + Block block = net.minecraft.core.registries.BuiltInRegistries.BLOCK.get(ResourceLocation.parse("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++) 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 37d5641b..475d58cc 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 @@ -127,7 +127,7 @@ private static void musket(List cases) { }; 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("basesteam:musket")); + Item weapon = ForgeRegistries.ITEMS.getValue(ResourceLocation.parse("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(InteractionHand.MAIN_HAND, shot.weapon); @@ -135,7 +135,7 @@ private static void musket(List cases) { 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("basesteam:blackpowder_cartridge")); + Item cartridge = ForgeRegistries.ITEMS.getValue(ResourceLocation.parse("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)); } @@ -200,8 +200,8 @@ private static float shotDamage() { } private static void enchant(MachineContext c, ItemStack weapon, String id) { - Enchantment enchantment = ForgeRegistries.ENCHANTMENTS.getValue(new ResourceLocation(id)); - c.require(enchantment != null, "Registered enchantment " + id); + var enchantment = c.world.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.ENCHANTMENT) + .getOrThrow(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.ENCHANTMENT, ResourceLocation.parse(id))); weapon.enchant(enchantment, 1); } 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 2135796a..2a22e901 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 @@ -60,7 +60,7 @@ private static int amount(MachineContext c, String getter) { // This is an actual face-capability insertion, not c.tank's direct reserve seeding. private static void fill(MachineContext c, String id, int amount) { - Fluid fluid = ForgeRegistries.FLUIDS.getValue(new ResourceLocation(id)); + Fluid fluid = ForgeRegistries.FLUIDS.getValue(ResourceLocation.parse(id)); c.require(fluid != null, "Registered input fluid " + id); IFluidHandler handler = c.tile(X, Y, Z).getCapability( net.minecraftforge.common.capabilities.ForgeCapabilities.FLUID_HANDLER, Direction.UP) @@ -78,7 +78,7 @@ private static void reserve(MachineContext c, String key, String fluid, int amou else if (key.equals("TankOut")) getter = "getOutputTank"; else throw new IllegalArgumentException("Unknown Steam reserve tank " + key); FluidTank tank = tank(c, getter); - Fluid type = ForgeRegistries.FLUIDS.getValue(new ResourceLocation(fluid)); + Fluid type = ForgeRegistries.FLUIDS.getValue(ResourceLocation.parse(fluid)); c.require(type != null && amount > 0 && amount <= tank.getCapacity(), "Valid finite setup reserve for " + key); // Replace the complete tank value: recursively merged NBT retains an old Empty marker. tank.setFluid(new FluidStack(type, amount)); 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 69c29d24..8ba33d2c 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 @@ -240,14 +240,15 @@ private static void inside(MachineContext c, Cow target) { } private static Item item(MachineContext c, String id) { - Item item = ForgeRegistries.ITEMS.getValue(new ResourceLocation(id)); + Item item = ForgeRegistries.ITEMS.getValue(ResourceLocation.parse(id)); c.require(item != null && item != net.minecraft.world.item.Items.AIR, "Registered item " + id); return item; } private static void enchant(MachineContext c, ItemStack stack, String id, int level) { - Enchantment enchantment = ForgeRegistries.ENCHANTMENTS.getValue(new ResourceLocation(id)); - c.require(enchantment != null && level >= 1 && level <= enchantment.getMaxLevel(), "Registered supported enchantment " + id); + var enchantment = c.world.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.ENCHANTMENT) + .getOrThrow(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.ENCHANTMENT, ResourceLocation.parse(id))); + c.require(level >= 1 && level <= enchantment.value().getMaxLevel(), "Registered supported enchantment " + id); stack.enchant(enchantment, level); } } 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 9b52a428..9e93b51f 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 @@ -81,7 +81,7 @@ static WorksController getInstance() { synchronized void loadDefinition() { if (definition != null) return; - definition = WorksDefinitionLoader.loadNative1206(); + definition = WorksDefinitionLoader.loadNative1211(); for (WorksDefinition.Station station : definition.stations) { stations.put(station.id.toUpperCase(Locale.ROOT), station); statuses.put(station.id, "NOT_BUILT"); @@ -633,11 +633,9 @@ private void applyActions(ServerLevel world, WorksDefinition.Station station, } } if (action.enchantment != null) { - Enchantment enchantment = ForgeRegistries.ENCHANTMENTS.getValue( - new ResourceLocation(action.enchantment)); - if (enchantment == null) { - throw new IllegalStateException("Unknown enchantment " + action.enchantment); - } + var enchantment = world.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.ENCHANTMENT) + .getOrThrow(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.ENCHANTMENT, + ResourceLocation.parse(action.enchantment))); stack.enchant(enchantment, action.level); } ((Container) tile).setItem(action.slot, stack); @@ -702,7 +700,7 @@ private void applyActions(ServerLevel world, WorksDefinition.Station station, world.addFreshEntity(entityItem); break; case "spawnEntity": - EntityType entityType = ForgeRegistries.ENTITY_TYPES.getValue(new ResourceLocation(action.entity)); + EntityType entityType = ForgeRegistries.ENTITY_TYPES.getValue(ResourceLocation.parse(action.entity)); Entity entity = entityType == null ? null : entityType.create(world); if (entity == null) throw new IllegalStateException("Unknown entity " + action.entity); entity.setPos(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); @@ -864,7 +862,7 @@ private static Block block(String name) { if (name == null) return Blocks.AIR; String resolved = "minecraft:flowing_water".equals(name) ? "minecraft:water" : "minecraft:stonebrick".equals(name) ? "minecraft:stone_bricks" : name; - Block value = ForgeRegistries.BLOCKS.getValue(new ResourceLocation(resolved)); + Block value = ForgeRegistries.BLOCKS.getValue(ResourceLocation.parse(resolved)); return value == null ? Blocks.AIR : value; } @@ -872,7 +870,7 @@ private static Item item(String name) { if (name == null) return null; String resolved = "minecraft:sapling".equals(name) ? "minecraft:oak_sapling" : "minecraft:planks".equals(name) ? "minecraft:oak_planks" : name; - ResourceLocation key = new ResourceLocation(resolved); + ResourceLocation key = ResourceLocation.parse(resolved); return ForgeRegistries.ITEMS.containsKey(key) ? ForgeRegistries.ITEMS.getValue(key) : null; } @@ -883,7 +881,7 @@ private static IllegalStateException commandError(String message) { private static BlockState legacyState(String name, Block block, int metadata) { if (name != null && (name.startsWith("basepower:") || name.startsWith("basesteam:") || name.startsWith("baseelectric:"))) { - return LegacyMigrationApi.decodeBlockState(new ResourceLocation(name), block, metadata); + return LegacyMigrationApi.decodeBlockState(ResourceLocation.parse(name), block, metadata); } if (block instanceof LiquidBlock) { return block.defaultBlockState().setValue(LiquidBlock.LEVEL, Integer.valueOf(metadata & 15)); @@ -899,17 +897,17 @@ 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("basepower", name); + ResourceLocation id = name.indexOf(':') >= 0 ? ResourceLocation.parse(name) + : ResourceLocation.fromNamespaceAndPath("basepower", name); Fluid value = net.minecraft.core.registries.BuiltInRegistries.FLUID.getOptional(id).orElse(null); if (value == null) { String migratedName = LegacyMigrationApi.fluidTarget(name); if (!name.equals(migratedName)) { - value = net.minecraft.core.registries.BuiltInRegistries.FLUID.getOptional(new ResourceLocation(migratedName)).orElse(null); + value = net.minecraft.core.registries.BuiltInRegistries.FLUID.getOptional(ResourceLocation.parse(migratedName)).orElse(null); } } if (value == null && name.indexOf(':') < 0) { - value = net.minecraft.core.registries.BuiltInRegistries.FLUID.getOptional(new ResourceLocation("minecraft", name)).orElse(null); + value = net.minecraft.core.registries.BuiltInRegistries.FLUID.getOptional(ResourceLocation.fromNamespaceAndPath("minecraft", name)).orElse(null); } return value; } @@ -917,6 +915,6 @@ private static Fluid fluid(String name) { private static Block wool(int metadata) { String[] colors = {"white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "gray", "light_gray", "cyan", "purple", "blue", "brown", "green", "red", "black"}; - return ForgeRegistries.BLOCKS.getValue(new ResourceLocation("minecraft", colors[metadata & 15] + "_wool")); + return ForgeRegistries.BLOCKS.getValue(ResourceLocation.fromNamespaceAndPath("minecraft", colors[metadata & 15] + "_wool")); } } 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 85a340cc..0d6f3ddf 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 @@ -46,6 +46,10 @@ static WorksDefinition loadNative1206() { return load("/assets/advantageworkstest/works/works-1.20.6.json", "1.20.6"); } + static WorksDefinition loadNative1211() { + return load("/assets/advantageworkstest/works/works-1.21.1.json", "1.21.1"); + } + static WorksDefinition load(String resource, String minecraft) { JsonObject target = readResource(resource); if (target.get("format").getAsInt() != 1 || !minecraft.equals(target.get("minecraft").getAsString()) @@ -256,7 +260,7 @@ private static void validateNbt(com.google.gson.JsonObject nbt, String label) { private static void requireResource(String value, String label) { requireId(value, label); - new ResourceLocation(value); + ResourceLocation.parse(value); } private static void validatePosition(WorksDefinition.Station station, int[] pos, String label) { diff --git a/test-rig/src/main/resources/META-INF/mods.toml b/test-rig/src/main/resources/META-INF/mods.toml index 239f6b86..0fc5e5f5 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="[50,51)" +loaderVersion="[52,53)" license="LGPL-3.0-only" [[mods]] @@ -12,20 +12,20 @@ description='''Server-side commissioning harness for the Advantage family. Not f [[dependencies.advantageworkstest]] modId="basepower" mandatory=true -versionRange="[2.4.13.120061,3.0.0)" +versionRange="[2.4.14.121011,3.0.0)" ordering="AFTER" side="SERVER" [[dependencies.advantageworkstest]] modId="forge" mandatory=true -versionRange="[50.2.0,51)" +versionRange="[52.1.0,53)" ordering="NONE" side="SERVER" [[dependencies.advantageworkstest]] modId="minecraft" mandatory=true -versionRange="[1.20.6]" +versionRange="[1.21.1]" ordering="NONE" side="SERVER" diff --git a/test-rig/src/main/resources/assets/advantageworkstest/native1211-helper-contract.json b/test-rig/src/main/resources/assets/advantageworkstest/native1211-helper-contract.json new file mode 100644 index 00000000..035bc65e --- /dev/null +++ b/test-rig/src/main/resources/assets/advantageworkstest/native1211-helper-contract.json @@ -0,0 +1,22 @@ +{ + "format": 1, + "policy": "native1211-leash-knot-nosave-v1", + "minecraftVersion": "1.21.1", + "dataVersion": 3955, + "forgeVersion": "52.1.0", + "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": "D7BC1CA192106808495C6A4928CA69B02B06509B47D0A44587EED8BF6A590B05", + "net.minecraft.world.entity.EntityType": "C0058623DD898DB6E3B9423A7F009ABF37D48DEF0FFBFCFBA62F330D31CE0E7A", + "net.minecraft.world.entity.decoration.LeashFenceKnotEntity": "8D5AB259B502457FEE5E55CA01F966E453088AB07D827BFF773F16AEC3D423E9" + } +} diff --git a/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.21.1.json b/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.21.1.json new file mode 100644 index 00000000..fc2332ee --- /dev/null +++ b/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.21.1.json @@ -0,0 +1,6 @@ +{ + "format": 1, + "minecraft": "1.21.1", + "title": "Advantage Works 1.21.1 Commissioning Ground", + "stationSource": "/assets/advantageworkstest/works/works-1.13.2.json" +} 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 de8f8f02..8507740a 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,14 +67,14 @@ private WorksRunner() { public static void main(String[] args) throws Exception { Arguments arguments = Arguments.parse(args); - Profile profile = readNative1206Profile(arguments.profile); + Profile profile = readNative1211Profile(arguments.profile); if (arguments.serverPort != null) profile.serverPort = arguments.serverPort; File testRig = findTestRig(arguments.profile); File powerRoot = testRig.getParentFile().getCanonicalFile(); // Pin validation precedes directory creation, downloads, and world access. requireInputPins(profile); profile.preparedSeed = preparedServerSeed(testRig, profile, arguments); - requireNative1206CompilePin(testRig, profile); + requireNativeCompilePin(testRig, profile); if (arguments.validateOnly) { List> inputs = stageMods(testRig, null, profile.mods); if ("packaged".equals(profile.mode)) { @@ -89,7 +89,7 @@ public static void main(String[] args) throws Exception { File runtime = new File(runtimeBase, profile.name + "-" + stamp).getCanonicalFile(); requireChild(new File(testRig, "build").getCanonicalFile(), runtime); if (profile.preparedSeed == null) { - throw new IllegalArgumentException("Forge 50 runtime requires an official prepared server and sealed inventory"); + throw new IllegalArgumentException("Forge 52 runtime requires an official prepared server and sealed inventory"); } if (profile.preparedSeed != null) profile.preparedSeed.requireDisjoint(runtime); if (!runtime.mkdirs()) throw new IllegalStateException("Cannot create runtime " + runtime); @@ -319,14 +319,17 @@ static void requireModularLauncher(File runtime, String launcher) throws Excepti PreparedServerSeed.requirePlain(root); Path args = root.resolve(launcher); PreparedServerSeed.requirePlain(args); - if (launcher.startsWith("libraries/net/minecraftforge/forge/1.20.6-50.2.0/")) { - String shim = "forge-1.20.6-50.2.0-shim.jar"; + boolean forge52 = launcher.startsWith("libraries/net/minecraftforge/forge/1.21.1-52.1.0/"); + if (forge52 || launcher.startsWith("libraries/net/minecraftforge/forge/1.20.6-50.2.0/")) { + String shim = forge52 ? "forge-1.21.1-52.1.0-shim.jar" : "forge-1.20.6-50.2.0-shim.jar"; Path jar = root.resolve(shim); PreparedServerSeed.requirePlain(jar); if (!Files.readString(args, StandardCharsets.UTF_8).strip().equals( "-Djava.net.preferIPv6Addresses=system -jar " + shim) - || !sha256(jar.toFile()).equals("7BECDDA6D36C8359EADA6018C2EDB21C5914C50028C191BE09A39FF9F516E10C")) { - throw new IllegalArgumentException("Unexpected official Forge 50 launcher or shim"); + || !sha256(jar.toFile()).equals(forge52 + ? "F7B706C79896728723DA0E4E8C5A6ECDB6A617AD4DF9D7EA1DDC6367187BC291" + : "7BECDDA6D36C8359EADA6018C2EDB21C5914C50028C191BE09A39FF9F516E10C")) { + throw new IllegalArgumentException("Unexpected official Forge shim launcher"); } return; } @@ -804,7 +807,9 @@ static PreparedServerSeed preparedServerSeed(File testRig, Profile profile, Argu } return new PreparedServerSeed(preparedPath(testRig, arguments.preparedServerSeed), preparedPath(testRig, arguments.preparedServerSeedInventory), arguments.preparedServerSeedSha256, - serverLauncherPath(profile), "1.20.6".equals(profile.minecraftVersion) + serverLauncherPath(profile), "1.21.1".equals(profile.minecraftVersion) + ? "forge-1.21.1-52.1.0-shim.jar" + : "1.20.6".equals(profile.minecraftVersion) ? "forge-1.20.6-50.2.0-shim.jar" : "1.20.1".equals(profile.minecraftVersion) ? "libraries/net/minecraft/server/1.20.1-20230612.114412/server-1.20.1-20230612.114412-extra.jar" @@ -853,11 +858,12 @@ static final class PreparedServerSeed { final Map files = new LinkedHashMap<>(); final Set directories = new LinkedHashSet<>(); final List processes = new ArrayList<>(); - final boolean forge50; + final boolean forge50, forge52; PreparedServerSeed(Path root, Path inventory, String expectedHash, String launcher, String minecraft) throws Exception { forge50 = "forge-1.20.6-50.2.0-shim.jar".equals(minecraft); + forge52 = "forge-1.21.1-52.1.0-shim.jar".equals(minecraft); this.root = root.toAbsolutePath().normalize(); this.inventory = inventory.toAbsolutePath().normalize(); if (expectedHash == null || !expectedHash.matches("[0-9A-F]{64}")) { @@ -925,9 +931,9 @@ static final class PreparedServerSeed { } } reader.endObject(); - if (forge50 && reader.hasNext() && (!"forge".equals(reader.nextName()) - || !"1.20.6-50.2.0".equals(reader.nextString()))) - throw new IllegalArgumentException("Incorrect Forge 50 inventory metadata"); + if ((forge50 || forge52) && reader.hasNext() && (!"forge".equals(reader.nextName()) + || !(forge52 ? "1.21.1-52.1.0" : "1.20.6-50.2.0").equals(reader.nextString()))) + throw new IllegalArgumentException("Incorrect Forge shim inventory metadata"); if (reader.hasNext()) throw new IllegalArgumentException("Unexpected inventory root field"); reader.endObject(); if (reader.peek() != JsonToken.END_DOCUMENT) throw new IllegalArgumentException("Trailing inventory data"); @@ -980,8 +986,8 @@ void verifyTree(Path location, boolean codeOnly) throws Exception { catch (Exception failure) { throw new IOException(failure); } return FileVisitResult.CONTINUE; } - if (codeOnly && forge50 && Set.of("README.txt", "run.bat", "run.sh", "user_jvm_args.txt", - "forge-1.20.6-50.2.0-installer.jar.log").contains(relative)) { + if (codeOnly && (forge50 || forge52) && Set.of("README.txt", "run.bat", "run.sh", "user_jvm_args.txt", + forge52 ? "forge-1.21.1-52.1.0-installer.jar.log" : "forge-1.20.6-50.2.0-installer.jar.log").contains(relative)) { requirePlain(path); return FileVisitResult.CONTINUE; } @@ -1134,12 +1140,13 @@ static String serverLauncherPath(Profile profile) { private static boolean modularForge(Profile profile) { return "1.17.1".equals(profile.minecraftVersion) || "1.18.2".equals(profile.minecraftVersion) - || "1.20.1".equals(profile.minecraftVersion) || "1.20.6".equals(profile.minecraftVersion); + || "1.20.1".equals(profile.minecraftVersion) || "1.20.6".equals(profile.minecraftVersion) + || "1.21.1".equals(profile.minecraftVersion); } private static File runtimeJavaHome(Profile profile) throws Exception { if (!modularForge(profile)) return java8Home(profile); - if ("1.20.6".equals(profile.minecraftVersion)) { + if ("1.20.6".equals(profile.minecraftVersion) || "1.21.1".equals(profile.minecraftVersion)) { String configured = profile.java21Home; if (configured == null || configured.isBlank()) configured = System.getenv("ADVANTAGE_WORKS_JAVA21_HOME"); File home = new File(configured == null || configured.isBlank() @@ -1328,16 +1335,24 @@ static Profile readNative1201Profile(File file) throws Exception { } static Profile readNative1206Profile(File file) throws Exception { - Profile profile = readProfile(file, "1.20.6", "1.20.6-50.2.0"); + return readNativeShimProfile(file, "1.20.6", "1.20.6-50.2.0", ".120061"); + } + + static Profile readNative1211Profile(File file) throws Exception { + return readNativeShimProfile(file, "1.21.1", "1.21.1-52.1.0", ".121011"); + } + + private static Profile readNativeShimProfile(File file, String minecraft, String forge, String suffix) throws Exception { + Profile profile = readProfile(file, minecraft, forge); if (!"EXPLORATORY_UNQUALIFIED".equals(profile.qualificationStatus) || !"packaged".equals(profile.mode)) { - throw new IllegalArgumentException("Forge 50 launches must use ordinary pinned packaged artifacts"); + throw new IllegalArgumentException("Native shim launches must use ordinary pinned packaged artifacts"); } Set ids = new LinkedHashSet<>(); for (ModInput input : profile.mods) { - if (input.id == null || !ids.add(input.id) || input.version == null || !input.version.endsWith(".120061") + if (input.id == null || !ids.add(input.id) || input.version == null || !input.version.endsWith(suffix) || input.file == null || !input.file.endsWith("-" + input.version + ".jar") || !input.required || !input.verifyIdentity || input.workspaceProduced) { - throw new IllegalArgumentException("Invalid ordinary Forge 50 mod input: " + input.id); + throw new IllegalArgumentException("Invalid ordinary native mod input: " + input.id); } } if (!ids.containsAll(Set.of("basepower", "advantageworkstest", "orespawn"))) { @@ -1346,7 +1361,7 @@ static Profile readNative1206Profile(File file) throws Exception { return withFlatSettings(profile); } - private static void requireNative1206CompilePin(File testRig, Profile profile) throws Exception { + private static void requireNativeCompilePin(File testRig, Profile profile) throws Exception { ModInput power = profile.mods.stream().filter(input -> "basepower".equals(input.id)).findFirst().orElseThrow(); ModInput harness = profile.mods.stream().filter(input -> "advantageworkstest".equals(input.id)).findFirst().orElseThrow(); try (var jar = new java.util.jar.JarFile(resolve(testRig, harness.path))) { diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117PlayerIoTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117PlayerIoTest.java index 661d0324..3468d514 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117PlayerIoTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117PlayerIoTest.java @@ -47,24 +47,25 @@ public class Native117PlayerIoTest { @BeforeClass public static void bootstrap() { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); - var enchantments = (net.minecraftforge.registries.ForgeRegistry) - net.minecraftforge.registries.ForgeRegistries.ENCHANTMENTS; - boolean locked = enchantments.isLocked(); - if (locked) enchantments.unfreeze(); - var vanillaView = (net.minecraft.core.MappedRegistry) BuiltInRegistries.ENCHANTMENT; - vanillaView.unfreeze(); - try { - // Forge's item component codec resolves these IDs in its native registry. - for (String name : List.of("high_explosive", "powderless", "rapid_reload", "recoil")) { - enchantments.register("basesteam:" + name, new Enchantment(Enchantment.definition( - net.minecraft.tags.ItemTags.MINING_ENCHANTABLE, 1, 1, Enchantment.constantCost(1), - Enchantment.constantCost(2), 1, net.minecraft.world.entity.EquipmentSlot.MAINHAND))); - } - } finally { - vanillaView.freeze(); - if (locked) enchantments.freeze(); + var enchantments = new net.minecraft.core.MappedRegistry( + net.minecraft.core.registries.Registries.ENCHANTMENT, com.mojang.serialization.Lifecycle.stable()); + var vanilla = net.minecraft.data.registries.VanillaRegistries.createLookup(); + vanilla.lookupOrThrow(net.minecraft.core.registries.Registries.ENCHANTMENT).listElements().forEach(holder -> + net.minecraft.core.Registry.register(enchantments, holder.key(), holder.value())); + // Only the item-codec fixture definitions are synthetic; live tests load Steam's data pack. + for (String name : List.of("high_explosive", "powderless", "rapid_reload", "recoil")) { + var id = net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("basesteam", name); + var enchantment = Enchantment.enchantment(Enchantment.definition( + net.minecraft.core.HolderSet.direct(net.minecraft.world.item.Items.BOW.builtInRegistryHolder()), + 1, 1, Enchantment.constantCost(1), Enchantment.constantCost(2), 1, + net.minecraft.world.entity.EquipmentSlotGroup.MAINHAND)).build(id); + net.minecraft.core.Registry.register(enchantments, id, enchantment); } - registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + enchantments.freeze(); + registries = new RegistryAccess.ImmutableRegistryAccess(java.util.stream.Stream.concat( + RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY).registries(), + java.util.stream.Stream.of(new RegistryAccess.RegistryEntry<>( + net.minecraft.core.registries.Registries.ENCHANTMENT, enchantments)))).freeze(); } @Test public void realForgePlayerListSaveWithoutConnectionLeavesLegacyBytesUnchanged() throws Exception { 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 index e1388db8..9d87bd92 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2Test.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2Test.java @@ -75,7 +75,7 @@ public class Native117StoppedEntitiesSnapshotV2Test { } @Test public void resourceIsExactIndependentNativeControl() throws Exception { - assertEquals("59B45FFB7D3CA1F726EFDE111D1716FB0583B092D4F7EAA1A2AE09FA71C7F73F", + assertEquals("83990C70FA3F6AD0CDC2803CA956B6CF1227EA5B5701D95175874B7D9EEA0AB8", Native117StoppedEntitiesSnapshotV2.sha(Native117StoppedEntitiesSnapshotV2.contract())); } } diff --git a/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java b/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java index 3eaae5db..5e31da8f 100644 --- a/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java +++ b/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java @@ -90,7 +90,7 @@ private static void addLoaderFixture(ClassLoader loader, Class nodeType, Obje list.getMethod("insert", list).invoke(instructions, entry); return; } - throw new NoSuchMethodException("Forge 50 ModLoader()"); + throw new NoSuchMethodException("Forge 52 ModLoader()"); } public static void beforeModLoader(Class owner) throws ReflectiveOperationException { diff --git a/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/Forge50UnitControl.java b/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/Forge52UnitControl.java similarity index 94% rename from test-rig/unit-support/zone/moddev/mc/advantageworks/unit/Forge50UnitControl.java rename to test-rig/unit-support/zone/moddev/mc/advantageworks/unit/Forge52UnitControl.java index 62225446..edc01c8b 100644 --- a/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/Forge50UnitControl.java +++ b/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/Forge52UnitControl.java @@ -11,7 +11,7 @@ import net.minecraftforge.registries.ForgeRegistries; /** Proves the test-only fixture reaches Forge's native item, registry, and capability paths. */ -public final class Forge50UnitControl { +public final class Forge52UnitControl { public static void main(String[] args) throws Exception { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); @@ -32,6 +32,6 @@ public static void main(String[] args) throws Exception { || (Object) ForgeCapabilities.FLUID_HANDLER == ForgeCapabilities.ITEM_HANDLER) { throw new AssertionError("Native Forge capability tokens were not transformed"); } - System.out.println("Forge 50 plain-JUnit bootstrap control PASS"); + System.out.println("Forge 52 plain-JUnit bootstrap control PASS"); } } diff --git a/tools/generate-recipe-unlocks.js b/tools/generate-recipe-unlocks.js index b5fbdc1b..40945374 100644 --- a/tools/generate-recipe-unlocks.js +++ b/tools/generate-recipe-unlocks.js @@ -6,8 +6,8 @@ const root = path.resolve(__dirname, '..'); const properties = fs.readFileSync(path.join(root, 'gradle.properties'), 'utf8'); const mod = /^mod_id=(.+)$/m.exec(properties)[1].trim(); const data = path.join(root, 'src/main/resources/data', mod); -const recipes = path.join(data, 'recipes'); -const output = path.join(data, 'advancements/recipes'); +const recipes = path.join(data, 'recipe'); +const output = path.join(data, 'advancement/recipes'); const check = process.argv.includes('--check'); const ignored = new Set(['item:minecraft:stick', 'item:minecraft:string', 'item:minecraft:feather', 'tag:forge:rods/wooden']); diff --git a/tools/verification/Verify-NativeEclipse.ps1 b/tools/verification/Verify-NativeEclipse.ps1 new file mode 100644 index 00000000..88a9b513 --- /dev/null +++ b/tools/verification/Verify-NativeEclipse.ps1 @@ -0,0 +1,75 @@ +param( + [string]$ProjectRoot = (Join-Path $PSScriptRoot '../..'), + [Parameter(Mandatory = $true)][string]$Evidence, + [Parameter(Mandatory = $true)][string]$Compiler, + [Parameter(Mandatory = $true)][string]$JavaHome +) +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +$ProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot).Path +$Evidence = [IO.Path]::GetFullPath($Evidence) +if (Test-Path -LiteralPath $Evidence) { throw 'Refusing to replace Eclipse evidence' } +if ((Get-Content -LiteralPath (Join-Path $JavaHome 'release') -Raw) -notmatch '(?m)^JAVA_VERSION="21(?:\.|\+|")') { + throw 'The Forge 52 Eclipse build requires Java 21' +} +$null = New-Item -ItemType Directory -Path $Evidence +$java = Join-Path $JavaHome 'bin/java.exe' +[xml]$classpath = Get-Content -LiteralPath (Join-Path $ProjectRoot '.classpath') -Raw +$report = [ordered]@{ + status='RUNNING'; project=$ProjectRoot + classpathSha256=(Get-FileHash -LiteralPath (Join-Path $ProjectRoot '.classpath')).Hash + compilerSha256=(Get-FileHash -LiteralPath $Compiler).Hash + javaSha256=(Get-FileHash -LiteralPath $java).Hash + compilations=[ordered]@{} +} +$outputs = @{} +try { + foreach ($scope in 'main','test') { + $sources = @() + foreach ($entry in $classpath.SelectNodes('/classpath/classpathentry[@kind="src"]')) { + $scopeAttribute = $entry.SelectSingleNode('attributes/attribute[@name="gradle_scope"]/@value') + if ($scopeAttribute -and $scopeAttribute.Value -eq $scope) { + $sources += @(Get-ChildItem -LiteralPath (Join-Path $ProjectRoot ([string]$entry.path)) -Recurse -File -Filter '*.java') + } + } + if (-not $sources.Count) { throw "No Eclipse $scope sources" } + $libraries = @() + foreach ($entry in $classpath.SelectNodes('/classpath/classpathentry[@kind="lib"]')) { + $scopes = $entry.SelectSingleNode('attributes/attribute[@name="gradle_used_by_scope"]/@value') + if ($scopes -and $scopes.Value.Split(',') -notcontains $scope) { continue } + $path = [string]$entry.path + if (-not [IO.Path]::IsPathRooted($path)) { $path = Join-Path $ProjectRoot $path } + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Missing Eclipse library: $path" } + $libraries += $path + } + if (-not $libraries.Count) { throw 'Generate the resolved Eclipse classpath first' } + $output = Join-Path $Evidence $scope + $null = New-Item -ItemType Directory -Path $output + $outputs[$scope] = $output + if ($scope -eq 'test') { $libraries += @($outputs.main, (Join-Path $ProjectRoot 'build/resources/main')) } + $arguments = @('-21','-proc:none','-encoding','UTF-8','-warn:none','-classpath', + ('"' + ($libraries -join ';') + '"'), '-d', ('"' + $output + '"')) + $arguments += @($sources | Sort-Object FullName | ForEach-Object { '"' + $_.FullName + '"' }) + $argFile = Join-Path $Evidence "$scope.args" + [IO.File]::WriteAllLines($argFile, $arguments, [Text.UTF8Encoding]::new($false)) + & $java -Xmx2G -jar $Compiler "@$argFile" *> (Join-Path $Evidence "$scope.log") + $code = $LASTEXITCODE + $report.compilations[$scope] = @{ exitCode=$code; sources=$sources.Count; argumentsSha256=(Get-FileHash -LiteralPath $argFile).Hash } + if ($code -ne 0) { throw "Eclipse $scope compilation failed; inspect retained diagnostics" } + $classes = @(Get-ChildItem -LiteralPath $output -Recurse -Filter '*.class' -File) + if (-not $classes.Count) { throw "No Eclipse $scope classes" } + foreach ($file in $classes) { + $bytes = [IO.File]::ReadAllBytes($file.FullName) + if ($bytes[6] * 256 + $bytes[7] -ne 65) { throw "Non-Java-21 class: $file" } + } + $report.compilations[$scope].classes = $classes.Count + } + $report.status = 'PASS' +} catch { + $report.status = 'FAIL' + $report['error'] = $_.Exception.Message + throw +} finally { + $report | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $Evidence 'ecj-results.json') -Encoding utf8 +} +Write-Output 'PASS: both Eclipse source scopes compile with the generated production and test classpaths'