From 5fdbb2c2b2d9bbc760aca3919e5b5b648f4545e2 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Tue, 10 Nov 2026 12:03:22 +0000 Subject: [PATCH 1/9] Port basePower to Minecraft 1.20.6 --- .github/workflows/ci.yml | 38 +- .github/workflows/codeql-analysis.yml | 24 +- .github/workflows/validate-gradle-build.yml | 4 +- CHANGELOG.txt | 7 + README.md | 40 +- build.gradle | 174 +++---- docs/1.20.6/PORT-STATUS.md | 18 + gradle.properties | 40 +- gradle/forge50-mavenizer.gradle | 23 + .../cyano/poweradvantage/api/GUIBlock.java | 32 +- .../poweradvantage/api/PoweredEntity.java | 26 +- .../api/fluid/FluidPoweredEntity.java | 9 +- .../api/modsupport/TileEntityConverter.java | 9 +- .../api/simple/BlockSimpleFluidConduit.java | 2 +- .../api/simple/BlockSimplePowerConduit.java | 2 +- .../api/simple/BlockSimplePowerMachine.java | 2 +- .../api/simple/SimpleMachineGUI.java | 12 +- .../simple/TileEntitySimpleFluidMachine.java | 29 +- .../simple/TileEntitySimplePowerMachine.java | 21 +- .../moddev/mc/basepower/PowerAdvantage.java | 6 +- .../basepower/gui/FilteredFluidTankGUI.java | 5 + .../moddev/mc/basepower/gui/FluidTankGUI.java | 5 + .../moddev/mc/basepower/gui/StillGUI.java | 6 +- .../zone/moddev/mc/basepower/init/Fluids.java | 13 +- .../zone/moddev/mc/basepower/init/Items.java | 6 +- .../moddev/mc/basepower/init/Villages.java | 5 +- .../conveyors/TileEntityConveyor.java | 21 +- .../conveyors/TileEntityFoodFilter.java | 2 +- .../creative/InfiniteEnergyTileEntity.java | 9 +- .../FluidDischargeTileEntity.java | 9 +- .../fluidmachines/FluidDrainTileEntity.java | 2 +- .../fluidmachines/StillTileEntity.java | 9 +- .../fluidmachines/StorageTankBlock.java | 17 +- .../migration/LegacyEntityDataFixes.java | 8 +- .../migration/LegacyMachineItemDataFixes.java | 16 +- .../migration/LegacyPlayerDataFixes.java | 21 +- .../migration/LegacyWorldDataHook.java | 22 +- .../migration/LegacyWorldMetadata.java | 8 +- .../migration/VanillaRegistryRemaps.java | 15 + .../basepower/recipe/MaterialPolicyPack.java | 9 +- .../mc/basepower/recipe/MaterialTags.java | 24 +- .../moddev/mc/basepower/recipe/PartsLoot.java | 15 +- .../mc/basepower/recipe/RecipeConditions.java | 75 ++- .../mc/basepower/recipe/RecipeDiscovery.java | 12 +- .../registry/MachineGUIRegistry.java | 7 +- .../basepower/util/FluidContainerHelper.java | 48 +- .../moddev/mc/basepower/util/ItemData.java | 54 +++ .../resources/META-INF/accesstransformer.cfg | 2 +- src/main/resources/META-INF/mods.toml | 4 +- .../coremods/poweradvantage_118_migration.js | 155 +++--- .../recipes/converter_rf_electricity.json | 4 +- .../recipes/converter_rf_quantum.json | 2 +- .../recipes/converter_rf_steam.json | 4 +- .../converter_tr_electricity_iron.json | 4 +- ...converter_tr_electricity_refined_iron.json | 4 +- .../advancements/recipes/fluid_discharge.json | 8 +- .../advancements/recipes/fluid_drain.json | 8 +- .../recipes/fluid_metal_tank_iron_normal.json | 4 +- .../recipes/fluid_metal_tank_steel.json | 4 +- .../fluid_pipe_copper_apocalyptic.json | 2 +- .../recipes/fluid_pipe_copper_normal.json | 2 +- .../recipes/fluid_pipe_iron_apocalyptic.json | 2 +- .../recipes/fluid_pipe_iron_normal.json | 2 +- .../recipes/fluid_pipe_iron_tech.json | 2 +- .../recipes/fluid_pipe_lead_apocalyptic.json | 2 +- .../recipes/fluid_storage_tank.json | 4 +- .../advancements/recipes/fluid_switch.json | 4 +- .../advancements/recipes/item_conveyor.json | 4 +- .../advancements/recipes/item_filter_ore.json | 2 +- .../recipes/item_filter_plant.json | 2 +- .../advancements/recipes/rotator_tool.json | 4 +- .../sprocket_bronze_nugget_tech_no_rod.json | 4 +- .../recipes/sprocket_rod_normal.json | 4 +- .../recipes/sprocket_rod_tech.json | 4 +- .../recipes/sprocket_stickwood_normal.json | 2 +- .../advancements/recipes/steel_frame.json | 2 +- .../basepower/advancements/recipes/still.json | 2 +- .../blocks/fluid_storage_tank.json | 2 +- .../recipes/bioplastic_from_starch.json | 5 +- .../recipes/converter_rf_electricity.json | 27 +- .../recipes/converter_rf_quantum.json | 14 +- .../basepower/recipes/converter_rf_steam.json | 27 +- .../converter_tr_electricity_iron.json | 35 +- ...converter_tr_electricity_refined_iron.json | 27 +- .../recipes/crushing_fluid_discharge.json | 25 +- .../recipes/crushing_fluid_drain.json | 25 +- .../recipes/crushing_fluid_storage_tank.json | 25 +- .../recipes/crushing_item_conveyor.json | 33 +- .../recipes/crushing_item_filter_block.json | 33 +- .../recipes/crushing_item_filter_food.json | 33 +- .../recipes/crushing_item_filter_fuel.json | 33 +- .../crushing_item_filter_inventory.json | 33 +- .../recipes/crushing_item_filter_ore.json | 33 +- .../crushing_item_filter_overflow.json | 33 +- .../recipes/crushing_item_filter_plant.json | 33 +- .../recipes/crushing_item_filter_smelt.json | 33 +- .../recipes/crushing_potato_starch.json | 12 +- .../basepower/recipes/fluid_discharge.json | 41 +- .../data/basepower/recipes/fluid_drain.json | 41 +- .../recipes/fluid_metal_tank_iron_normal.json | 35 +- .../recipes/fluid_metal_tank_steel.json | 25 +- .../fluid_pipe_copper_apocalyptic.json | 27 +- .../recipes/fluid_pipe_copper_normal.json | 27 +- .../recipes/fluid_pipe_iron_apocalyptic.json | 27 +- .../recipes/fluid_pipe_iron_normal.json | 27 +- .../recipes/fluid_pipe_iron_tech.json | 27 +- .../recipes/fluid_pipe_lead_apocalyptic.json | 27 +- .../basepower/recipes/fluid_storage_tank.json | 25 +- .../data/basepower/recipes/fluid_switch.json | 25 +- .../data/basepower/recipes/item_conveyor.json | 25 +- .../basepower/recipes/item_filter_block.json | 2 +- .../basepower/recipes/item_filter_food.json | 2 +- .../basepower/recipes/item_filter_fuel.json | 2 +- .../recipes/item_filter_inventory.json | 2 +- .../basepower/recipes/item_filter_ore.json | 12 +- .../recipes/item_filter_overflow.json | 2 +- .../basepower/recipes/item_filter_plant.json | 2 +- .../basepower/recipes/item_filter_smelt.json | 2 +- .../data/basepower/recipes/rotator_tool.json | 25 +- .../sprocket_bronze_nugget_tech_no_rod.json | 43 +- .../recipes/sprocket_rod_normal.json | 35 +- .../basepower/recipes/sprocket_rod_tech.json | 35 +- .../recipes/sprocket_stickwood_normal.json | 35 +- .../basepower/recipes/starch_from_potato.json | 13 +- .../data/basepower/recipes/steel_frame.json | 12 +- .../data/basepower/recipes/still.json | 12 +- src/main/resources/pack.mcmeta | 5 +- .../conveyors/ConveyorTransferTest.java | 7 +- .../migration/LegacyEntitiesTest.java | 14 +- .../LegacyEntityAttributeAliasesTest.java | 33 +- .../LegacyForgeAttributeRegistryTest.java | 42 +- .../migration/LegacyMachineItemsTest.java | 147 +++++- .../migration/LegacyPlayerDataFixesTest.java | 124 ++++- .../LegacyPlayerTransformerTest.java | 449 +++++++++++------- .../migration/LegacyRegistryArchiveTest.java | 10 +- .../migration/LegacyTransformerOrderTest.java | 241 +++++++--- .../migration/NativeWolfTestWorld.java | 46 ++ .../VanillaRegistryMappingEventTest.java | 11 + .../migration/VanillaRegistryRemapsTest.java | 9 + .../mc/basepower/recipe/PartsLootTest.java | 20 +- .../registry/MachineGuiTitleTest.java | 5 +- .../resources/BuildWorkflowContractTest.java | 5 +- .../CreativeTabInitializationTest.java | 10 +- .../NativeBlockEntityFactoryTest.java | 3 +- .../resources/NativePlatformContractTest.java | 18 +- .../basepower/resources/NativeTestTags.java | 2 +- .../RecipeRegistrationThreadTest.java | 58 ++- .../resources/RecipeUnlockContractTest.java | 18 +- .../basepower/resources/StarchRecipeTest.java | 6 +- .../client/ClientResourceProbe.java | 8 +- .../client/NativeGuiProjection.java | 10 +- .../advantageworks/client/OilRenderProbe.java | 5 +- .../client/RecipeBookGroupingProbe.java | 121 +++-- .../client/TankVisualProbe.java | 67 ++- .../resources/META-INF/mods.toml | 8 +- .../client-resources/resources/pack.mcmeta | 2 +- test-rig/client-resources/run.ps1 | 38 +- test-rig/client-resources/test-launcher.ps1 | 30 +- .../ForgeCapabilityBootstrap.java | 86 +++- .../unit/Forge50UnitControl.java | 37 ++ .../unit/ForgeUnitRegistries.java | 46 +- tools/generate-recipe-unlocks.js | 2 +- 162 files changed, 2563 insertions(+), 1530 deletions(-) create mode 100644 docs/1.20.6/PORT-STATUS.md create mode 100644 gradle/forge50-mavenizer.gradle create mode 100644 src/main/java/zone/moddev/mc/basepower/util/ItemData.java create mode 100644 src/test/java/zone/moddev/mc/basepower/migration/NativeWolfTestWorld.java create mode 100644 test-rig/unit-support/zone/moddev/mc/advantageworks/unit/Forge50UnitControl.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 547e767e..4c2b8eb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,16 @@ -name: basePower 1.19 CI +name: basePower 1.20.6 CI on: push: - branches: [master-1.20.1, 'feature/**'] + branches: [master-1.20.6, 'feature/**'] pull_request: - branches: [master-1.20.1] + branches: [master-1.20.6] permissions: contents: read concurrency: - group: basepower-1.19-${{ github.workflow }}-${{ github.ref }} + group: basepower-1.20.6-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/OreSpawn - ref: 9d9db6cd315c9fce542808aa2551c94bf9267e93 + ref: 4085315345ff043448b316d75e03f4b8da45402d path: .ci-dependencies/OreSpawn - name: Use canonical LF dependency inputs # Use the same source bytes on hosted and local dependency builds. @@ -34,23 +34,23 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/BaseMetals - ref: d9614e3d26a3b57ddff9703861f18a283c7e8358 + ref: 0c2cdd4d0e8c3bf2e5fa234d49e57960bbf80bde path: .ci-dependencies/BaseMetals - name: Install pinned Java 25 Mavenizer runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '25.0.3+9.0.LTS' - - name: Install Java 8 renamer runtime + - name: Install Java 8 launcher helper uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '8.0.502+7' - - name: Install Java 17 for Gradle and production + - name: Install Java 21 for Gradle and production uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '17.0.1+12' + java-version: '21.0.7+6.0.LTS' - name: Use pinned build toolchains # These are Gradle project properties, not JVM system properties. shell: bash @@ -68,11 +68,13 @@ jobs: orespawn="$GITHUB_WORKSPACE/.ci-dependencies/OreSpawn" basemetals="$GITHUB_WORKSPACE/.ci-dependencies/BaseMetals" mavenizer="$orespawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar" + mirror="$RUNNER_TEMP/orespawn-mirror" + bash "$basemetals/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 --no-daemon --stacktrace) - osjar="$orespawn/build/libs-dev/OreSpawn-4.1.0.120011-deobf.jar" - bmjar="$basemetals/build/libs-dev/BaseMetals-3.1.2.120011-deobf.jar" + (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" test -f "$osjar" && test -f "$bmjar" echo "orespawn=$osjar" >> "$GITHUB_OUTPUT" echo "basemetals=$bmjar" >> "$GITHUB_OUTPUT" @@ -83,25 +85,25 @@ jobs: writeReleaseChecksums verifyEclipseProductionClasspath -PbaseMetalsDeobfJar=${{ steps.dependencies.outputs.basemetals }} -PoreSpawnDeobfJar=${{ steps.dependencies.outputs.orespawn }} - -Pforge47MavenizerJar=${{ steps.dependencies.outputs.mavenizer }} + -Pforge50MavenizerJar=${{ steps.dependencies.outputs.mavenizer }} --no-daemon --stacktrace - name: Upload audited release candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: basePower-1.20.1-${{ github.sha }} + name: basePower-1.20.6-${{ github.sha }} if-no-files-found: error retention-days: 30 path: | - build/libs/basePower-2.4.12.120011.jar - build/libs/basePower-2.4.12.120011-sources.jar - build/libs/basePower-2.4.12.120011-javadoc.jar + 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/release/SHA256SUMS CHANGELOG.txt - name: Upload diagnostics on failure if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: basePower-1.20.1-diagnostics-${{ github.sha }} + name: basePower-1.20.6-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 fbcc42dc..1e5d2415 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.1, 'feature/**'] + branches: [master-1.20.6, 'feature/**'] pull_request: - branches: [master-1.20.1] + branches: [master-1.20.6] schedule: - cron: '47 7 * * 4' @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/OreSpawn - ref: 9d9db6cd315c9fce542808aa2551c94bf9267e93 + ref: 4085315345ff043448b316d75e03f4b8da45402d path: .ci-dependencies/OreSpawn - name: Use canonical LF dependency inputs # Use the same source bytes on hosted and local dependency builds. @@ -34,23 +34,23 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/BaseMetals - ref: d9614e3d26a3b57ddff9703861f18a283c7e8358 + ref: 0c2cdd4d0e8c3bf2e5fa234d49e57960bbf80bde path: .ci-dependencies/BaseMetals - name: Install pinned Java 25 Mavenizer runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '25.0.3+9.0.LTS' - - name: Install Java 8 renamer runtime + - name: Install Java 8 launcher helper uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '8.0.502+7' - - name: Install Java 17 for Gradle and production + - name: Install Java 21 for Gradle and production uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '17.0.1+12' + java-version: '21.0.7+6.0.LTS' - name: Use pinned build toolchains # These are Gradle project properties, not JVM system properties. shell: bash @@ -68,11 +68,13 @@ jobs: orespawn="$GITHUB_WORKSPACE/.ci-dependencies/OreSpawn" basemetals="$GITHUB_WORKSPACE/.ci-dependencies/BaseMetals" mavenizer="$orespawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar" + mirror="$RUNNER_TEMP/orespawn-mirror" + bash "$basemetals/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 --no-daemon --stacktrace) - echo "orespawn=$orespawn/build/libs-dev/OreSpawn-4.1.0.120011-deobf.jar" >> "$GITHUB_OUTPUT" - echo "basemetals=$basemetals/build/libs-dev/BaseMetals-3.1.2.120011-deobf.jar" >> "$GITHUB_OUTPUT" + (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 "mavenizer=$mavenizer" >> "$GITHUB_OUTPUT" - name: Initialize CodeQL uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 @@ -84,6 +86,6 @@ jobs: ./gradlew $MMD_GRADLE_TOOLCHAINS clean classes --rerun-tasks --no-build-cache --no-daemon --stacktrace \ -PbaseMetalsDeobfJar=${{ steps.dependencies.outputs.basemetals }} \ -PoreSpawnDeobfJar=${{ steps.dependencies.outputs.orespawn }} \ - -Pforge47MavenizerJar=${{ steps.dependencies.outputs.mavenizer }} + -Pforge50MavenizerJar=${{ 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 63abc33f..2c47d128 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.1 + - master-1.20.6 - 'feature/**' pull_request: branches: - - master-1.20.1 + - master-1.20.6 permissions: contents: read diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c179b794..d1c51242 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +basePower 2.4.13.120061 (Minecraft 1.20.6 local candidate) + +- Bring basePower to Forge 50 and Java 21 without changing the names of existing machines or items. +- Read old machine inventories into Minecraft's new item components. Portable fluid tanks still keep their contents when picked up. +- Move recipes, recipe unlocks and loot to the new Forge and Minecraft formats. +- Keep the shared upgrade hook so Base Metals and basePower can preserve old machine data before Minecraft converts the chunks. + basePower 2.4.12.120011 (Minecraft 1.20.1 local candidate) - Bring basePower to Forge 47 with the same blocks, items, and machine data. diff --git a/README.md b/README.md index 1f2fdf11..74eda1d7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # basePower -**Minecraft 1.20.1, version 2.4.12.120011.** +**Minecraft 1.20.6, version 2.4.13.120061 (local candidate).** -See the [1.20.1 port record](docs/1.20.1/PORT-STATUS.md) for the current checks. +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. basePower continues DrCyano's Power Advantage. It provides shared power, @@ -12,7 +12,7 @@ fluids and item transport for baseSteam and baseElectric. 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.1 before you upgrade. +other mods in your world support 1.20.6 before you upgrade. The accepted [1.14 results](docs/1.14/BROADER-QUALIFICATION.md) are historical evidence, not qualification of this port or a whole modpack. @@ -20,21 +20,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.1 +## Building Minecraft 1.20.6 -The build uses Forge 47.4.10, official `1.20.1` mappings, -MCP `20230612.114412`, ForgeGradle 7.0.34 and Gradle 9.6.1. -Use Java 17 for Gradle, compilation and Minecraft. Mavenizer also -needs Java 25. CI pins these JDKs and Java 8 for the legacy renamer. Pass the installed homes using -`-Porg.gradle.java.installations.paths=,,` and disable +The build uses Forge 50.2.0, official `1.20.6` mappings, +MCP `20240429.135109`, 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. -Build the pinned Base Metals `3.1.2.120011` and OreSpawn `4.1.0.120011` deobf jars first. +Use the pinned Base Metals `3.1.2.120061` and OreSpawn `4.1.0.120061` jars. The source pins and checksums are in `gradle.properties`. -Forge 47 also needs the Mavenizer compatibility jar in the pinned OreSpawn +Forge 50 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 `-Pforge47MavenizerJar=` when that checkout is in another location. +Set `-Pforge50MavenizerJar=` 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. @@ -48,8 +48,8 @@ gradlew.bat clean check build javadoc genEclipseRuns eclipse isolateEclipseProdu The default dependency jars are: ```text -D:\BaseMetals\BaseMetals 1201\BaseMetals\build\libs-dev\BaseMetals-3.1.2.120011-deobf.jar -D:\MinecraftMineralogy\OS1201\OreSpawn\build\libs-dev\OreSpawn-4.1.0.120011-deobf.jar +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 ``` Other layouts can pass `-PbaseMetalsDeobfJar=` and `-PoreSpawnDeobfJar=`. @@ -59,7 +59,8 @@ Dependency checksums and source commits are recorded in `gradle.properties`. Unknown artifacts fail the dependency audit. Archives use reproducible ordering, LF text and no empty directories, so fresh checkouts produce the same bytes. -Release jars are written to `build/libs`; `deobfJar` writes the sibling-development jar to `build/libs-dev`. +Release jars are written to `build/libs`. Forge 50 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: @@ -71,10 +72,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.13.120011` deobf jars, +To prepare a combined client, build the baseSteam and baseElectric `2.2.14.120061` jars, run `gradlew.bat prepareCombinedEclipseLaunch`, refresh Eclipse, then launch `runClientAllAdvantage.launch`. Inputs must match -`test-rig/profiles/native1201/dev-core.json`; alternative layouts can pass +`test-rig/profiles/native1206/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. @@ -83,7 +84,7 @@ preparing a launch does not copy, open or qualify a save. matching their packages. They also read every development runtime jar entry and check its size and CRC to catch damaged cached dependencies. -Keep Eclipse's access-restriction checks enabled and use Java 17 for its project +Keep Eclipse's access-restriction checks enabled and use Java 21 for its project JRE and Minecraft launches. Check visuals, menus and recipe discovery against the same jars used for the automated machine tests. @@ -109,6 +110,7 @@ data-pack tags. The shared pre-flattening migration hook remains necessary; its presence is not proof that an upgrade is safe. The [legacy migration guide](docs/1.13/MIGRATION.md) describes the earlier protocols. -The [Advantage Works 1.20.1 guide](test-rig/NATIVE1201.md) lists the automated checks. +The [Advantage Works 1.20.1 guide](test-rig/NATIVE1201.md) describes the prior +qualification and the checks carried into 1.20.6. 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 dcdcaf2a..42455554 100644 --- a/build.gradle +++ b/build.gradle @@ -11,11 +11,10 @@ plugins { id 'eclipse' id 'idea' id 'maven-publish' - id 'net.minecraftforge.renamer' version '1.1.5' id 'net.minecraftforge.gradle' version '7.0.34' } -apply from: 'gradle/forge47-mavenizer.gradle' +apply from: 'gradle/forge50-mavenizer.gradle' group = project.mod_group version = project.mod_version @@ -47,7 +46,7 @@ ext.release_tag = project.mod_version java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) vendor = JvmVendorSpec.ADOPTIUM } withSourcesJar() @@ -56,11 +55,11 @@ java { tasks.withType(JavaCompile).configureEach { javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) vendor = JvmVendorSpec.ADOPTIUM } - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 options.encoding = 'UTF-8' options.compilerArgs.addAll(['-Xmaxerrs', '1000']) } @@ -134,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.1 launches.' + description = 'Merges classes and processed resources for Forge 1.20.6 launches.' dependsOn tasks.named('classes') from sourceSets.main.output into forgeRunModClassesDirectory @@ -168,12 +167,14 @@ repositories { // The default paths mirror the local MMD checkout layout without tying the // build to a drive letter. CI and other layouts can override either path. -def baseMetalsDeobfJar = file(providers.gradleProperty('baseMetalsDeobfJar') - .getOrElse('../../../BaseMetals/BaseMetals 1201/BaseMetals/build/libs-dev/' - + "BaseMetals-${project.basemetals_version}-deobf.jar")) -def oreSpawnDeobfJar = file(providers.gradleProperty('oreSpawnDeobfJar') - .getOrElse('../../../MinecraftMineralogy/OS1201/OreSpawn/build/libs-dev/' - + "OreSpawn-${project.orespawn_version}-deobf.jar")) +def baseMetalsDeobfJar = file(providers.gradleProperty('baseMetalsJar') + .orElse(providers.gradleProperty('baseMetalsDeobfJar')) + .getOrElse('../../../BaseMetals/.verification-basemetals1206/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/' + + "OreSpawn-${project.orespawn_version}.jar")) dependencies { implementation minecraft.dependency( @@ -233,41 +234,9 @@ def prepareEclipseResources = tasks.register('prepareEclipseResources') { } tasks.named('jar', Jar) { - archiveClassifier = 'deobf' - destinationDirectory = layout.buildDirectory.dir('libs-dev') - exclude 'fernflower_abstract_parameter_names.txt' - manifest { - attributes([ - 'Maven-Artifact' : "${project.group}:${base.archivesName.get()}:${project.version}", - 'Specification-Title' : project.mod_id, - 'Specification-Vendor' : project.mod_authors, - 'Specification-Version' : '1', - 'Implementation-Title' : base.archivesName.get(), - 'Implementation-Version': project.version, - 'Implementation-Vendor' : project.mod_authors, - 'Built-On-Java' : project.java_version, - 'Built-On' : "${project.minecraft_version}-${project.forge_version}" - ]) - } -} - -def renamedJar = renamer.classes(tasks.named('jar', Jar)) { - map.from minecraft.dependency.toSrgFile - archiveClassifier = 'renamed' - output.set(layout.buildDirectory.file( - "intermediates/renamed/${base.archivesName.get()}-${project.version}.jar")) -} - -// Renamer adds an internal Fernflower parameter map. Repack its output so the -// public jar is deterministic and contains only the mod plus its own manifest. -def releaseJar = tasks.register('releaseJar', Jar) { - dependsOn renamedJar archiveClassifier = null destinationDirectory = layout.buildDirectory.dir('libs') - from({ zipTree(renamedJar.get().output.get().asFile) }) { - exclude 'META-INF/MANIFEST.MF' - exclude 'fernflower_abstract_parameter_names.txt' - } + exclude 'fernflower_abstract_parameter_names.txt' manifest { attributes([ 'Maven-Artifact' : "${project.group}:${base.archivesName.get()}:${project.version}", @@ -283,9 +252,12 @@ def releaseJar = tasks.register('releaseJar', Jar) { } } +// Forge 50 uses the official names at runtime, so the same jar serves both uses. +def releaseJar = tasks.named('jar', Jar) + tasks.register('deobfJar') { group = 'build' - description = 'Builds the deobfuscated development jar under build/libs-dev.' + description = 'Builds the Forge-native jar under build/libs for development.' dependsOn tasks.named('jar') } @@ -316,11 +288,6 @@ tasks.named('javadocJar', Jar) { } } -['apiElements', 'runtimeElements'].each { configurationName -> - configurations.named(configurationName) { artifacts.clear() } - artifacts { add(configurationName, releaseJar) } -} - tasks.named('assemble') { dependsOn releaseJar dependsOn tasks.named('sourcesJar') @@ -338,23 +305,23 @@ 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 != 15 || pack['forge:resource_pack_format'] != 15 || pack['forge:data_pack_format'] != 15) { - throw new GradleException('Minecraft 1.20.1 requires resource and data pack format 15') + 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 (project.mod_version != '2.4.12.120011') { + if (project.mod_version != '2.4.13.120061') { throw new GradleException("Unexpected PowerAdvantage release version: ${project.mod_version}") } - if (project.minecraft_version != '1.20.1' - || project.forge_version != '47.4.10' + if (project.minecraft_version != '1.20.6' + || project.forge_version != '50.2.0' || project.mapping_channel != 'official' - || project.mapping_version != '1.20.1' - || project.mcp_version != '20230612.114412') { + || project.mapping_version != '1.20.6' + || project.mcp_version != '20240429.135109') { throw new GradleException('Unexpected Minecraft, Forge, or mappings target') } if (project.loader_name != 'forge' || project.loader_code != '1' - || project.java_version != '17' - || project.java_toolchain_version != '17.0.1+12' - || project.gradle_java_version != '17') { + || project.java_version != '21' + || project.java_toolchain_version != '21.0.7+6' + || project.gradle_java_version != '21') { throw new GradleException('Unexpected release dispatcher target metadata') } if (project.mod_id != 'basepower' @@ -365,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.1') { + || project.cf_versions != '1.20.6') { throw new GradleException('Unexpected CurseForge dependency or game-version metadata') } - if (project.basemetals_version != '3.1.2.120011' - || project.orespawn_version != '4.1.0.120011' - || project.basemetals_source_commit != 'd9614e3d26a3b57ddff9703861f18a283c7e8358' - || project.orespawn_source_commit != '9d9db6cd315c9fce542808aa2551c94bf9267e93') { + if (project.basemetals_version != '3.1.2.120061' + || project.orespawn_version != '4.1.0.120061' + || !(project.basemetals_source_commit ==~ /[a-f0-9]{40}/) + || project.orespawn_source_commit != '4085315345ff043448b316d75e03f4b8da45402d') { 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.12.120011"') + || !mainSource.contains('VERSION = "2.4.13.120061"') || !mainSource.contains('@Mod(PowerAdvantage.MODID)')) { throw new GradleException('PowerAdvantage source identity changed') } @@ -588,7 +555,7 @@ tasks.register('verifyReleaseArtifacts') { List accessRules = zip.getInputStream(zip.getEntry('META-INF/accesstransformer.cfg')) .getText('UTF-8').readLines().collect { it.trim() } .findAll { it && !it.startsWith('#') } - if (accessRules != ['public-f net.minecraft.util.datafix.fixes.BlockStateData f_14934_']) { + if (accessRules != ['public-f net.minecraft.util.datafix.fixes.BlockStateData MAP']) { throw new GradleException('Packaged pre-flattening table access rule is incorrect') } String modsToml = zip.getInputStream(zip.getEntry('META-INF/mods.toml')) @@ -614,8 +581,8 @@ tasks.register('verifyReleaseArtifacts') { } } int major = ((header[6] & 0xff) << 8) | (header[7] & 0xff) - if (major != 61) { - throw new GradleException("${entry.name} uses Java class major ${major}, expected 61") + if (major != 65) { + throw new GradleException("${entry.name} uses Java class major ${major}, expected 65") } } } @@ -870,17 +837,6 @@ tasks.register('isolateEclipseProductionRuns') { String eclipseModClasses = "${project.mod_id}%%${mainOutput}" + "${File.pathSeparator}${project.mod_id}%%${mainOutput}" modClasses[0].@value = eclipseModClasses - def vmArguments = launchXml.stringAttribute.find { it.@key == 'org.eclipse.jdt.launching.VM_ARGUMENTS' } - if (vmArguments != null) { - // ForgeGradle leaves these path arguments bare even when a directory contains spaces. - vmArguments.@value = (vmArguments.@value as String) - .replaceAll(/(? - "\"${argument}\"" - } - .replaceAll(/(? - "-p \"${argument}\"" - } - } contents = groovy.xml.XmlUtil.serialize(launchXml) } launch.setText(contents.replace('\r\n', '\n'), 'UTF-8') @@ -1038,13 +994,27 @@ tasks.register('verifyEclipseProductionClasspath') { ['runClient.launch', 'runServer.launch'].each { name -> def launch = new groovy.xml.XmlSlurper().parse(file(name)) String contents = file(name).getText('UTF-8').replace('\\', '/') - String vmArguments = launch.stringAttribute.find { - it.@key.text() == 'org.eclipse.jdt.launching.VM_ARGUMENTS' + String main = launch.stringAttribute.find { + it.@key.text() == 'org.eclipse.jdt.launching.MAIN_TYPE' }.@value.text() - def legacyFile = vmArguments =~ /(?:^|\s)"-DlegacyClassPath\.file=([^"]+)"(?=\s|$)/ - if (!legacyFile.find() || !file(legacyFile.group(1)).isFile() - || !(vmArguments =~ /(?:^|\s)-p\s+"[^"]+"/).find()) { - throw new GradleException("${name} must quote the Forge classpath and module paths") + String program = launch.stringAttribute.find { + it.@key.text() == 'org.eclipse.jdt.launching.PROGRAM_ARGUMENTS' + }.@value.text() + String target = name == 'runClient.launch' ? 'forge_userdev_client' : 'forge_userdev_server' + 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") + } + ['--cache', '--metadata', '--to-srg', '--to-obf'].each { option -> + def argument = program =~ /(?:^|\s)${option}\s+(?:"([^"]+)"|([^"\s]+))(?=\s+--|$)/ + if (!argument.find()) { + throw new GradleException("${name} must keep ${option} as one Slime Launcher argument") + } + File path = file(argument.group(1) ?: argument.group(2)) + if (option != '--cache' && !path.isFile()) { + throw new GradleException("${name} ${option} points to a missing file: ${path}") + } } if (launch.booleanAttribute.find { it.@key.text() == 'org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE' @@ -1187,7 +1157,7 @@ tasks.register('verifyRecipeTagGuards') { collectTags = { Object node, Set tags -> if (node instanceof Map) { node.each { key, value -> - if (key == 'conditions' || key == 'result') return + if (key == 'forge:condition' || key == 'result') return if (key == 'tag' && value instanceof String && value.startsWith('forge:')) { tags.add(value) } else { @@ -1198,14 +1168,26 @@ tasks.register('verifyRecipeTagGuards') { node.each { collectTags(it, tags) } } } + Closure requiredConditions + requiredConditions = { Object node, String path -> + if (!(node instanceof Map) || !(node.type instanceof String)) { + throw new GradleException("${path} contains an invalid Forge recipe condition") + } + if (node.type != 'forge:and') return [node] + if (!(node.values instanceof Collection) || node.values.isEmpty()) { + throw new GradleException("${path} contains an empty Forge conjunction") + } + node.values.collectMany { requiredConditions(it, path) } + } fileTree(recipeRoot) { include '**/*.json' }.files.sort().each { File recipeFile -> Map recipe = new JsonSlurper().parse(recipeFile) as Map Set tags = [] as Set collectTags(recipe, tags) - Collection conditions = (recipe.conditions ?: []) as Collection - if (conditions.any { !(it instanceof Map) }) { - throw new GradleException("${recipeFile.path} contains a non-object recipe condition") + if (recipe.containsKey('conditions')) { + throw new GradleException("${recipeFile.path} uses legacy recipe conditions") } + List conditions = recipe.containsKey('forge:condition') + ? requiredConditions(recipe['forge:condition'], recipeFile.path) : [] Set guards = conditions .findAll { it instanceof Map && it.type == 'basepower:tag_not_empty' } .collect { it.tag as String } as Set @@ -1250,12 +1232,12 @@ tasks.register('clientResourceProbeJar', Jar) { } // Optional coordinated play-test launch. The ordinary launch stays Power-only; -// these deobf jars are runtime inputs, never release contents or compile dependencies. +// The expansion jars are runtime inputs, never release contents or compile dependencies. def advantageExtensionJars = [ basesteam: file(providers.gradleProperty('steamAdvantageDeobfJar').getOrElse( - '../SteamAdvantage/SteamAdvantage/build/libs-dev/baseSteam-2.2.13.120011-deobf.jar')), + '../SteamAdvantage/SteamAdvantage/build/libs/baseSteam-2.2.14.120061.jar')), baseelectric: file(providers.gradleProperty('electricAdvantageDeobfJar').getOrElse( - '../ElectricAdvantage/ElectricAdvantage/build/libs-dev/baseElectric-2.2.13.120011-deobf.jar')) + '../ElectricAdvantage/ElectricAdvantage/build/libs/baseElectric-2.2.14.120061.jar')) ] tasks.register('prepareCombinedEclipseLaunch') { @@ -1265,7 +1247,7 @@ tasks.register('prepareCombinedEclipseLaunch') { dependsOn tasks.named('deobfJar') doLast { File inputProfile = file(providers.gradleProperty('advantageDevelopmentProfile').getOrElse( - 'test-rig/profiles/native1201/dev-core.json')) + 'test-rig/profiles/native1206/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.20.6/PORT-STATUS.md b/docs/1.20.6/PORT-STATUS.md new file mode 100644 index 00000000..c41c9d56 --- /dev/null +++ b/docs/1.20.6/PORT-STATUS.md @@ -0,0 +1,18 @@ +# Minecraft 1.20.6 Port + +This branch is a local port of the accepted 1.20.1 sources. It is not a release. +Results from older versions do not qualify this candidate. + +The 1.20.6 build uses Forge 50.2.0 and Java 21. The old item data is read into +native item components; block, item and block-entity IDs stay the same. +Base Metals' shared pre-flattening hook is on a separate local branch and must +be present when testing upgrades with Base Metals installed. + +Before handing back for manual testing, verify the three exact jars together: + +- Startup on the packaged dedicated server and the combined client. +- Recipe modes, survival progression, harvest and loot, including both tank drops. +- Connected machines, chunk reload, full restarts and the timed endurance runs. +- Original Advantage and released base* worlds, using copies, with inventory, + machine state, item components and nearby entity state checked after migration. +- The Eclipse production classpath and release artifact contents. diff --git a/gradle.properties b/gradle.properties index fb9548bd..b16e28bb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,42 +5,42 @@ org.gradle.caching=true org.gradle.parallel=false net.minecraftforge.gradle.merge-source-sets=false -minecraft_version=1.20.1 -minecraft_version_range=[1.20.1] -forge_version=47.4.10 -forge_installer_sha256=1912760B4CB6B803D8A826DE603C9076B1DA71EC2765E9A1F8C1CA78F65278E3 -forge_version_range=[47.4.10,48) -loader_version_range=[47,) +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,) mapping_channel=official -mapping_version=1.20.1 -mcp_version=20230612.114412 +mapping_version=1.20.6 +mcp_version=20240429.135109 loader_name=forge loader_code=1 -java_version=17 -java_toolchain_version=17.0.1+12 -java_setup_version=17.0.1+12 -gradle_java_version=17 +java_version=21 +java_toolchain_version=21.0.7+6 +java_setup_version=21.0.7+6 +gradle_java_version=21 curseforge_project_id=1780140 mod_id=basepower mod_name=basePower mod_license=LGPL-3.0-only -mod_version=2.4.12.120011 +mod_version=2.4.13.120061 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.1 +cf_versions=1.20.6 -basemetals_version=3.1.2.120011 -basemetals_source_commit=d9614e3d26a3b57ddff9703861f18a283c7e8358 +basemetals_version=3.1.2.120061 +basemetals_source_commit=0c2cdd4d0e8c3bf2e5fa234d49e57960bbf80bde basemetals_curse_project_id=240967 -basemetals_deobf_sha256=A67EB0CB74971E53D528C4D4F1275AE007106D7F8F71DFE4A16FB50411BDCC28 +basemetals_deobf_sha256=FC6DE01771ED9CD799D946A0ED4B6782B289C47AA8CFCA7753F9E102DD0F0417 -orespawn_version=4.1.0.120011 -orespawn_source_commit=9d9db6cd315c9fce542808aa2551c94bf9267e93 +orespawn_version=4.1.0.120061 +orespawn_source_commit=4085315345ff043448b316d75e03f4b8da45402d orespawn_curse_project_id=245586 -orespawn_deobf_sha256=A0B096894DC48308D59455F7EC67AB0BBF7EB02354D903024A9E9E876EB07CBB +orespawn_deobf_sha256=FAA7107B9FB84E2C62ED847CAFD880AA3C4238A37799BAD955EA9602A944AA26 diff --git a/gradle/forge50-mavenizer.gradle b/gradle/forge50-mavenizer.gradle new file mode 100644 index 00000000..8e9c6dd7 --- /dev/null +++ b/gradle/forge50-mavenizer.gradle @@ -0,0 +1,23 @@ +import java.security.MessageDigest +import org.gradle.jvm.toolchain.JvmVendorSpec + +// Use the same source preparation tool as OreSpawn on Forge 50. +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' +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}") +} + +fgtools.configure('mavenizer') { + classpath.from(fixture) + mainClass.set('net.minecraftforge.mcmaven.cli.Main') + javaLauncher.set(javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(25) + vendor = JvmVendorSpec.ADOPTIUM + }) +} diff --git a/src/main/java/cyano/poweradvantage/api/GUIBlock.java b/src/main/java/cyano/poweradvantage/api/GUIBlock.java index f066f9f0..31983431 100644 --- a/src/main/java/cyano/poweradvantage/api/GUIBlock.java +++ b/src/main/java/cyano/poweradvantage/api/GUIBlock.java @@ -3,6 +3,7 @@ import net.minecraft.world.level.material.MapColor; import net.minecraft.world.level.material.PushReaction; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.SoundType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.entity.player.Player; @@ -16,6 +17,10 @@ import net.minecraft.core.Direction; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.BlockHitResult; import net.minecraft.core.BlockPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.BlockGetter; @@ -36,7 +41,7 @@ * @author DrCyano * */ -public abstract class GUIBlock extends net.minecraft.world.level.block.BaseEntityBlock{ +public abstract class GUIBlock extends Block implements EntityBlock { /** Defaults for machine blocks formerly made from the piston material. */ public static Block.Properties machineProperties(float hardness) { @@ -48,6 +53,18 @@ protected GUIBlock(Block.Properties properties) { super(properties); } + @Override + protected boolean triggerEvent(BlockState state, Level level, BlockPos pos, int id, int data) { + super.triggerEvent(state, level, pos, id, data); + BlockEntity tile = level.getBlockEntity(pos); + return tile != null && tile.triggerEvent(id, data); + } + + @Override + protected MenuProvider getMenuProvider(BlockState state, Level level, BlockPos pos) { + return level.getBlockEntity(pos) instanceof MenuProvider provider ? provider : null; + } + @Override public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { @@ -136,7 +153,6 @@ public RenderShape getRenderShape(BlockState bs) { * otherwise */ @SuppressWarnings("deprecation") - @Override public InteractionResult use(BlockState bs, Level w, BlockPos coord, Player player, InteractionHand hand, net.minecraft.world.phys.BlockHitResult hit) { Direction facing = hit.getDirection(); @@ -164,6 +180,18 @@ public InteractionResult use(BlockState bs, Level w, BlockPos coord, return InteractionResult.SUCCESS; } + @Override + protected ItemInteractionResult useItemOn(ItemStack item, BlockState state, Level level, + BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + return switch (use(state, level, pos, player, hand, hit)) { + case SUCCESS, SUCCESS_NO_ITEM_USED -> ItemInteractionResult.SUCCESS; + case CONSUME -> ItemInteractionResult.CONSUME; + case CONSUME_PARTIAL -> ItemInteractionResult.CONSUME_PARTIAL; + case FAIL -> ItemInteractionResult.FAIL; + case PASS -> ItemInteractionResult.SKIP_DEFAULT_BLOCK_INTERACTION; + }; + } + @Override diff --git a/src/main/java/cyano/poweradvantage/api/PoweredEntity.java b/src/main/java/cyano/poweradvantage/api/PoweredEntity.java index 1bb5a22d..fd520078 100644 --- a/src/main/java/cyano/poweradvantage/api/PoweredEntity.java +++ b/src/main/java/cyano/poweradvantage/api/PoweredEntity.java @@ -1,5 +1,6 @@ package cyano.poweradvantage.api; +import net.minecraft.core.HolderLookup; import zone.moddev.mc.basepower.conduitnetwork.ConduitRegistry; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.server.level.ServerPlayer; @@ -279,8 +280,8 @@ private final int getTickOffset() { * packet. */ @Override - public void load(final CompoundTag tagRoot) { - super.load(tagRoot); + protected void loadAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.loadAdditional(tagRoot, registries); this.conduitRefreshDelay = 2; if (tagRoot.contains("Energy", 11)) { int[] data = tagRoot.getIntArray("Energy"); @@ -296,8 +297,8 @@ public void load(final CompoundTag tagRoot) { * the network. */ @Override - protected void saveAdditional(final CompoundTag tagRoot) { - super.saveAdditional(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.saveAdditional(tagRoot, registries); int[] data = new int[this.getTypes().length]; for (int i = 0; i < data.length; i++) { data[i] = Float.floatToIntBits(this.getEnergy(this.getTypes()[i])); @@ -305,15 +306,24 @@ protected void saveAdditional(final CompoundTag tagRoot) { tagRoot.putIntArray("Energy", data); } - /** @deprecated Minecraft now serializes through {@link #saveWithFullMetadata()}. */ + private HolderLookup.Provider serializationRegistries() { + return getLevel() == null + ? net.minecraft.core.RegistryAccess.fromRegistryOfRegistries(net.minecraft.core.registries.BuiltInRegistries.REGISTRY) + : getLevel().registryAccess(); + } + + /** @deprecated Prefer the registry-aware Minecraft save methods. */ @Deprecated public CompoundTag save(CompoundTag tag) { - return tag.merge(saveWithFullMetadata()); + return tag.merge(saveWithFullMetadata(serializationRegistries())); } - /** @deprecated Minecraft now calls {@link #load(CompoundTag)}. */ + /** @deprecated Prefer {@link #loadWithComponents}. */ + @Deprecated public void load(CompoundTag tag) { loadWithComponents(tag, serializationRegistries()); } + + /** @deprecated Prefer {@link #loadWithComponents}. */ @Deprecated public void read(CompoundTag tag) { load(tag); } - /** @deprecated Minecraft now calls {@link #save(CompoundTag)}. */ + /** @deprecated Prefer the registry-aware Minecraft save methods. */ @Deprecated public CompoundTag write(CompoundTag tag) { return save(tag); } diff --git a/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java b/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java index 8e5c81a5..6e56b617 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java @@ -1,5 +1,6 @@ package cyano.poweradvantage.api.fluid; +import net.minecraft.core.HolderLookup; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PowerRequest; import cyano.poweradvantage.api.PoweredEntity; @@ -81,8 +82,8 @@ protected List getRequestsForFluid(ConduitType powerType) { * @param tagRoot An NBT tag */ @Override - protected void saveAdditional(final CompoundTag tagRoot) { - super.saveAdditional(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.saveAdditional(tagRoot, registries); CompoundTag tankTag = new CompoundTag(); this.getTank().writeToNBT(tankTag); tagRoot.put("Tank", tankTag); @@ -94,8 +95,8 @@ protected void saveAdditional(final CompoundTag tagRoot) { * @param tagRoot An NBT tag */ @Override - public void load(final CompoundTag tagRoot) { - super.load(tagRoot); + protected void loadAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.loadAdditional(tagRoot, registries); if (tagRoot.contains("Tank", 10)) { CompoundTag tankTag = tagRoot .getCompound("Tank"); getTank().readFromNBT(tankTag); diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java index 076da655..5c5b752d 100644 --- a/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java +++ b/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java @@ -1,5 +1,6 @@ package cyano.poweradvantage.api.modsupport; +import net.minecraft.core.HolderLookup; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PowerRequest; import cyano.poweradvantage.api.simple.TileEntitySimplePowerMachine; @@ -174,16 +175,16 @@ public byte getMinimumSinkPriority() { } @Override - public void load(final CompoundTag tagRoot) { - super.load(tagRoot); + protected void loadAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.loadAdditional(tagRoot, registries); if (tagRoot.contains("Other", 99)) { this.setOtherEnergy(tagRoot.getDouble("Other")); } } @Override - protected void saveAdditional(final CompoundTag tagRoot) { - super.saveAdditional(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.saveAdditional(tagRoot, registries); tagRoot.putDouble("Other", this.getOtherEnergy()); } diff --git a/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidConduit.java b/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidConduit.java index 3f7f7ed2..c6ed71d9 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidConduit.java +++ b/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidConduit.java @@ -114,7 +114,7 @@ public VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, ne } public boolean isFullCube(BlockState state) { return false; } - @Override public boolean isPathfindable(BlockState state, BlockGetter world, BlockPos pos, PathComputationType type) { return false; } + @Override public boolean isPathfindable(BlockState state, PathComputationType type) { return false; } /** Legacy metadata bridge retained for migration tests. */ public int getMetaFromState(BlockState state) { return 0; } diff --git a/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerConduit.java b/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerConduit.java index 8113f598..a9b14d97 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerConduit.java +++ b/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerConduit.java @@ -124,7 +124,7 @@ public VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, ne } public boolean isFullCube(BlockState state) { return false; } - @Override public boolean isPathfindable(BlockState state, BlockGetter world, BlockPos pos, PathComputationType type) { return false; } + @Override public boolean isPathfindable(BlockState state, PathComputationType type) { return false; } /** Legacy metadata bridge retained for migration tests. */ public int getMetaFromState(BlockState state) { return 0; } diff --git a/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerMachine.java b/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerMachine.java index eebc214d..73491499 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerMachine.java +++ b/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerMachine.java @@ -134,7 +134,7 @@ public BlockState getStateForPlacement(final BlockPlaceContext context) { public void setPlacedBy(final Level world, final BlockPos coord, final BlockState bs, final LivingEntity placer, final ItemStack srcItemStack) { world.setBlock(coord, bs.setValue((Property) FACING, (Comparable)placer.getDirection().getOpposite()), 2); - if (srcItemStack.hasCustomHoverName()) { + if (srcItemStack.has(net.minecraft.core.component.DataComponents.CUSTOM_NAME)) { final BlockEntity tileEntity = world.getBlockEntity(coord); if (tileEntity instanceof PoweredEntity){ ((PoweredEntity)tileEntity).setCustomInventoryName(srcItemStack.getDisplayName().getString()); diff --git a/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java b/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java index 5b772b6a..4c1988fe 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java +++ b/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java @@ -115,6 +115,7 @@ public SimpleMachineGUI(String guiImage, Integer2D... inventorySlotCoordinates){ * the GUI * @param z This is the z coordinate (no units) into the depth of the screen */ + @OnlyIn(Dist.CLIENT) public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z){} /** @@ -148,7 +149,14 @@ public Container createMenu(int windowId, Inventory player, net.minecraft.world. @Override @OnlyIn(Dist.CLIENT) public AbstractContainerScreen getContainerGUI(BlockEntity e, Player player) { - return new GUIContainer(player.getInventory(),(net.minecraft.world.Container)e); + return ClientScreens.legacy(this, e, player); + } + + @OnlyIn(Dist.CLIENT) + private static final class ClientScreens { + private static AbstractContainerScreen legacy(SimpleMachineGUI gui, BlockEntity tile, Player player) { + return gui.new GUIContainer(player.getInventory(), (net.minecraft.world.Container) tile); + } } @@ -266,7 +274,7 @@ public GUIContainer(Container container, Inventory playerItems, Component title) @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { - this.renderBackground(graphics); + this.renderBackground(graphics, mouseX, mouseY, partialTicks); super.render(graphics, mouseX, mouseY, partialTicks); this.renderTooltip(graphics, mouseX, mouseY); if (font.width(title.getString()) > width - 8 diff --git a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java index 9565785e..d147cf74 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java +++ b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java @@ -1,5 +1,7 @@ package cyano.poweradvantage.api.simple; +import net.minecraft.core.HolderLookup; +import zone.moddev.mc.basepower.util.ItemData; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PowerConnectorContext; import cyano.poweradvantage.api.PowerRequest; @@ -263,8 +265,8 @@ protected int transmitFluidToConsumers(FluidStack available, byte minumimPriorit * @param tagRoot The root of the NBT */ @Override - public void load(final CompoundTag tagRoot) { - super.load(tagRoot); + protected void loadAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.loadAdditional(tagRoot, registries); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ for(int i = 0; i < inventory.length; i++){ @@ -275,7 +277,7 @@ public void load(final CompoundTag tagRoot) { final CompoundTag nbttagcompound1 = nbttaglist .getCompound(i); final byte n = nbttagcompound1.getByte("Slot"); if (n >= 0 && n < inventory.length) { - ItemStack stack = ItemStack.of(nbttagcompound1); + ItemStack stack = ItemData.readStack(registries, nbttagcompound1); inventory[n] = stack.isEmpty() ? null : stack; } } @@ -292,16 +294,15 @@ public void load(final CompoundTag tagRoot) { * @param tagRoot The root of the NBT */ @Override - protected void saveAdditional(final CompoundTag tagRoot) { - super.saveAdditional(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.saveAdditional(tagRoot, registries); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ final ListTag nbttaglist = new ListTag(); for (int i = 0; i < inventory.length; ++i) { if (inventory[i] != null && !inventory[i].isEmpty()) { - final CompoundTag nbttagcompound1 = new CompoundTag(); + final CompoundTag nbttagcompound1 = (CompoundTag) inventory[i].save(registries); nbttagcompound1.putByte("Slot", (byte)i); - inventory[i] .save(nbttagcompound1); nbttaglist .add(nbttagcompound1); } } @@ -430,24 +431,24 @@ public void readDataFieldUpdateTag(CompoundTag tag){ */ @Override public ClientboundBlockEntityDataPacket getUpdatePacket(){ - return ClientboundBlockEntityDataPacket.create(this, tile -> createDataFieldUpdateTag()); + return ClientboundBlockEntityDataPacket.create(this, (tile, registries) -> createDataFieldUpdateTag()); } @Override - public CompoundTag getUpdateTag() { - return saveWithFullMetadata(); + public CompoundTag getUpdateTag(HolderLookup.Provider registries) { + return saveWithFullMetadata(registries); } @Override - public void handleUpdateTag(CompoundTag tag) { - load(tag); + public void handleUpdateTag(CompoundTag tag, HolderLookup.Provider registries) { + loadWithComponents(tag, registries); } /** * Receives the network packet made by getDescriptionPacket() */ @Override - public void onDataPacket(Connection net, ClientboundBlockEntityDataPacket packet) { + public void onDataPacket(Connection net, ClientboundBlockEntityDataPacket packet, HolderLookup.Provider registries) { readDataFieldUpdateTag(packet.getTag()); } @@ -634,7 +635,7 @@ public void setItem(int slot, ItemStack item) { if(this.getInventory() == null) return; ItemStack existing = this.getInventory()[slot]; final boolean flag = item != null && !item.isEmpty() && existing != null && !existing.isEmpty() - && ItemStack.isSameItemSameTags(item, existing); + && ItemStack.isSameItemSameComponents(item, existing); this.getInventory()[slot] = (item == null || item.isEmpty()) ? null : item; if (item != null && item.getCount() > this.getMaxStackSize()) { item.setCount(this.getMaxStackSize()); diff --git a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java index e78c7d3b..3ccb847c 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java +++ b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java @@ -1,5 +1,7 @@ package cyano.poweradvantage.api.simple; +import net.minecraft.core.HolderLookup; +import zone.moddev.mc.basepower.util.ItemData; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.PowerConnectorContext; import cyano.poweradvantage.api.PowerRequest; @@ -290,8 +292,8 @@ public PowerRequest getPowerRequest(ConduitType type){ * @param tagRoot The root of the NBT */ @Override - public void load(final CompoundTag tagRoot) { - super.load(tagRoot); + protected void loadAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.loadAdditional(tagRoot, registries); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ Arrays.fill(inventory, null); @@ -300,7 +302,7 @@ public void load(final CompoundTag tagRoot) { final CompoundTag nbttagcompound1 = nbttaglist .getCompound(i); final byte n = nbttagcompound1.getByte("Slot"); if (n >= 0 && n < inventory.length) { - ItemStack stack = ItemStack.of(nbttagcompound1); + ItemStack stack = ItemData.readStack(registries, nbttagcompound1); inventory[n] = stack.isEmpty() ? null : stack; } } @@ -317,16 +319,15 @@ public void load(final CompoundTag tagRoot) { * @param tagRoot The root of the NBT */ @Override - protected void saveAdditional(final CompoundTag tagRoot) { - super.saveAdditional(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.saveAdditional(tagRoot, registries); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ final ListTag nbttaglist = new ListTag(); for (int i = 0; i < inventory.length; ++i) { if (inventory[i] != null && !inventory[i].isEmpty()) { - final CompoundTag nbttagcompound1 = new CompoundTag(); + final CompoundTag nbttagcompound1 = (CompoundTag) inventory[i].save(registries); nbttagcompound1.putByte("Slot", (byte)i); - inventory[i] .save(nbttagcompound1); nbttaglist .add(nbttagcompound1); } } @@ -428,14 +429,14 @@ public void readDataFieldUpdateTag(CompoundTag tag){ */ @Override public ClientboundBlockEntityDataPacket getUpdatePacket(){ - return ClientboundBlockEntityDataPacket.create(this, tile -> createDataFieldUpdateTag()); + return ClientboundBlockEntityDataPacket.create(this, (tile, registries) -> createDataFieldUpdateTag()); } /** * Receives the network packet made by getDescriptionPacket() */ @Override - public void onDataPacket(Connection net, ClientboundBlockEntityDataPacket packet) { + public void onDataPacket(Connection net, ClientboundBlockEntityDataPacket packet, HolderLookup.Provider registries) { readDataFieldUpdateTag(packet.getTag()); } @@ -622,7 +623,7 @@ public void setItem(int slot, ItemStack item) { if(this.getInventory() == null) return; ItemStack existing = this.getInventory()[slot]; final boolean flag = item != null && !item.isEmpty() && existing != null && !existing.isEmpty() - && ItemStack.isSameItemSameTags(item, existing); + && ItemStack.isSameItemSameComponents(item, existing); this.getInventory()[slot] = (item == null || item.isEmpty()) ? null : item; if (item != null && item.getCount() > this.getMaxStackSize()) { item.setCount(this.getMaxStackSize()); diff --git a/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java b/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java index 2607b55e..cc95c1be 100644 --- a/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java +++ b/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java @@ -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.12.120011"; + public static final String VERSION = "2.4.13.120061"; public static final Logger LOGGER = LogManager.getLogger(MODID); private static PowerAdvantage instance; @@ -83,9 +83,11 @@ public PowerAdvantage() { MinecraftForge.EVENT_BUS.addListener(zone.moddev.mc.basepower.init.Villages::onTrades); FMLJavaModLoadingContext.get().getModEventBus().addListener( zone.moddev.mc.basepower.recipe.MaterialTags::registerPack); + FMLJavaModLoadingContext.get().getModEventBus().addListener(MachineGUIRegistry::registerTypes); Fluids.init(); ItemGroups.register(FMLJavaModLoadingContext.get().getModEventBus()); + RecipeConditions.register(FMLJavaModLoadingContext.get().getModEventBus()); migrateLegacyOreSpawnConfiguration(); migrateLegacyConfiguration(); @@ -139,8 +141,6 @@ private static void migrateLegacyConfiguration() { private void commonSetup(FMLCommonSetupEvent event) { Fuels.init(); GUI.init(); - // CraftingHelper's shared condition map is not safe during parallel loading. - event.enqueueWork(RecipeConditions::register); if (ModList.get().isLoaded("basemetals")) { event.enqueueWork(() -> BaseMetalsLegacyMigration.register()); } diff --git a/src/main/java/zone/moddev/mc/basepower/gui/FilteredFluidTankGUI.java b/src/main/java/zone/moddev/mc/basepower/gui/FilteredFluidTankGUI.java index 6bdc640e..077b1b25 100644 --- a/src/main/java/zone/moddev/mc/basepower/gui/FilteredFluidTankGUI.java +++ b/src/main/java/zone/moddev/mc/basepower/gui/FilteredFluidTankGUI.java @@ -1,5 +1,8 @@ package zone.moddev.mc.basepower.gui; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + import cyano.poweradvantage.api.fluid.FluidPoweredEntity; import cyano.poweradvantage.api.simple.SimpleMachineGUI; import zone.moddev.mc.basepower.math.Integer2D; @@ -15,6 +18,7 @@ public FilteredFluidTankGUI(ResourceLocation image) { } @Override + @OnlyIn(Dist.CLIENT) public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z) { // draw indicator if (srcEntity instanceof FluidPoweredEntity) { @@ -30,6 +34,7 @@ public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int } + @OnlyIn(Dist.CLIENT) public static void drawFluidBar( FluidStack fs, float barHeight, int xPos, int yPos, ResourceLocation displayImage, GUIContainer guiContainer, int x, int y, float zLevel 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 e5e9d6f6..8f173799 100644 --- a/src/main/java/zone/moddev/mc/basepower/gui/FluidTankGUI.java +++ b/src/main/java/zone/moddev/mc/basepower/gui/FluidTankGUI.java @@ -11,6 +11,8 @@ import com.mojang.blaze3d.vertex.DefaultVertexFormat; import net.minecraft.resources.ResourceLocation; import net.minecraft.client.renderer.BiomeColors; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; import cyano.poweradvantage.api.fluid.FluidStack; @@ -24,6 +26,7 @@ public FluidTankGUI(ResourceLocation image) { } @Override + @OnlyIn(Dist.CLIENT) public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z) { // draw indicator if (srcEntity instanceof FluidPoweredEntity) { @@ -39,6 +42,7 @@ public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int } + @OnlyIn(Dist.CLIENT) public static void drawFluidBar( FluidStack fs, float barHeight, int xPos, int yPos, ResourceLocation displayImage, GUIContainer guiContainer, int x, int y, float zLevel @@ -53,6 +57,7 @@ public static void drawFluidBar( } + @OnlyIn(Dist.CLIENT) public static void drawFluidFilledRectangle( GUIContainer guiContainer, FluidStack fs, int x, int y, int width, int height, diff --git a/src/main/java/zone/moddev/mc/basepower/gui/StillGUI.java b/src/main/java/zone/moddev/mc/basepower/gui/StillGUI.java index a3cc90b7..9e11f684 100644 --- a/src/main/java/zone/moddev/mc/basepower/gui/StillGUI.java +++ b/src/main/java/zone/moddev/mc/basepower/gui/StillGUI.java @@ -1,5 +1,8 @@ package zone.moddev.mc.basepower.gui; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + import cyano.poweradvantage.api.simple.SimpleMachineGUI; import zone.moddev.mc.basepower.machines.fluidmachines.StillTileEntity; import zone.moddev.mc.basepower.math.Integer2D; @@ -15,6 +18,7 @@ public StillGUI(ResourceLocation image) { } @Override + @OnlyIn(Dist.CLIENT) public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z) { // draw indicator if (srcEntity instanceof StillTileEntity) { @@ -38,4 +42,4 @@ public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int } } } -} \ No newline at end of file +} 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 0098eaf3..af1e364d 100644 --- a/src/main/java/zone/moddev/mc/basepower/init/Fluids.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Fluids.java @@ -64,10 +64,7 @@ public static void registerFluids(RegisterEvent event) { event.register(ForgeRegistries.Keys.FLUID_TYPES, id("refined_oil"), () -> refinedOilType); } if (!ForgeRegistries.Keys.FLUIDS.equals(event.getRegistryKey())) return; - flowing_crude_oil = new PowerFluid.Flowing(Kind.CRUDE); - crude_oil = new PowerFluid.Source(Kind.CRUDE); - flowing_refined_oil = new PowerFluid.Flowing(Kind.REFINED); - refined_oil = new PowerFluid.Source(Kind.REFINED); + prepareOilFluids(); event.register(ForgeRegistries.Keys.FLUIDS, id("flowing_crude_oil"), () -> flowing_crude_oil); event.register(ForgeRegistries.Keys.FLUIDS, id("crude_oil"), () -> crude_oil); @@ -79,6 +76,14 @@ public static void registerFluids(RegisterEvent event) { registerNewFluid(flowing_refined_oil); } + static synchronized void prepareOilFluids() { + if (crude_oil != null) return; + flowing_crude_oil = new PowerFluid.Flowing(Kind.CRUDE); + crude_oil = new PowerFluid.Source(Kind.CRUDE); + flowing_refined_oil = new PowerFluid.Flowing(Kind.REFINED); + refined_oil = new PowerFluid.Source(Kind.REFINED); + } + private static ResourceLocation id(String name) { return new ResourceLocation(PowerAdvantage.MODID, name); } 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 92cceb25..eb4348f0 100644 --- a/src/main/java/zone/moddev/mc/basepower/init/Items.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Items.java @@ -35,9 +35,11 @@ public static synchronized void init() { bioplastic_ingot = add("bioplastic_ingot", new Item(properties())); sprocket = add("sprocket", new Item(properties())); rotator_tool = add("rotator_tool", new RotationTool(properties().stacksTo(1))); - crude_oil_bucket = add("crude_oil_bucket", new BucketItem(() -> Fluids.crude_oil, + Fluids.prepareOilFluids(); + // Forge 50's vanilla bucket placement still reads the eager fluid field. + crude_oil_bucket = add("crude_oil_bucket", new BucketItem(Fluids.crude_oil, properties().stacksTo(1).craftRemainder(net.minecraft.world.item.Items.BUCKET))); - refined_oil_bucket = add("refined_oil_bucket", new BucketItem(() -> Fluids.refined_oil, + refined_oil_bucket = add("refined_oil_bucket", new BucketItem(Fluids.refined_oil, properties().stacksTo(1).craftRemainder(net.minecraft.world.item.Items.BUCKET))); initialized = true; } diff --git a/src/main/java/zone/moddev/mc/basepower/init/Villages.java b/src/main/java/zone/moddev/mc/basepower/init/Villages.java index de04a4ea..499519f9 100644 --- a/src/main/java/zone/moddev/mc/basepower/init/Villages.java +++ b/src/main/java/zone/moddev/mc/basepower/init/Villages.java @@ -3,6 +3,7 @@ import net.minecraft.world.entity.npc.VillagerProfession; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.item.trading.ItemCost; import net.minecraftforge.event.village.VillagerTradesEvent; /** The old farmer and toolsmith careers now have their own vanilla professions. */ @@ -12,11 +13,11 @@ private Villages() {} public static void onTrades(VillagerTradesEvent event) { if (event.getType() == VillagerProfession.TOOLSMITH) { event.getTrades().get(1).add((villager, random) -> new MerchantOffer( - new ItemStack(net.minecraft.world.item.Items.EMERALD), + new ItemCost(net.minecraft.world.item.Items.EMERALD), new ItemStack(Items.sprocket, 1 + random.nextInt(4)), 7, 1, 0.0F)); } else if (event.getType() == VillagerProfession.FARMER) { event.getTrades().get(1).add((villager, random) -> new MerchantOffer( - new ItemStack(net.minecraft.world.item.Items.EMERALD), + new ItemCost(net.minecraft.world.item.Items.EMERALD), new ItemStack(Items.bioplastic_ingot, 4 + random.nextInt(5)), 7, 1, 0.0F)); } } diff --git a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyor.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyor.java index fe6f5a84..54eaa087 100644 --- a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyor.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityConveyor.java @@ -1,5 +1,7 @@ package zone.moddev.mc.basepower.machines.conveyors; +import net.minecraft.core.HolderLookup; +import zone.moddev.mc.basepower.util.ItemData; import zone.moddev.mc.basepower.util.InventoryWrapper; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.entity.item.ItemEntity; @@ -159,7 +161,7 @@ protected static boolean canInsertItemInto(ItemStack item, WorldlyContainer dest ItemStack destItem = dest.getItem(slot); if(destItem == null || destItem.isEmpty()) { return true; - } else if (ItemStack.isSameItemSameTags(item, destItem) + } else if (ItemStack.isSameItemSameComponents(item, destItem) && destItem.getCount() < Math.min(dest.getMaxStackSize(), destItem.getMaxStackSize())) { return true; } @@ -183,7 +185,7 @@ protected static boolean transferItem(WorldlyContainer src, Direction srcFace, W if(dest.canPlaceItemThroughFace(destValidSlots[j], item, destFace)){ ItemStack otherItem = dest.getItem(destValidSlots[j]); if(otherItem != null && !otherItem.isEmpty() - && ItemStack.isSameItemSameTags(item, otherItem) + && ItemStack.isSameItemSameComponents(item, otherItem) && otherItem.getCount() < dest.getMaxStackSize() && otherItem.getCount() < otherItem.getMaxStackSize()){ src.removeItem(srcValidSlots[i], 1); @@ -239,8 +241,8 @@ protected ItemStack[] getInventory(){ * @param tagRoot The root of the NBT */ @Override - public void load(final CompoundTag tagRoot) { - super.load(tagRoot); + protected void loadAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.loadAdditional(tagRoot, registries); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ Arrays.fill(inventory, null); @@ -249,7 +251,7 @@ public void load(final CompoundTag tagRoot) { final CompoundTag nbttagcompound1 = nbttaglist .getCompound(i); final byte n = nbttagcompound1.getByte("Slot"); if (n >= 0 && n < inventory.length) { - ItemStack stack = ItemStack.of(nbttagcompound1); + ItemStack stack = ItemData.readStack(registries, nbttagcompound1); inventory[n] = stack.isEmpty() ? null : stack; } } @@ -266,16 +268,15 @@ public void load(final CompoundTag tagRoot) { * @param tagRoot The root of the NBT */ @Override - protected void saveAdditional(final CompoundTag tagRoot) { - super.saveAdditional(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.saveAdditional(tagRoot, registries); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ final ListTag nbttaglist = new ListTag(); for (int i = 0; i < inventory.length; ++i) { if (inventory[i] != null && !inventory[i].isEmpty()) { - final CompoundTag nbttagcompound1 = new CompoundTag(); + final CompoundTag nbttagcompound1 = (CompoundTag) inventory[i].save(registries); nbttagcompound1.putByte("Slot", (byte)i); - inventory[i] .save(nbttagcompound1); nbttaglist .add(nbttagcompound1); } } @@ -394,7 +395,7 @@ public void setField(int arg0, int arg1) { public void setItem(int slot, ItemStack item) { ItemStack existing = this.getInventory()[slot]; final boolean flag = item != null && !item.isEmpty() && existing != null && !existing.isEmpty() - && ItemStack.isSameItemSameTags(item, existing); + && ItemStack.isSameItemSameComponents(item, existing); this.getInventory()[slot] = (item == null || item.isEmpty()) ? null : item; if (item != null && item.getCount() > this.getMaxStackSize()) { item.setCount(this.getMaxStackSize()); diff --git a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityFoodFilter.java b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityFoodFilter.java index 902595e8..735ef364 100644 --- a/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityFoodFilter.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/conveyors/TileEntityFoodFilter.java @@ -9,7 +9,7 @@ public class TileEntityFoodFilter extends TileEntityConveyorFilter { @Override public boolean matchesFilter(ItemStack item) { if (item == null) return false; - return !item.isEmpty() && item.getItem().isEdible(); + return !item.isEmpty() && item.has(net.minecraft.core.component.DataComponents.FOOD); } } diff --git a/src/main/java/zone/moddev/mc/basepower/machines/creative/InfiniteEnergyTileEntity.java b/src/main/java/zone/moddev/mc/basepower/machines/creative/InfiniteEnergyTileEntity.java index 34ad7b2c..21401aa0 100644 --- a/src/main/java/zone/moddev/mc/basepower/machines/creative/InfiniteEnergyTileEntity.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/creative/InfiniteEnergyTileEntity.java @@ -1,5 +1,6 @@ package zone.moddev.mc.basepower.machines.creative; +import net.minecraft.core.HolderLookup; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.simple.TileEntitySimplePowerMachine; import net.minecraft.world.item.ItemStack; @@ -89,14 +90,14 @@ public void powerUpdate() { } @Override - protected void saveAdditional(final CompoundTag tagRoot) { - super.saveAdditional(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.saveAdditional(tagRoot, registries); tagRoot.putString("InfType", this.type[0].toString()); } @Override - public void load(final CompoundTag tagRoot) { - super.load(tagRoot); + protected void loadAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.loadAdditional(tagRoot, registries); if (tagRoot.contains("InfType", 8)) { this.type[0] = new ConduitType(tagRoot.getString("InfType")); } diff --git a/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDischargeTileEntity.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDischargeTileEntity.java index df99311c..be5965ad 100644 --- a/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDischargeTileEntity.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDischargeTileEntity.java @@ -1,5 +1,6 @@ package zone.moddev.mc.basepower.machines.fluidmachines; +import net.minecraft.core.HolderLookup; import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.FluidTank; @@ -81,17 +82,17 @@ private static BlockPos findPlacement(Level world, BlockPos initial, Fluid fluid @Override public boolean isFluidSource() { return false; } @Override public boolean isFluidSink() { return true; } - @Override public CompoundTag getUpdateTag() { + @Override public CompoundTag getUpdateTag(HolderLookup.Provider registries) { CompoundTag tag = new CompoundTag(); getTank().writeToNBT(tag); return tag; } - @Override public void handleUpdateTag(CompoundTag tag) { getTank().readFromNBT(tag); } + @Override public void handleUpdateTag(CompoundTag tag, HolderLookup.Provider registries) { getTank().readFromNBT(tag); } @Override public ClientboundBlockEntityDataPacket getUpdatePacket() { return ClientboundBlockEntityDataPacket.create(this); } - @Override public void onDataPacket(Connection net, ClientboundBlockEntityDataPacket packet) { - handleUpdateTag(packet.getTag()); + @Override public void onDataPacket(Connection net, ClientboundBlockEntityDataPacket packet, HolderLookup.Provider registries) { + handleUpdateTag(packet.getTag(), registries); } public static BlockPos scanFluidSpaceForNonsourceBlock(Level world, BlockPos initial, Fluid fluid, int range) { diff --git a/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDrainTileEntity.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDrainTileEntity.java index 9b0d546b..5c92f517 100644 --- a/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDrainTileEntity.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/FluidDrainTileEntity.java @@ -64,7 +64,7 @@ public void powerUpdate() { static Fluid pickupSource(LevelAccessor world, BlockPos position, BlockState state) { if (!(state.getBlock() instanceof BucketPickup)) return net.minecraft.world.level.material.Fluids.EMPTY; Fluid fluid = state.getFluidState().getType(); - ItemStack bucket = ((BucketPickup)state.getBlock()).pickupBlock(world, position, state); + ItemStack bucket = ((BucketPickup)state.getBlock()).pickupBlock(null, world, position, state); return bucket.isEmpty() ? net.minecraft.world.level.material.Fluids.EMPTY : fluid; } diff --git a/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StillTileEntity.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StillTileEntity.java index 3bf67576..f3e3bee0 100644 --- a/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StillTileEntity.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StillTileEntity.java @@ -1,5 +1,6 @@ package zone.moddev.mc.basepower.machines.fluidmachines; +import net.minecraft.core.HolderLookup; import cyano.poweradvantage.api.fluid.FluidRequest; import cyano.poweradvantage.api.simple.TileEntitySimpleFluidMachine; import zone.moddev.mc.basepower.registry.FuelRegistry; @@ -247,8 +248,8 @@ public FluidTankInfo[] getTankInfo(Direction face) { * @param tagRoot An NBT tag */ @Override - protected void saveAdditional(final CompoundTag tagRoot) { - super.saveAdditional(tagRoot); + protected void saveAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.saveAdditional(tagRoot, registries); CompoundTag tankTag2 = new CompoundTag(); inputTank.writeToNBT(tankTag2); tagRoot.put("InputTank", tankTag2); @@ -262,8 +263,8 @@ protected void saveAdditional(final CompoundTag tagRoot) { * @param tagRoot An NBT tag */ @Override - public void load(final CompoundTag tagRoot) { - super.load(tagRoot); + protected void loadAdditional(final CompoundTag tagRoot, HolderLookup.Provider registries) { + super.loadAdditional(tagRoot, registries); if (tagRoot.contains("InputTank", 10)) { CompoundTag tankTag2 = tagRoot .getCompound("InputTank"); inputTank.readFromNBT(tankTag2); diff --git a/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StorageTankBlock.java b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StorageTankBlock.java index b379b0d0..e1bd397d 100644 --- a/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StorageTankBlock.java +++ b/src/main/java/zone/moddev/mc/basepower/machines/fluidmachines/StorageTankBlock.java @@ -14,14 +14,17 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.item.ItemStack; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.CustomData; import net.minecraft.nbt.CompoundTag; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.core.BlockPos; import net.minecraft.core.Registry; import net.minecraft.network.chat.Component; -import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.item.Item; import net.minecraft.world.level.Level; import zone.moddev.mc.basepower.init.Fluids; +import zone.moddev.mc.basepower.util.ItemData; /** Portable tank block. FluidID and Volume remain unchanged for old item stacks. */ public class StorageTankBlock extends BlockSimpleFluidMachine { @@ -40,9 +43,9 @@ public class StorageTankBlock extends BlockSimpleFluidMachine { public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { super.setPlacedBy(world, pos, state, placer, stack); - CompoundTag tag = stack.getTag(); + CompoundTag tag = ItemData.customData(stack); BlockEntity tile = world.getBlockEntity(pos); - if (tag != null && tag.contains("FluidID", 8) && tag.contains("Volume", 99) + if (tag.contains("FluidID", 8) && tag.contains("Volume", 99) && tile instanceof StorageTankTileEntity) { net.minecraft.world.level.material.Fluid fluid = Fluids.byName(tag.getString("FluidID")); if (fluid != null) ((StorageTankTileEntity) tile).getTank() @@ -58,16 +61,16 @@ public ItemStack createItem(StorageTankTileEntity tile) { .getKey(tile.getTank().getFluid().getFluid()); if (id != null) tag.putString("FluidID", id.toString()); tag.putInt("Volume", tile.getTank().getFluidAmount()); - stack.setTag(tag); + stack.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)); } return stack; } @Override - public void appendHoverText(ItemStack stack, @Nullable BlockGetter world, + public void appendHoverText(ItemStack stack, Item.TooltipContext context, List tooltip, TooltipFlag flag) { - CompoundTag tag = stack.getTag(); - if (tag == null || !tag.contains("FluidID", 8) || !tag.contains("Volume", 99)) { + CompoundTag tag = ItemData.customData(stack); + if (!tag.contains("FluidID", 8) || !tag.contains("Volume", 99)) { tooltip.add(Component.translatable("tooltip.basepower.empty")); return; } 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 6725f4c7..d879a840 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/LegacyEntityDataFixes.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyEntityDataFixes.java @@ -20,8 +20,12 @@ final class LegacyEntityDataFixes { private static final String[][] FORGE_ATTRIBUTES = { {"forge.swimSpeed", "forge:swim_speed"}, {"forge.nameTagDistance", "forge:nametag_distance"}, - {"forge.entity_gravity", "forge:entity_gravity"}, - {"generic.reachDistance", "forge:reach_distance"} + {"forge.entity_gravity", "minecraft:generic.gravity"}, + {"forge:entity_gravity", "minecraft:generic.gravity"}, + {"generic.reachDistance", "minecraft:player.block_interaction_range"}, + {"forge:reach_distance", "minecraft:player.block_interaction_range"}, + {"forge:block_reach", "minecraft:player.block_interaction_range"}, + {"forge:entity_reach", "minecraft:player.entity_interaction_range"} }; private LegacyEntityDataFixes() {} diff --git a/src/main/java/zone/moddev/mc/basepower/migration/LegacyMachineItemDataFixes.java b/src/main/java/zone/moddev/mc/basepower/migration/LegacyMachineItemDataFixes.java index d255afc5..7a992db9 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/LegacyMachineItemDataFixes.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyMachineItemDataFixes.java @@ -5,6 +5,7 @@ import com.mojang.serialization.Dynamic; import java.util.ArrayList; import java.util.Objects; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.Tag; import net.minecraft.nbt.ListTag; @@ -13,6 +14,7 @@ import net.minecraft.SharedConstants; import net.minecraft.util.datafix.DataFixers; import net.minecraft.util.datafix.fixes.References; +import zone.moddev.mc.basepower.util.ItemData; /** Vanilla cannot traverse inventories behind unknown mod block-entity choices. */ final class LegacyMachineItemDataFixes { @@ -43,11 +45,17 @@ private static Tag rewrite(Tag value, int version, int current) { if (supported(id)) { DataFixer fixer = DataFixers.getDataFixer(); CompoundTag input = version < 704 ? upgradeTo704(compound, version, fixer) : compound.copy(); - CompoundTag fixed = (CompoundTag) fixer.update(References.ITEM_STACK, - new Dynamic<>(NbtOps.INSTANCE, input), Math.max(version, 704), current).getValue(); + int from = Math.max(version, 704); + if (from < ItemData.LAST_PORT_DATA_VERSION) { + CompoundTag fixed = (CompoundTag) fixer.update(References.ITEM_STACK, + new Dynamic<>(NbtOps.INSTANCE, input), from, ItemData.LAST_PORT_DATA_VERSION).getValue(); + input = (CompoundTag) preserve(compound, fixed); + from = ItemData.LAST_PORT_DATA_VERSION; + } // The native item schema also fixes its nested vanilla inventories. // Do not visit them again with the old version and reset tool damage. - return preserve(compound, fixed); + return fixer.update(References.ITEM_STACK, + new Dynamic<>(NbtOps.INSTANCE, input), from, current).getValue(); } return compound; } @@ -102,7 +110,7 @@ private static Tag preserve(Tag original, Tag result) { || before.getInt("Slot") != after.getInt("Slot")))) { throw new IllegalStateException("Native item migration changed inventory count or slot"); } - if (!supported(id)) return before.copy(); + if (!supported(id)) return ItemData.withLegacyWear(before.copy(), BuiltInRegistries.ITEM.asLookup()); restoreNamedEnchantments(before, after, "ench", "Enchantments"); restoreNamedEnchantments(before, after, "StoredEnchantments", "StoredEnchantments"); } diff --git a/src/main/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes.java b/src/main/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes.java index bee4ec80..5fd34420 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes.java @@ -1,11 +1,13 @@ package zone.moddev.mc.basepower.migration; import com.mojang.datafixers.DataFixer; +import com.mojang.serialization.Dynamic; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.Tag; import net.minecraft.nbt.ListTag; import net.minecraft.SharedConstants; import net.minecraft.util.datafix.DataFixTypes; +import zone.moddev.mc.basepower.util.ItemData; /** Keeps saved Forge enchantment names across the native numeric player fixers. */ public final class LegacyPlayerDataFixes { @@ -15,6 +17,16 @@ public static CompoundTag updateNativePlayer(DataFixTypes type, DataFixer fixer, return update(fixer, type, source, version); } + public static Dynamic updateNativeEmbeddedPlayer(DataFixTypes type, DataFixer fixer, + Dynamic source, int version) { + if (source.getValue() instanceof CompoundTag compound) { + @SuppressWarnings("unchecked") + T result = (T) update(fixer, type, compound, version); + return new Dynamic<>(source.getOps(), result); + } + return type.updateToCurrentVersion(fixer, source, version); + } + public static CompoundTag update(DataFixer fixer, DataFixTypes type, CompoundTag source, int version) { if (type != DataFixTypes.PLAYER) throw new IllegalArgumentException("Expected native PLAYER type"); CompoundTag result = source.copy(); @@ -26,9 +38,12 @@ public static CompoundTag update(DataFixer fixer, DataFixTypes type, CompoundTag result = type.update(fixer, result, version, 704); version = 704; } - result = type.update(fixer, result, version, current); - preserveNames(source, result); - return result; + if (version < ItemData.LAST_PORT_DATA_VERSION) { + result = type.update(fixer, result, version, ItemData.LAST_PORT_DATA_VERSION); + preserveNames(source, result); + version = ItemData.LAST_PORT_DATA_VERSION; + } + return type.update(fixer, result, version, current); } static void preserveNames(Tag original, Tag result) { 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 e1185f00..2b655f8a 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldDataHook.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldDataHook.java @@ -23,11 +23,11 @@ import com.mojang.serialization.Dynamic; -import cpw.mods.modlauncher.api.INameMappingService; import cyano.poweradvantage.api.migration.LegacyMigrationApi; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtAccounter; import net.minecraft.nbt.Tag; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; @@ -68,7 +68,7 @@ public static synchronized void prepareLegacyWorld(File levelDat) { try { if (source != null) { try (FileInputStream input = new FileInputStream(source)) { - CompoundTag registryArchive = NbtIo.readCompressed(input); + CompoundTag registryArchive = NbtIo.readCompressed(input, NbtAccounter.unlimitedHeap()); if (!containsLegacyRegistryEntry(registryArchive)) { throw new IOException("Invalid retained Advantage registry snapshot " + source); } @@ -79,7 +79,7 @@ public static synchronized void prepareLegacyWorld(File levelDat) { if (levelDat.isFile()) { // A backup can supply registry IDs, but never replace newer world data. try (FileInputStream current = new FileInputStream(levelDat)) { - CompoundTag root = NbtIo.readCompressed(current); + CompoundTag root = NbtIo.readCompressed(current, NbtAccounter.unlimitedHeap()); if (source != null || hasAdvantageNamespaceAliases(root)) migrateLoosePlayerData(levelDat, root); } } @@ -104,7 +104,7 @@ static void prepareForgeRegistryMetadata(File levelDat) { try { CompoundTag root; try (FileInputStream input = new FileInputStream(levelDat)) { - root = NbtIo.readCompressed(input); + root = NbtIo.readCompressed(input, NbtAccounter.unlimitedHeap()); } boolean changed = removeRetiredVanillaProfessionRegistry(root); changed |= renameFlattenedBiomeRegistry(root); @@ -221,6 +221,11 @@ public static CompoundTag prepareEntityChunkForMigration(CompoundTag source) { return root; } + public static CompoundTag upgradeEntityChunk( + net.minecraft.world.level.chunk.storage.SimpleRegionStorage storage, CompoundTag source, int fallbackVersion) { + return finalizeLegacyChunk(storage.upgradeChunkTag(prepareEntityChunkForMigration(source), fallbackVersion)); + } + /** Called after all mods' entry hooks, immediately before native chunk datafixing. */ public static void prepareLegacyEntities(CompoundTag root) { if (root != null) { @@ -374,7 +379,7 @@ private static void migrateLoosePlayerData(File levelDat, CompoundTag root) thro Arrays.sort(files); for (File file : files) { try (FileInputStream input = new FileInputStream(file)) { - CompoundTag player = NbtIo.readCompressed(input); + CompoundTag player = NbtIo.readCompressed(input, NbtAccounter.unlimitedHeap()); if (migrateLegacyNbt(player) > 0) writeWithBackup(file, player); } } @@ -465,7 +470,7 @@ static File legacyRegistrySource(File levelDat) { for (File candidate : new File[] { levelDat, new File(levelDat.getParentFile(), "level.dat_old") }) { if (!candidate.isFile()) continue; try (FileInputStream input = new FileInputStream(candidate)) { - CompoundTag root = NbtIo.readCompressed(input); + CompoundTag root = NbtIo.readCompressed(input, NbtAccounter.unlimitedHeap()); if (root.contains("FML", 10) && containsLegacyRegistryEntry(root)) return candidate; } catch (IOException ignored) {} } @@ -480,7 +485,7 @@ static void preserveLegacyRegistry(File levelDat, CompoundTag source) throws IOE archive.put("FML", source.getCompound("FML").copy()); java.nio.file.Path temporary = Files.createTempFile(target.getParent(), "basepower-legacy-", ".tmp"); try { - NbtIo.writeCompressed(archive, temporary.toFile()); + NbtIo.writeCompressed(archive, temporary); Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE); } finally { Files.deleteIfExists(temporary); @@ -560,8 +565,7 @@ private static int indexLegacyChunks(ResourceLocation dimension, File regionDire @SuppressWarnings("unchecked") private static Dynamic[] expandFlatteningTable(int requiredLength) { try { - String fieldName = ObfuscationReflectionHelper.remapName(INameMappingService.Domain.FIELD, "f_14934_"); - Field valuesField = BlockStateData.class.getDeclaredField(fieldName); + Field valuesField = BlockStateData.class.getDeclaredField("MAP"); valuesField.setAccessible(true); if (Modifier.isFinal(valuesField.getModifiers())) { throw new IllegalStateException("The Advantage flattening-table hook did not run"); diff --git a/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldMetadata.java b/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldMetadata.java index f0214a4f..5c26da0e 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldMetadata.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/LegacyWorldMetadata.java @@ -10,6 +10,7 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtAccounter; import net.minecraft.nbt.StringTag; import net.minecraft.nbt.Tag; import net.minecraft.resources.ResourceLocation; @@ -21,7 +22,7 @@ private LegacyWorldMetadata() {} public static CompoundTag read(File levelDat) throws IOException { LegacyWorldDataHook.prepareForgeRegistryMetadata(levelDat); - CompoundTag root = NbtIo.readCompressed(levelDat); + CompoundTag root = NbtIo.readCompressed(levelDat.toPath(), NbtAccounter.unlimitedHeap()); remap(root, ModList.get()::isLoaded); return root; } @@ -31,6 +32,11 @@ public static Tag remapLightweight(Tag root) { return root; } + public static CompoundTag remapRaw(CompoundTag root) { + remap(root, ModList.get()::isLoaded); + return root; + } + static void remap(CompoundTag root, Predicate loaded) { CompoundTag forge = root.getCompound("fml"); CompoundTag registries = forge.getCompound("Registries"); 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 756e1036..bc9ebe12 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java @@ -39,6 +39,7 @@ public static void register() { 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); } @@ -70,8 +71,10 @@ private static void fluids(MissingMappingsEvent event) { id -> new ResourceLocation(LegacyMigrationApi.fluidTarget(id.toString()))); } private static void enchantments(MissingMappingsEvent event) { + remap(event, ForgeRegistries.ENCHANTMENTS); remapAdvantage(event, ForgeRegistries.ENCHANTMENTS, LegacyMigrationApi::registryTarget); } + private static void potions(MissingMappingsEvent event) { remap(event, ForgeRegistries.POTIONS); } private static void menus(MissingMappingsEvent event) { remapAdvantage(event, ForgeRegistries.MENU_TYPES, LegacyMigrationApi::registryTarget); } @@ -92,6 +95,12 @@ private static void remapAdvantage(MissingMappingsEvent event, static void remap(MissingMappingsEvent event, IForgeRegistry active) { for (MissingMappingsEvent.Mapping mapping : event.getMappings(active.getRegistryKey(), "minecraft")) { + 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. + mapping.ignore(); + continue; + } if (retiredTransientMemory(active.getRegistryName(), mapping.getKey())) { // Both old types used register(name) without a serializer: no saved // brain values exist. Retire only their obsolete Forge ID indexes. @@ -153,6 +162,11 @@ static ResourceLocation replacement(ResourceLocation registry, ResourceLocation if ((registry.equals(new ResourceLocation("minecraft:block")) || registry.equals(new ResourceLocation("minecraft:item"))) && old.getPath().equals("grass_path")) { target = "dirt_path"; + } else if ((registry.equals(ResourceLocation.parse("minecraft:block")) || registry.equals(ResourceLocation.parse("minecraft:item"))) + && old.getPath().equals("grass")) { + 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")) { target = "zombified_piglin"; } else if (registry.equals(new ResourceLocation("minecraft:item")) && old.getPath().equals("zombie_pigman_spawn_egg")) { @@ -164,6 +178,7 @@ static ResourceLocation replacement(ResourceLocation registry, ResourceLocation case "pottery_shard_arms_up" -> "arms_up_pottery_sherd"; case "pottery_shard_prize" -> "prize_pottery_sherd"; case "pottery_shard_skull" -> "skull_pottery_sherd"; + case "scute" -> "turtle_scute"; default -> null; }; } else if (registry.equals(new ResourceLocation("minecraft:worldgen/biome")) && old.getPath().equals("nether")) { 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 79b46eb5..c0bddba6 100644 --- a/src/main/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPack.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/MaterialPolicyPack.java @@ -8,14 +8,17 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; import java.util.Set; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.PackLocationInfo; import net.minecraft.server.packs.PackType; import net.minecraft.server.packs.metadata.MetadataSectionSerializer; import net.minecraft.server.packs.metadata.pack.PackMetadataSection; import net.minecraft.server.packs.resources.IoSupplier; +import net.minecraft.server.packs.repository.PackSource; import net.minecraftforge.fml.ModList; import zone.moddev.mc.basepower.PowerAdvantage; import zone.moddev.mc.basepower.RecipeMode; @@ -24,6 +27,8 @@ final class MaterialPolicyPack implements PackResources { static final String ID = "basepower:material_policy"; static final String NAME = "Power Advantage material aliases"; + static final PackLocationInfo LOCATION = + new PackLocationInfo(ID, Component.literal(NAME), PackSource.BUILT_IN, Optional.empty()); private final Map resources = new LinkedHashMap<>(); MaterialPolicyPack() { @@ -77,8 +82,8 @@ 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), 12) : null; + ? (T)new PackMetadataSection(Component.literal(NAME), 41, Optional.empty()) : null; } - @Override public String packId() { return ID; } + @Override public PackLocationInfo location() { return LOCATION; } @Override public void close() {} } diff --git a/src/main/java/zone/moddev/mc/basepower/recipe/MaterialTags.java b/src/main/java/zone/moddev/mc/basepower/recipe/MaterialTags.java index ce20d88f..3fa3a25f 100644 --- a/src/main/java/zone/moddev/mc/basepower/recipe/MaterialTags.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/MaterialTags.java @@ -1,17 +1,27 @@ package zone.moddev.mc.basepower.recipe; +import net.minecraft.server.packs.PackLocationInfo; +import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.PackSelectionConfig; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.repository.Pack; +import net.minecraftforge.event.AddPackFindersEvent; + /** Config-dependent aliases resolved by the server's native tag reload. */ public final class MaterialTags { private MaterialTags() {} - public static void registerPack(net.minecraftforge.event.AddPackFindersEvent event) { - if (event.getPackType() != net.minecraft.server.packs.PackType.SERVER_DATA) return; + public static void registerPack(AddPackFindersEvent event) { + if (event.getPackType() != PackType.SERVER_DATA) return; event.addRepositorySource(consumer -> { - net.minecraft.server.packs.repository.Pack pack = net.minecraft.server.packs.repository.Pack.readMetaAndCreate( - MaterialPolicyPack.ID, net.minecraft.network.chat.Component.literal(MaterialPolicyPack.NAME), - true, id -> new MaterialPolicyPack(), event.getPackType(), - net.minecraft.server.packs.repository.Pack.Position.BOTTOM, - net.minecraft.server.packs.repository.PackSource.BUILT_IN); + Pack pack = Pack.readMetaAndCreate(MaterialPolicyPack.LOCATION, new Pack.ResourcesSupplier() { + @Override public PackResources openPrimary(PackLocationInfo location) { + return new MaterialPolicyPack(); + } + @Override public PackResources openFull(PackLocationInfo location, Pack.Metadata metadata) { + return new MaterialPolicyPack(); + } + }, event.getPackType(), new PackSelectionConfig(true, Pack.Position.BOTTOM, false)); if (pack != null) consumer.accept(pack); }); } 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 cc1bef70..313b9352 100644 --- a/src/main/java/zone/moddev/mc/basepower/recipe/PartsLoot.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/PartsLoot.java @@ -6,7 +6,9 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.storage.loot.providers.number.ConstantValue; import net.minecraft.world.level.storage.loot.LootPool; -import net.minecraft.world.level.storage.loot.entries.LootTableReference; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.storage.loot.entries.NestedLootTable; import net.minecraftforge.event.LootTableLoadEvent; import net.minecraftforge.fml.ModList; import zone.moddev.mc.basepower.PowerAdvantage; @@ -32,9 +34,14 @@ static void addPools(net.minecraft.world.level.storage.loot.LootTable table, Res if (!loaded.test(mod) || (mod.equals("basepower") && name.getPath().equals("chests/end_city_treasure"))) continue; String poolName = mod + ":legacy_parts"; - if (table.getPool(poolName) != null) continue; - table.addPool(LootPool.lootPool().name(poolName) .setRolls(ConstantValue.exactly(1)) - .add(LootTableReference.lootTableReference(new ResourceLocation(mod, "chests/advantage_parts"))).build()); + LootPool existing = table.removePool(poolName); + if (existing != null) { + table.addPool(existing); + continue; + } + table.addPool(LootPool.lootPool().name(poolName).setRolls(ConstantValue.exactly(1)) + .add(NestedLootTable.lootTableReference(ResourceKey.create(Registries.LOOT_TABLE, + new ResourceLocation(mod, "chests/advantage_parts")))).build()); } } diff --git a/src/main/java/zone/moddev/mc/basepower/recipe/RecipeConditions.java b/src/main/java/zone/moddev/mc/basepower/recipe/RecipeConditions.java index 3d16854c..74eb3c31 100644 --- a/src/main/java/zone/moddev/mc/basepower/recipe/RecipeConditions.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/RecipeConditions.java @@ -1,64 +1,59 @@ package zone.moddev.mc.basepower.recipe; -import com.google.gson.JsonObject; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.MapCodec; import java.util.Locale; -import net.minecraft.tags.ItemTags; -import net.minecraft.util.GsonHelper; import net.minecraft.resources.ResourceLocation; -import net.minecraftforge.common.crafting.CraftingHelper; +import net.minecraft.tags.ItemTags; import net.minecraftforge.common.crafting.conditions.ICondition; -import net.minecraftforge.common.crafting.conditions.IConditionSerializer; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; import zone.moddev.mc.basepower.PowerAdvantage; import zone.moddev.mc.basepower.RecipeMode; -/** Registers the data-pack conditions used by PowerAdvantage recipe modes. */ +/** Forge recipe conditions for the existing modes and optional material tags. */ public final class RecipeConditions { - private static boolean registered; + private static final DeferredRegister> SERIALIZERS = + DeferredRegister.create(ForgeRegistries.Keys.CONDITION_SERIALIZERS, PowerAdvantage.MODID); + + static { + SERIALIZERS.register("recipe_mode", () -> Mode.MATCH); + SERIALIZERS.register("not_recipe_mode", () -> Mode.MISMATCH); + SERIALIZERS.register("tag_empty", () -> Material.EMPTY); + SERIALIZERS.register("tag_not_empty", () -> Material.NOT_EMPTY); + } private RecipeConditions() {} - public static synchronized void register() { - if (registered) return; - for (String name : new String[] {"recipe_mode", "not_recipe_mode", "tag_empty", "tag_not_empty"}) { - CraftingHelper.register(new Serializer(name)); - } - registered = true; - } + public static void register(IEventBus bus) { SERIALIZERS.register(bus); } - private static final class Condition implements ICondition { - private final ResourceLocation id; - private final String value; + private record Mode(String mode, boolean match) implements ICondition { + private static final MapCodec MATCH = create(true); + private static final MapCodec MISMATCH = create(false); - private Condition(ResourceLocation id, String value) { - this.id = id; - this.value = value; + private static MapCodec create(boolean match) { + return Codec.STRING.fieldOf("mode").xmap(value -> new Mode(value, match), Mode::mode); } - @Override public ResourceLocation getID() { return id; } - - - @Override public boolean test(IContext context) { - switch (id.getPath()) { - case "recipe_mode": return PowerAdvantage.recipeMode == RecipeMode.valueOf(value.toUpperCase(Locale.ROOT)); - case "not_recipe_mode": return PowerAdvantage.recipeMode != RecipeMode.valueOf(value.toUpperCase(Locale.ROOT)); - case "tag_empty": return context.getTag(ItemTags.create(new ResourceLocation(value))).isEmpty(); - case "tag_not_empty": return !context.getTag(ItemTags.create(new ResourceLocation(value))).isEmpty(); - default: throw new IllegalStateException("Unknown Advantage condition " + id); - } + @Override public MapCodec codec() { return match ? MATCH : MISMATCH; } + @Override public boolean test(IContext context, DynamicOps ops) { + return (PowerAdvantage.recipeMode == RecipeMode.valueOf(mode.toUpperCase(Locale.ROOT))) == match; } } - private static final class Serializer implements IConditionSerializer { - private final ResourceLocation id; - private final String key; + private record Material(ResourceLocation tag, boolean empty) implements ICondition { + private static final MapCodec EMPTY = create(true); + private static final MapCodec NOT_EMPTY = create(false); - private Serializer(String name) { - id = new ResourceLocation(PowerAdvantage.MODID, name); - key = name.contains("recipe_mode") ? "mode" : "tag"; + private static MapCodec create(boolean empty) { + return ResourceLocation.CODEC.fieldOf("tag").xmap(id -> new Material(id, empty), Material::tag); } - @Override public ResourceLocation getID() { return id; } - @Override public Condition read(JsonObject json) { return new Condition(id, GsonHelper.getAsString(json, key)); } - @Override public void write(JsonObject json, Condition condition) { json.addProperty(key, condition.value); } + @Override public MapCodec codec() { return empty ? EMPTY : NOT_EMPTY; } + @Override public boolean test(IContext context, DynamicOps ops) { + return context.getTag(ItemTags.create(tag)).isEmpty() == empty; + } } } 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 8fdd8b5d..45bb6ee6 100644 --- a/src/main/java/zone/moddev/mc/basepower/recipe/RecipeDiscovery.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/RecipeDiscovery.java @@ -4,9 +4,9 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; -import net.minecraft.advancements.Advancement; +import net.minecraft.advancements.AdvancementHolder; import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeHolder; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.event.entity.player.PlayerEvent; @@ -21,11 +21,11 @@ public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { if (!(event.getEntity() instanceof ServerPlayer player)) return; // Disabled recipes have no reward to grant. Replay only already-earned // discoveries when that exact recipe becomes available on a later login. - for (Recipe recipe : player.getServer().getRecipeManager().getRecipes()) { - if (!NAMESPACES.contains(recipe.getId().getNamespace()) || recipe.isSpecial() + for (RecipeHolder recipe : player.getServer().getRecipeManager().getRecipes()) { + if (!NAMESPACES.contains(recipe.id().getNamespace()) || recipe.value().isSpecial() || player .getRecipeBook().contains(recipe)) continue; - Advancement advancement = player.getServer().getAdvancements().getAdvancement( - new ResourceLocation(recipe.getId().getNamespace(), "recipes/" + recipe.getId().getPath())); + AdvancementHolder advancement = player.getServer().getAdvancements().get( + new ResourceLocation(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 675ce26d..c71e326d 100644 --- a/src/main/java/zone/moddev/mc/basepower/registry/MachineGUIRegistry.java +++ b/src/main/java/zone/moddev/mc/basepower/registry/MachineGUIRegistry.java @@ -22,9 +22,6 @@ import net.minecraft.resources.ResourceLocation; import net.minecraftforge.registries.ForgeRegistries; import net.minecraftforge.registries.RegisterEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.network.NetworkHooks; import zone.moddev.mc.basepower.PowerAdvantage; import java.util.Map; @@ -32,7 +29,6 @@ import java.util.concurrent.atomic.AtomicInteger; /** Legacy GUI definitions backed by Forge's registered container and screen factories. */ -@Mod.EventBusSubscriber(modid = PowerAdvantage.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) public final class MachineGUIRegistry { private static final MachineGUIRegistry INSTANCE = new MachineGUIRegistry(); private static final AtomicInteger NEXT_ID = new AtomicInteger(1); @@ -51,7 +47,6 @@ public static int addGUI(ITileEntityGUI gui) { public void removeGUI(int id) { DEFINITIONS.remove(id); } - @SubscribeEvent public static void registerTypes(RegisterEvent event) { if (!ForgeRegistries.Keys.MENU_TYPES.equals(event.getRegistryKey())) return; TYPE = IForgeMenuType.create(MachineGUIRegistry::createClientContainer); @@ -89,7 +84,7 @@ public static void open(ServerPlayer player, Level world, BlockPos pos, int id) BlockEntity tile = world.getBlockEntity(pos); if (world.isClientSide || player.level() != world || !(tile instanceof Container)) return; SimpleMachineGUI gui = definition(id); - NetworkHooks.openScreen(player, new MenuProvider() { + player.openMenu(new MenuProvider() { @Override public AbstractContainerMenu createMenu(int windowId, Inventory inventory, Player owner) { return gui.createMenu(windowId, inventory, (Container) tile); } diff --git a/src/main/java/zone/moddev/mc/basepower/util/FluidContainerHelper.java b/src/main/java/zone/moddev/mc/basepower/util/FluidContainerHelper.java index 5a3e54b2..1290b705 100644 --- a/src/main/java/zone/moddev/mc/basepower/util/FluidContainerHelper.java +++ b/src/main/java/zone/moddev/mc/basepower/util/FluidContainerHelper.java @@ -1,8 +1,9 @@ package zone.moddev.mc.basepower.util; -import cyano.poweradvantage.api.fluid.FluidStack; -import cyano.poweradvantage.api.fluid.FluidTank; import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; +import java.util.Optional; +import net.minecraft.world.item.BucketItem; +import net.minecraft.world.item.ItemUtils; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.material.Fluid; import net.minecraft.world.item.Items; @@ -10,7 +11,13 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.core.Direction; import net.minecraft.world.InteractionHand; -import net.minecraft.core.Registry; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.FluidUtil; +import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction; +import net.minecraftforge.fluids.capability.IFluidHandlerItem; +import net.minecraftforge.fluids.capability.wrappers.FluidBucketWrapper; /** Uses Forge's container handling for buckets and capability-based fluid containers. */ public final class FluidContainerHelper { @@ -19,11 +26,36 @@ private FluidContainerHelper() {} public static boolean interact(Player player, InteractionHand hand, IAdvantageFluidHandler handler, Direction face) { if (player.level().isClientSide) return false; - return net.minecraftforge.fluids.FluidUtil.interactWithFluidHandler(player, hand, - new ForgeFluidAdapter(handler, face, () -> { - if (handler instanceof net.minecraft.world.level.block.entity.BlockEntity) - ((net.minecraft.world.level.block.entity.BlockEntity) handler).setChanged(); - })); + ItemStack held = player.getItemInHand(hand); + if (held.isEmpty()) return false; + IFluidHandlerItem container = itemHandler(held.copyWithCount(1)).orElse(null); + if (container == null) return false; + var machine = new ForgeFluidAdapter(handler, face, () -> { + if (handler instanceof BlockEntity tile) tile.setChanged(); + }); + // Match Forge's normal direction preference: fill the item first, then try emptying it. + if (FluidUtil.tryFluidTransfer(container, machine, Integer.MAX_VALUE, true).isEmpty() + && FluidUtil.tryFluidTransfer(machine, container, Integer.MAX_VALUE, true).isEmpty()) { + return false; + } + if (!player.hasInfiniteMaterials()) { + player.setItemInHand(hand, ItemUtils.createFilledResult(held, player, container.getContainer())); + } + return true; + } + + public static Optional getFluidContained(ItemStack stack) { + if (stack.isEmpty()) return Optional.empty(); + return itemHandler(stack.copyWithCount(1)).map(handler -> handler.drain(Integer.MAX_VALUE, FluidAction.SIMULATE)) + .filter(fluid -> !fluid.isEmpty()); + } + + private static Optional itemHandler(ItemStack stack) { + // Forge 50's FluidUtil#getFluidHandler(ItemStack) is a stub. Use its standard + // bucket adapter directly and accept explicit providers from other items. + if (stack.getItem() instanceof BucketItem) return Optional.of(new FluidBucketWrapper(stack)); + var provider = stack.getItem().getCapabilityProvider(stack); + return provider == null ? Optional.empty() : provider.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).resolve(); } public static Fluid fluidForBucket(Item bucketItem) { diff --git a/src/main/java/zone/moddev/mc/basepower/util/ItemData.java b/src/main/java/zone/moddev/mc/basepower/util/ItemData.java new file mode 100644 index 00000000..02341105 --- /dev/null +++ b/src/main/java/zone/moddev/mc/basepower/util/ItemData.java @@ -0,0 +1,54 @@ +package zone.moddev.mc.basepower.util; + +import com.mojang.serialization.Dynamic; +import net.minecraft.core.HolderGetter; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.fixes.References; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; + +/** Bridges saved machine inventories and the native item component format. */ +public final class ItemData { + public static final int LAST_PORT_DATA_VERSION = 3465; + + private ItemData() {} + + public static CompoundTag customData(ItemStack stack) { + return stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); + } + + public static ItemStack readStack(HolderLookup.Provider registries, CompoundTag saved) { + if (saved.contains("Count", 99)) { + // Vanilla does not descend into arbitrary mod block-entity inventories. + saved = withLegacyWear(saved, registries.lookupOrThrow(Registries.ITEM)); + saved = (CompoundTag) DataFixers.getDataFixer().update(References.ITEM_STACK, + new Dynamic<>(NbtOps.INSTANCE, saved), LAST_PORT_DATA_VERSION, + net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()).getValue(); + } + return ItemStack.parseOptional(registries, saved); + } + + public static CompoundTag withLegacyWear(CompoundTag saved, HolderGetter items) { + if (!saved.contains("Damage", Tag.TAG_ANY_NUMERIC) || saved.getInt("Damage") <= 0 + || saved.getCompound("tag").contains("Damage", Tag.TAG_ANY_NUMERIC)) return saved; + ResourceLocation id = ResourceLocation.tryParse(saved.getString("id")); + if (id == null || id.getNamespace().equals("minecraft") || items.get(ResourceKey.create(Registries.ITEM, id)) + .filter(holder -> holder.value().components().has(DataComponents.MAX_DAMAGE)).isEmpty()) return saved; + // The native component fixer reads wear from tag.Damage; it cannot infer + // pre-flattening metadata semantics for a modded item. + CompoundTag copy = saved.copy(); + CompoundTag tag = copy.getCompound("tag"); + tag.putInt("Damage", copy.getInt("Damage")); + copy.put("tag", tag); + return copy; + } +} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index b133ff28..051cf9b7 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -1,2 +1,2 @@ # Install legacy numeric IDs before vanilla flattens pre-1.13 block states. -public-f net.minecraft.util.datafix.fixes.BlockStateData f_14934_ +public-f net.minecraft.util.datafix.fixes.BlockStateData MAP diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index 5756dc9f..bf430c4a 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -14,14 +14,14 @@ description='''API and shared machines for pressure, electricity, quantum power, [[dependencies.basepower]] modId="basemetals" mandatory=false -versionRange="[3.1.2.120011,4.0.0)" +versionRange="[3.1.2.120061,4.0.0)" ordering="AFTER" side="BOTH" [[dependencies.basepower]] modId="orespawn" mandatory=true -versionRange="[4.1.0.120011,5.0.0)" +versionRange="[4.1.0.120061,5.0.0)" ordering="AFTER" side="BOTH" diff --git a/src/main/resources/coremods/poweradvantage_118_migration.js b/src/main/resources/coremods/poweradvantage_118_migration.js index ab66bc13..e8261837 100644 --- a/src/main/resources/coremods/poweradvantage_118_migration.js +++ b/src/main/resources/coremods/poweradvantage_118_migration.js @@ -5,11 +5,32 @@ var JumpInsnNode = Java.type('org.objectweb.asm.tree.JumpInsnNode'); var LabelNode = Java.type('org.objectweb.asm.tree.LabelNode'); var MethodInsnNode = Java.type('org.objectweb.asm.tree.MethodInsnNode'); var VarInsnNode = Java.type('org.objectweb.asm.tree.VarInsnNode'); -var ASMAPI = Java.type('net.minecraftforge.coremod.api.ASMAPI'); // Only pre-datafix migration needs bytecode integration. var hook = 'zone/moddev/mc/basepower/migration/LegacyWorldDataHook'; var metadata = 'zone/moddev/mc/basepower/migration/LegacyWorldMetadata'; +var dataFixTypes = 'net/minecraft/util/datafix/DataFixTypes'; + +function previousInstruction(instruction) { + var previous = instruction.getPrevious(); + while (previous !== null && previous.getOpcode() < 0) previous = previous.getPrevious(); + return previous; +} + +function isEmbeddedPlayerCall(call) { + // The Forge 50 reader pushes PLAYER, then the fixer, value and version. + // Inspect the receiver for this invocation, not an unrelated earlier GETSTATIC. + var argument = call; + var loads = [Opcodes.ILOAD, Opcodes.ALOAD, Opcodes.ALOAD]; + for (var index = 0; index < loads.length; ++index) { + argument = previousInstruction(argument); + if (argument === null || argument.getOpcode() !== loads[index]) return false; + } + var receiver = previousInstruction(argument); + return receiver !== null && receiver.getOpcode() === Opcodes.GETSTATIC && + receiver.owner === dataFixTypes && receiver.name === 'PLAYER' && + receiver.desc === 'L' + dataFixTypes + ';'; +} function patchPlayerDataFix(classNode) { var descriptor = '(Lcom/mojang/datafixers/DataFixer;' + @@ -22,7 +43,7 @@ function patchPlayerDataFix(classNode) { for (var instruction = method.instructions.getFirst(); instruction !== null; instruction = instruction.getNext()) { if (instruction.getOpcode() === Opcodes.INVOKEVIRTUAL && - instruction.owner === 'net/minecraft/util/datafix/DataFixTypes' && instruction.desc === descriptor) { + instruction.owner === dataFixTypes && instruction.desc === descriptor) { ++matches; matchedMethod = method; matchedCall = instruction; @@ -44,77 +65,60 @@ function initializeCoreMod() { 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.level.storage.PlayerDataStorage' }, 'transformer': patchPlayerDataFix }, - 'poweradvantage_legacy_embedded_player': { - 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.level.storage.PrimaryLevelData' }, - 'transformer': patchPlayerDataFix - }, 'poweradvantage_legacy_world_info': { 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.level.storage.LevelStorageSource' }, 'transformer': function(classNode) { - var worldReaders = 0; - var packReaders = 0; + var worldReaders = []; + var rawReturns = []; + var lightweightReturns = []; + var embeddedSites = []; + var dynamic = 'Lcom/mojang/serialization/Dynamic;'; + var nbt = 'Lnet/minecraft/nbt/CompoundTag;'; + var embedded = '(Lcom/mojang/datafixers/DataFixer;' + dynamic + 'I)' + dynamic; for (var i = 0; i < classNode.methods.size(); ++i) { var method = classNode.methods.get(i); - var worldReader = method.desc === '(Lcom/mojang/serialization/DynamicOps;' + - 'Lnet/minecraft/world/level/WorldDataConfiguration;Lnet/minecraft/core/Registry;' + - 'Lcom/mojang/serialization/Lifecycle;' + - 'Ljava/nio/file/Path;Lcom/mojang/datafixers/DataFixer;)' + - 'Lcom/mojang/datafixers/util/Pair;'; - var packReader = method.desc === '(Ljava/nio/file/Path;Lcom/mojang/datafixers/DataFixer;)' + - 'Lnet/minecraft/world/level/WorldDataConfiguration;'; - if (!worldReader && !packReader) continue; - if (worldReader) { - var prefix = new InsnList(); - prefix.add(new VarInsnNode(Opcodes.ALOAD, 4)); - prefix.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, - 'java/nio/file/Path', 'toFile', '()Ljava/io/File;', true)); - prefix.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, - 'prepareLegacyWorld', '(Ljava/io/File;)V', false)); - method.instructions.insert(prefix); - ++worldReaders; - continue; + if (method.name === 'readLevelDataTagFixed' && + method.desc === '(Ljava/nio/file/Path;Lcom/mojang/datafixers/DataFixer;)' + dynamic) { + worldReaders.push(method); } - var reads = 0; - for (var read = method.instructions.getFirst(); read !== null; read = read.getNext()) { - if (read.getOpcode() === Opcodes.INVOKESTATIC && read.owner === classNode.name && - read.desc === '(Ljava/nio/file/Path;)Lnet/minecraft/nbt/Tag;') { - method.instructions.insert(read, new MethodInsnNode(Opcodes.INVOKESTATIC, - metadata, 'remapLightweight', '(Lnet/minecraft/nbt/Tag;)Lnet/minecraft/nbt/Tag;', false)); - ++reads; + var calls = []; + var playerCall = null; + for (var insn = method.instructions.getFirst(); insn !== null; insn = insn.getNext()) { + if (insn.getOpcode() === Opcodes.ARETURN) { + if (method.name === 'readLevelDataTagRaw' && method.desc === '(Ljava/nio/file/Path;)' + nbt) { + rawReturns.push({ method: method, instruction: insn }); + } else if (method.name === 'readLightweightData' && + method.desc === '(Ljava/nio/file/Path;)Lnet/minecraft/nbt/Tag;') { + lightweightReturns.push({ method: method, instruction: insn }); + } } - } - if (reads !== 1) throw new Error('Power Advantage expected one lightweight metadata read'); - ++packReaders; - } - if (worldReaders !== 1 || packReaders !== 1) { - throw new Error('Power Advantage could not attach both legacy level readers'); - } - return classNode; - } - }, - 'poweradvantage_forge_save_metadata': { - 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.level.storage.LevelStorageSource$LevelStorageAccess' }, - 'transformer': function(classNode) { - var matches = 0; - for (var i = 0; i < classNode.methods.size(); ++i) { - var method = classNode.methods.get(i); - var read = null; - for (var call = method.instructions.getFirst(); call !== null; call = call.getNext()) { - if (call.getOpcode() !== Opcodes.INVOKESTATIC) continue; - if (call.owner === 'net/minecraft/nbt/NbtIo' && - call.desc === '(Ljava/io/File;)Lnet/minecraft/nbt/CompoundTag;') read = call; - if (call.owner === 'net/minecraftforge/common/ForgeHooks' && - call.name === 'readAdditionalLevelSaveData' && - call.desc === '(Lnet/minecraft/nbt/CompoundTag;' + - 'Lnet/minecraft/world/level/storage/LevelStorageSource$LevelDirectory;)V') { - if (read === null) throw new Error('Power Advantage expected Forge to read level.dat'); - method.instructions.set(read, new MethodInsnNode(Opcodes.INVOKESTATIC, - metadata, 'read', read.desc, false)); - ++matches; + if (insn.getOpcode() === Opcodes.INVOKEVIRTUAL && insn.owner === dataFixTypes && + insn.desc === embedded) { + calls.push(insn); + if (isEmbeddedPlayerCall(insn)) playerCall = insn; } } + if (playerCall !== null) { + if (calls.length !== 1) throw new Error('Power Advantage found an ambiguous embedded player reader'); + embeddedSites.push({ method: method, instruction: playerCall }); + } } - if (matches !== 1) throw new Error('Power Advantage could not attach the Forge save metadata reader'); + if (worldReaders.length !== 1 || rawReturns.length !== 1 || + lightweightReturns.length !== 1 || embeddedSites.length !== 1) { + throw new Error('Power Advantage could not attach its Forge 50 level and embedded player readers'); + } + var prefix = new InsnList(); + prefix.add(new VarInsnNode(Opcodes.ALOAD, 0)); + prefix.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, 'java/nio/file/Path', 'toFile', '()Ljava/io/File;', true)); + prefix.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, 'prepareLegacyWorld', '(Ljava/io/File;)V', false)); + worldReaders[0].instructions.insert(prefix); + rawReturns[0].method.instructions.insertBefore(rawReturns[0].instruction, new MethodInsnNode( + Opcodes.INVOKESTATIC, metadata, 'remapRaw', '(' + nbt + ')' + nbt, false)); + lightweightReturns[0].method.instructions.insertBefore(lightweightReturns[0].instruction, new MethodInsnNode( + Opcodes.INVOKESTATIC, metadata, 'remapLightweight', '(Lnet/minecraft/nbt/Tag;)Lnet/minecraft/nbt/Tag;', false)); + embeddedSites[0].method.instructions.set(embeddedSites[0].instruction, new MethodInsnNode( + Opcodes.INVOKESTATIC, 'zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes', + 'updateNativeEmbeddedPlayer', '(L' + dataFixTypes + ';' + embedded.substring(1), false)); return classNode; } }, @@ -181,23 +185,16 @@ function initializeCoreMod() { 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.level.chunk.storage.EntityStorage' }, 'transformer': function(classNode) { var matches = 0; - var descriptor = '(Lnet/minecraft/nbt/CompoundTag;)Lnet/minecraft/nbt/CompoundTag;'; + var storage = 'net/minecraft/world/level/chunk/storage/SimpleRegionStorage'; + var descriptor = '(Lnet/minecraft/nbt/CompoundTag;I)Lnet/minecraft/nbt/CompoundTag;'; for (var i = 0; i < classNode.methods.size(); ++i) { var method = classNode.methods.get(i); - if (method.desc !== descriptor || (method.access & Opcodes.ACC_STATIC) !== 0) continue; - ++matches; - var prefix = new InsnList(); - prefix.add(new VarInsnNode(Opcodes.ALOAD, 1)); - prefix.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, - 'prepareEntityChunkForMigration', descriptor, false)); - prefix.add(new VarInsnNode(Opcodes.ASTORE, 1)); - method.instructions.insert(prefix); - for (var instruction = method.instructions.getFirst(); instruction !== null; - instruction = instruction.getNext()) { - if (instruction.getOpcode() === Opcodes.ARETURN) { - method.instructions.insertBefore(instruction, new MethodInsnNode(Opcodes.INVOKESTATIC, - hook, 'finalizeLegacyChunk', descriptor, false)); - } + for (var call = method.instructions.getFirst(); call !== null; call = call.getNext()) { + if (call.getOpcode() !== Opcodes.INVOKEVIRTUAL || call.owner !== storage || + call.name !== 'upgradeChunkTag' || call.desc !== descriptor) continue; + method.instructions.set(call, new MethodInsnNode(Opcodes.INVOKESTATIC, hook, + 'upgradeEntityChunk', '(L' + storage + ';' + descriptor.substring(1), false)); + ++matches; } } if (matches !== 1) throw new Error('Power Advantage requires exactly one native entity-chunk converter'); @@ -211,7 +208,7 @@ function initializeCoreMod() { var patched = false; for (var i = 0; i < classNode.methods.size(); ++i) { var method = classNode.methods.get(i); - if (method.desc !== descriptor || method.name !== ASMAPI.mapMethod('m_180807_')) continue; + if (method.desc !== descriptor || method.name !== 'ensureCanWrite') continue; var allowed = new LabelNode(); var prefix = new InsnList(); prefix.add(new VarInsnNode(Opcodes.ALOAD, 0)); diff --git a/src/main/resources/data/basepower/advancements/recipes/converter_rf_electricity.json b/src/main/resources/data/basepower/advancements/recipes/converter_rf_electricity.json index 29e395bc..928d9048 100644 --- a/src/main/resources/data/basepower/advancements/recipes/converter_rf_electricity.json +++ b/src/main/resources/data/basepower/advancements/recipes/converter_rf_electricity.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:power_supply_units" + "items": "#forge:power_supply_units" } ] } @@ -16,7 +16,7 @@ "conditions": { "items": [ { - "tag": "forge:frames/steel" + "items": "#forge:frames/steel" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/converter_rf_quantum.json b/src/main/resources/data/basepower/advancements/recipes/converter_rf_quantum.json index 09df8231..bbe4ff1b 100644 --- a/src/main/resources/data/basepower/advancements/recipes/converter_rf_quantum.json +++ b/src/main/resources/data/basepower/advancements/recipes/converter_rf_quantum.json @@ -18,7 +18,7 @@ "conditions": { "items": [ { - "tag": "forge:frames/steel" + "items": "#forge:frames/steel" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/converter_rf_steam.json b/src/main/resources/data/basepower/advancements/recipes/converter_rf_steam.json index 7538dfad..09cf833a 100644 --- a/src/main/resources/data/basepower/advancements/recipes/converter_rf_steam.json +++ b/src/main/resources/data/basepower/advancements/recipes/converter_rf_steam.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:governors" + "items": "#forge:governors" } ] } @@ -16,7 +16,7 @@ "conditions": { "items": [ { - "tag": "forge:frames/steel" + "items": "#forge:frames/steel" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_iron.json b/src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_iron.json index cc616be6..d547155c 100644 --- a/src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_iron.json +++ b/src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_iron.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/iron" + "items": "#forge:ingots/iron" } ] } @@ -16,7 +16,7 @@ "conditions": { "items": [ { - "tag": "forge:power_supply_units" + "items": "#forge:power_supply_units" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_refined_iron.json b/src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_refined_iron.json index 9ae4e5f7..77eda806 100644 --- a/src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_refined_iron.json +++ b/src/main/resources/data/basepower/advancements/recipes/converter_tr_electricity_refined_iron.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/refined_iron" + "items": "#forge:ingots/refined_iron" } ] } @@ -16,7 +16,7 @@ "conditions": { "items": [ { - "tag": "forge:power_supply_units" + "items": "#forge:power_supply_units" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_discharge.json b/src/main/resources/data/basepower/advancements/recipes/fluid_discharge.json index 6eee6ff0..bc16e154 100644 --- a/src/main/resources/data/basepower/advancements/recipes/fluid_discharge.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_discharge.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:bars" + "items": "#forge:bars" } ] } @@ -16,7 +16,7 @@ "conditions": { "items": [ { - "tag": "forge:plates/steel" + "items": "#forge:plates/steel" } ] } @@ -26,7 +26,7 @@ "conditions": { "items": [ { - "tag": "forge:frames/steel" + "items": "#forge:frames/steel" } ] } @@ -36,7 +36,7 @@ "conditions": { "items": [ { - "tag": "forge:pipes" + "items": "#forge:pipes" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_drain.json b/src/main/resources/data/basepower/advancements/recipes/fluid_drain.json index c79372f5..8f78a994 100644 --- a/src/main/resources/data/basepower/advancements/recipes/fluid_drain.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_drain.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:bars" + "items": "#forge:bars" } ] } @@ -16,7 +16,7 @@ "conditions": { "items": [ { - "tag": "forge:plates/steel" + "items": "#forge:plates/steel" } ] } @@ -26,7 +26,7 @@ "conditions": { "items": [ { - "tag": "forge:frames/steel" + "items": "#forge:frames/steel" } ] } @@ -36,7 +36,7 @@ "conditions": { "items": [ { - "tag": "forge:pipes" + "items": "#forge:pipes" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_iron_normal.json b/src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_iron_normal.json index c25d54ec..4287d659 100644 --- a/src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_iron_normal.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_iron_normal.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/iron" + "items": "#forge:ingots/iron" } ] } @@ -16,7 +16,7 @@ "conditions": { "items": [ { - "tag": "forge:pipes" + "items": "#forge:pipes" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_steel.json b/src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_steel.json index 98f2b0e3..96c83878 100644 --- a/src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_steel.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_metal_tank_steel.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/steel" + "items": "#forge:ingots/steel" } ] } @@ -16,7 +16,7 @@ "conditions": { "items": [ { - "tag": "forge:pipes" + "items": "#forge:pipes" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_apocalyptic.json b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_apocalyptic.json index 9ae581b8..299d2a2e 100644 --- a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_apocalyptic.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_apocalyptic.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/copper" + "items": "#forge:ingots/copper" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_normal.json b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_normal.json index 508016f9..2f56e9f8 100644 --- a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_normal.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_copper_normal.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/copper" + "items": "#forge:ingots/copper" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_apocalyptic.json b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_apocalyptic.json index 902e5684..7dfcae60 100644 --- a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_apocalyptic.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_apocalyptic.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/iron" + "items": "#forge:ingots/iron" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_normal.json b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_normal.json index 69dfe6cb..3ebe5a3c 100644 --- a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_normal.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_normal.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/iron" + "items": "#forge:ingots/iron" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_tech.json b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_tech.json index d9a093dc..05114108 100644 --- a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_tech.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_iron_tech.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/iron" + "items": "#forge:ingots/iron" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_lead_apocalyptic.json b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_lead_apocalyptic.json index 9ebf5a61..3eb6bf99 100644 --- a/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_lead_apocalyptic.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_pipe_lead_apocalyptic.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/lead" + "items": "#forge:ingots/lead" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_storage_tank.json b/src/main/resources/data/basepower/advancements/recipes/fluid_storage_tank.json index 93c5e276..e2fdc4de 100644 --- a/src/main/resources/data/basepower/advancements/recipes/fluid_storage_tank.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_storage_tank.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/plastic" + "items": "#forge:ingots/plastic" } ] } @@ -16,7 +16,7 @@ "conditions": { "items": [ { - "tag": "forge:pipes" + "items": "#forge:pipes" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/fluid_switch.json b/src/main/resources/data/basepower/advancements/recipes/fluid_switch.json index ab63dfb3..356ebc65 100644 --- a/src/main/resources/data/basepower/advancements/recipes/fluid_switch.json +++ b/src/main/resources/data/basepower/advancements/recipes/fluid_switch.json @@ -18,7 +18,7 @@ "conditions": { "items": [ { - "tag": "forge:pipes" + "items": "#forge:pipes" } ] } @@ -28,7 +28,7 @@ "conditions": { "items": [ { - "tag": "forge:frames/steel" + "items": "#forge:frames/steel" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/item_conveyor.json b/src/main/resources/data/basepower/advancements/recipes/item_conveyor.json index dc3f1487..67c696f5 100644 --- a/src/main/resources/data/basepower/advancements/recipes/item_conveyor.json +++ b/src/main/resources/data/basepower/advancements/recipes/item_conveyor.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:plates/steel" + "items": "#forge:plates/steel" } ] } @@ -16,7 +16,7 @@ "conditions": { "items": [ { - "tag": "forge:sprockets" + "items": "#forge:sprockets" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/item_filter_ore.json b/src/main/resources/data/basepower/advancements/recipes/item_filter_ore.json index 20d8379e..baf0362b 100644 --- a/src/main/resources/data/basepower/advancements/recipes/item_filter_ore.json +++ b/src/main/resources/data/basepower/advancements/recipes/item_filter_ore.json @@ -18,7 +18,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/gold" + "items": "#forge:ingots/gold" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/item_filter_plant.json b/src/main/resources/data/basepower/advancements/recipes/item_filter_plant.json index 061a6d22..7b5123b4 100644 --- a/src/main/resources/data/basepower/advancements/recipes/item_filter_plant.json +++ b/src/main/resources/data/basepower/advancements/recipes/item_filter_plant.json @@ -18,7 +18,7 @@ "conditions": { "items": [ { - "tag": "minecraft:saplings" + "items": "#minecraft:saplings" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/rotator_tool.json b/src/main/resources/data/basepower/advancements/recipes/rotator_tool.json index eed458d8..8a43e0b5 100644 --- a/src/main/resources/data/basepower/advancements/recipes/rotator_tool.json +++ b/src/main/resources/data/basepower/advancements/recipes/rotator_tool.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/iron" + "items": "#forge:ingots/iron" } ] } @@ -16,7 +16,7 @@ "conditions": { "items": [ { - "tag": "forge:sprockets" + "items": "#forge:sprockets" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json b/src/main/resources/data/basepower/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json index 2591d5ee..c34e4867 100644 --- a/src/main/resources/data/basepower/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json +++ b/src/main/resources/data/basepower/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/steel" + "items": "#forge:ingots/steel" } ] } @@ -16,7 +16,7 @@ "conditions": { "items": [ { - "tag": "forge:nuggets/bronze" + "items": "#forge:nuggets/bronze" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/sprocket_rod_normal.json b/src/main/resources/data/basepower/advancements/recipes/sprocket_rod_normal.json index ea3a3bcc..caae00f0 100644 --- a/src/main/resources/data/basepower/advancements/recipes/sprocket_rod_normal.json +++ b/src/main/resources/data/basepower/advancements/recipes/sprocket_rod_normal.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/steel" + "items": "#forge:ingots/steel" } ] } @@ -16,7 +16,7 @@ "conditions": { "items": [ { - "tag": "forge:rods" + "items": "#forge:rods" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/sprocket_rod_tech.json b/src/main/resources/data/basepower/advancements/recipes/sprocket_rod_tech.json index 4a6a2719..47ea8711 100644 --- a/src/main/resources/data/basepower/advancements/recipes/sprocket_rod_tech.json +++ b/src/main/resources/data/basepower/advancements/recipes/sprocket_rod_tech.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/steel" + "items": "#forge:ingots/steel" } ] } @@ -16,7 +16,7 @@ "conditions": { "items": [ { - "tag": "forge:rods" + "items": "#forge:rods" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/sprocket_stickwood_normal.json b/src/main/resources/data/basepower/advancements/recipes/sprocket_stickwood_normal.json index 203b43ad..3dd37bff 100644 --- a/src/main/resources/data/basepower/advancements/recipes/sprocket_stickwood_normal.json +++ b/src/main/resources/data/basepower/advancements/recipes/sprocket_stickwood_normal.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:ingots/steel" + "items": "#forge:ingots/steel" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/steel_frame.json b/src/main/resources/data/basepower/advancements/recipes/steel_frame.json index f0268ab3..c14410df 100644 --- a/src/main/resources/data/basepower/advancements/recipes/steel_frame.json +++ b/src/main/resources/data/basepower/advancements/recipes/steel_frame.json @@ -6,7 +6,7 @@ "conditions": { "items": [ { - "tag": "forge:bars/steel" + "items": "#forge:bars/steel" } ] } diff --git a/src/main/resources/data/basepower/advancements/recipes/still.json b/src/main/resources/data/basepower/advancements/recipes/still.json index 9b2de58d..c34476be 100644 --- a/src/main/resources/data/basepower/advancements/recipes/still.json +++ b/src/main/resources/data/basepower/advancements/recipes/still.json @@ -18,7 +18,7 @@ "conditions": { "items": [ { - "tag": "forge:pipes" + "items": "#forge:pipes" } ] } diff --git a/src/main/resources/data/basepower/loot_tables/blocks/fluid_storage_tank.json b/src/main/resources/data/basepower/loot_tables/blocks/fluid_storage_tank.json index c77ad259..3e34612e 100644 --- a/src/main/resources/data/basepower/loot_tables/blocks/fluid_storage_tank.json +++ b/src/main/resources/data/basepower/loot_tables/blocks/fluid_storage_tank.json @@ -9,7 +9,7 @@ "name": "basepower:fluid_storage_tank", "functions": [ { - "function": "minecraft:copy_nbt", + "function": "minecraft:copy_custom_data", "source": "block_entity", "ops": [ { diff --git a/src/main/resources/data/basepower/recipes/bioplastic_from_starch.json b/src/main/resources/data/basepower/recipes/bioplastic_from_starch.json index d0d38d7b..8eb5c06f 100644 --- a/src/main/resources/data/basepower/recipes/bioplastic_from_starch.json +++ b/src/main/resources/data/basepower/recipes/bioplastic_from_starch.json @@ -3,9 +3,10 @@ "ingredient": { "item": "basepower:starch" }, - "result": "basepower:bioplastic_ingot", + "result": { + "id": "basepower:bioplastic_ingot" + }, "experience": 0.1, "cookingtime": 200, - "conditions": [], "group": "basepower:bioplastic_ingot" } diff --git a/src/main/resources/data/basepower/recipes/converter_rf_electricity.json b/src/main/resources/data/basepower/recipes/converter_rf_electricity.json index 2684a731..f6842b6b 100644 --- a/src/main/resources/data/basepower/recipes/converter_rf_electricity.json +++ b/src/main/resources/data/basepower/recipes/converter_rf_electricity.json @@ -15,17 +15,20 @@ } }, "result": { - "item": "basepower:converter_rf_electricity" + "id": "basepower:converter_rf_electricity" }, - "conditions": [ - { - "type": "basepower:tag_not_empty", - "tag": "forge:power_supply_units" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:frames/steel" - } - ], - "group": "basepower:converter_rf_electricity" + "group": "basepower:converter_rf_electricity", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:tag_not_empty", + "tag": "forge:power_supply_units" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:frames/steel" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/converter_rf_quantum.json b/src/main/resources/data/basepower/recipes/converter_rf_quantum.json index 42803316..3d5d6d9b 100644 --- a/src/main/resources/data/basepower/recipes/converter_rf_quantum.json +++ b/src/main/resources/data/basepower/recipes/converter_rf_quantum.json @@ -15,13 +15,11 @@ } }, "result": { - "item": "basepower:converter_rf_quantum" + "id": "basepower:converter_rf_quantum" }, - "conditions": [ - { - "type": "basepower:tag_not_empty", - "tag": "forge:frames/steel" - } - ], - "group": "basepower:converter_rf_quantum" + "group": "basepower:converter_rf_quantum", + "forge:condition": { + "type": "basepower:tag_not_empty", + "tag": "forge:frames/steel" + } } diff --git a/src/main/resources/data/basepower/recipes/converter_rf_steam.json b/src/main/resources/data/basepower/recipes/converter_rf_steam.json index af33bdfc..46b3a00b 100644 --- a/src/main/resources/data/basepower/recipes/converter_rf_steam.json +++ b/src/main/resources/data/basepower/recipes/converter_rf_steam.json @@ -15,17 +15,20 @@ } }, "result": { - "item": "basepower:converter_rf_steam" + "id": "basepower:converter_rf_steam" }, - "conditions": [ - { - "type": "basepower:tag_not_empty", - "tag": "forge:governors" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:frames/steel" - } - ], - "group": "basepower:converter_rf_steam" + "group": "basepower:converter_rf_steam", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:tag_not_empty", + "tag": "forge:governors" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:frames/steel" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/converter_tr_electricity_iron.json b/src/main/resources/data/basepower/recipes/converter_tr_electricity_iron.json index 1e4061fc..3d674798 100644 --- a/src/main/resources/data/basepower/recipes/converter_tr_electricity_iron.json +++ b/src/main/resources/data/basepower/recipes/converter_tr_electricity_iron.json @@ -14,21 +14,24 @@ } }, "result": { - "item": "basepower:converter_tr_electricity" + "id": "basepower:converter_tr_electricity" }, - "conditions": [ - { - "type": "basepower:tag_empty", - "tag": "forge:ingots/refined_iron" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/iron" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:power_supply_units" - } - ], - "group": "basepower:converter_tr_electricity" + "group": "basepower:converter_tr_electricity", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:tag_empty", + "tag": "forge:ingots/refined_iron" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/iron" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:power_supply_units" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/converter_tr_electricity_refined_iron.json b/src/main/resources/data/basepower/recipes/converter_tr_electricity_refined_iron.json index 478f6afa..2842bff1 100644 --- a/src/main/resources/data/basepower/recipes/converter_tr_electricity_refined_iron.json +++ b/src/main/resources/data/basepower/recipes/converter_tr_electricity_refined_iron.json @@ -14,17 +14,20 @@ } }, "result": { - "item": "basepower:converter_tr_electricity" + "id": "basepower:converter_tr_electricity" }, - "conditions": [ - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/refined_iron" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:power_supply_units" - } - ], - "group": "basepower:converter_tr_electricity" + "group": "basepower:converter_tr_electricity", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/refined_iron" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:power_supply_units" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/crushing_fluid_discharge.json b/src/main/resources/data/basepower/recipes/crushing_fluid_discharge.json index 287cfbfd..d67b2602 100644 --- a/src/main/resources/data/basepower/recipes/crushing_fluid_discharge.json +++ b/src/main/resources/data/basepower/recipes/crushing_fluid_discharge.json @@ -4,17 +4,20 @@ "item": "basepower:fluid_discharge" }, "result": { - "item": "basepower:fluid_pipe", + "id": "basepower:fluid_pipe", "count": 2 }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/crushing_fluid_drain.json b/src/main/resources/data/basepower/recipes/crushing_fluid_drain.json index 69d8e470..5cb809dd 100644 --- a/src/main/resources/data/basepower/recipes/crushing_fluid_drain.json +++ b/src/main/resources/data/basepower/recipes/crushing_fluid_drain.json @@ -4,17 +4,20 @@ "item": "basepower:fluid_drain" }, "result": { - "item": "basepower:fluid_pipe", + "id": "basepower:fluid_pipe", "count": 2 }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/crushing_fluid_storage_tank.json b/src/main/resources/data/basepower/recipes/crushing_fluid_storage_tank.json index a6d41a9e..300411bb 100644 --- a/src/main/resources/data/basepower/recipes/crushing_fluid_storage_tank.json +++ b/src/main/resources/data/basepower/recipes/crushing_fluid_storage_tank.json @@ -4,17 +4,20 @@ "item": "basepower:fluid_storage_tank" }, "result": { - "item": "basepower:fluid_pipe", + "id": "basepower:fluid_pipe", "count": 1 }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/crushing_item_conveyor.json b/src/main/resources/data/basepower/recipes/crushing_item_conveyor.json index f20d15eb..38087ed5 100644 --- a/src/main/resources/data/basepower/recipes/crushing_item_conveyor.json +++ b/src/main/resources/data/basepower/recipes/crushing_item_conveyor.json @@ -4,21 +4,24 @@ "item": "basepower:item_conveyor" }, "result": { - "item": "basemetals:steel_plate", + "id": "basemetals:steel_plate", "count": 1 }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:plates/steel" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_block.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_block.json index d3429ad9..9c786824 100644 --- a/src/main/resources/data/basepower/recipes/crushing_item_filter_block.json +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_block.json @@ -4,21 +4,24 @@ "item": "basepower:item_filter_block" }, "result": { - "item": "basemetals:steel_plate", + "id": "basemetals:steel_plate", "count": 1 }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:plates/steel" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_food.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_food.json index 05ccc8e4..5388a4ab 100644 --- a/src/main/resources/data/basepower/recipes/crushing_item_filter_food.json +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_food.json @@ -4,21 +4,24 @@ "item": "basepower:item_filter_food" }, "result": { - "item": "basemetals:steel_plate", + "id": "basemetals:steel_plate", "count": 1 }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:plates/steel" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_fuel.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_fuel.json index 44f31b31..261ea312 100644 --- a/src/main/resources/data/basepower/recipes/crushing_item_filter_fuel.json +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_fuel.json @@ -4,21 +4,24 @@ "item": "basepower:item_filter_fuel" }, "result": { - "item": "basemetals:steel_plate", + "id": "basemetals:steel_plate", "count": 1 }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:plates/steel" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_inventory.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_inventory.json index 807e993d..6e7d8920 100644 --- a/src/main/resources/data/basepower/recipes/crushing_item_filter_inventory.json +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_inventory.json @@ -4,21 +4,24 @@ "item": "basepower:item_filter_inventory" }, "result": { - "item": "basemetals:steel_plate", + "id": "basemetals:steel_plate", "count": 1 }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:plates/steel" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_ore.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_ore.json index e6163d6b..9f29239f 100644 --- a/src/main/resources/data/basepower/recipes/crushing_item_filter_ore.json +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_ore.json @@ -4,21 +4,24 @@ "item": "basepower:item_filter_ore" }, "result": { - "item": "basemetals:steel_plate", + "id": "basemetals:steel_plate", "count": 1 }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:plates/steel" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_overflow.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_overflow.json index bebb9dbf..5a78fe08 100644 --- a/src/main/resources/data/basepower/recipes/crushing_item_filter_overflow.json +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_overflow.json @@ -4,21 +4,24 @@ "item": "basepower:item_filter_overflow" }, "result": { - "item": "basemetals:steel_plate", + "id": "basemetals:steel_plate", "count": 1 }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:plates/steel" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_plant.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_plant.json index 3477628d..6d1c44af 100644 --- a/src/main/resources/data/basepower/recipes/crushing_item_filter_plant.json +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_plant.json @@ -4,21 +4,24 @@ "item": "basepower:item_filter_plant" }, "result": { - "item": "basemetals:steel_plate", + "id": "basemetals:steel_plate", "count": 1 }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:plates/steel" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/crushing_item_filter_smelt.json b/src/main/resources/data/basepower/recipes/crushing_item_filter_smelt.json index 4b460e80..186812e2 100644 --- a/src/main/resources/data/basepower/recipes/crushing_item_filter_smelt.json +++ b/src/main/resources/data/basepower/recipes/crushing_item_filter_smelt.json @@ -4,21 +4,24 @@ "item": "basepower:item_filter_smelt" }, "result": { - "item": "basemetals:steel_plate", + "id": "basemetals:steel_plate", "count": 1 }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - }, - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:plates/steel" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "forge:mod_loaded", + "modid": "basemetals" + }, + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/crushing_potato_starch.json b/src/main/resources/data/basepower/recipes/crushing_potato_starch.json index 6e8df4fa..c512154d 100644 --- a/src/main/resources/data/basepower/recipes/crushing_potato_starch.json +++ b/src/main/resources/data/basepower/recipes/crushing_potato_starch.json @@ -9,13 +9,11 @@ } ], "result": { - "item": "basepower:starch", + "id": "basepower:starch", "count": 1 }, - "conditions": [ - { - "type": "forge:mod_loaded", - "modid": "basemetals" - } - ] + "forge:condition": { + "type": "forge:mod_loaded", + "modid": "basemetals" + } } diff --git a/src/main/resources/data/basepower/recipes/fluid_discharge.json b/src/main/resources/data/basepower/recipes/fluid_discharge.json index 6a49f66d..f11ceb8b 100644 --- a/src/main/resources/data/basepower/recipes/fluid_discharge.json +++ b/src/main/resources/data/basepower/recipes/fluid_discharge.json @@ -20,25 +20,28 @@ } }, "result": { - "item": "basepower:fluid_discharge" + "id": "basepower:fluid_discharge" }, "group": "basepower:fluid_discharge", - "conditions": [ - { - "type": "basepower:tag_not_empty", - "tag": "forge:bars" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:frames/steel" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:pipes" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:plates/steel" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:tag_not_empty", + "tag": "forge:bars" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:frames/steel" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:pipes" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/fluid_drain.json b/src/main/resources/data/basepower/recipes/fluid_drain.json index fd33b2c8..1e88fc2f 100644 --- a/src/main/resources/data/basepower/recipes/fluid_drain.json +++ b/src/main/resources/data/basepower/recipes/fluid_drain.json @@ -20,25 +20,28 @@ } }, "result": { - "item": "basepower:fluid_drain" + "id": "basepower:fluid_drain" }, "group": "basepower:fluid_drain", - "conditions": [ - { - "type": "basepower:tag_not_empty", - "tag": "forge:bars" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:frames/steel" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:pipes" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:plates/steel" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:tag_not_empty", + "tag": "forge:bars" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:frames/steel" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:pipes" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/fluid_metal_tank_iron_normal.json b/src/main/resources/data/basepower/recipes/fluid_metal_tank_iron_normal.json index 9c0fdc33..c542ca19 100644 --- a/src/main/resources/data/basepower/recipes/fluid_metal_tank_iron_normal.json +++ b/src/main/resources/data/basepower/recipes/fluid_metal_tank_iron_normal.json @@ -14,21 +14,24 @@ } }, "result": { - "item": "basepower:fluid_metal_tank" + "id": "basepower:fluid_metal_tank" }, - "conditions": [ - { - "type": "basepower:recipe_mode", - "mode": "NORMAL" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/iron" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:pipes" - } - ], - "group": "basepower:fluid_metal_tank" + "group": "basepower:fluid_metal_tank", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:recipe_mode", + "mode": "NORMAL" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/iron" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:pipes" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/fluid_metal_tank_steel.json b/src/main/resources/data/basepower/recipes/fluid_metal_tank_steel.json index 37733d05..3bf46ef3 100644 --- a/src/main/resources/data/basepower/recipes/fluid_metal_tank_steel.json +++ b/src/main/resources/data/basepower/recipes/fluid_metal_tank_steel.json @@ -14,17 +14,20 @@ } }, "result": { - "item": "basepower:fluid_metal_tank" + "id": "basepower:fluid_metal_tank" }, "group": "basepower:fluid_metal_tank", - "conditions": [ - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/steel" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:pipes" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/steel" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:pipes" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/fluid_pipe_copper_apocalyptic.json b/src/main/resources/data/basepower/recipes/fluid_pipe_copper_apocalyptic.json index 50fbd0c6..4c3063d2 100644 --- a/src/main/resources/data/basepower/recipes/fluid_pipe_copper_apocalyptic.json +++ b/src/main/resources/data/basepower/recipes/fluid_pipe_copper_apocalyptic.json @@ -11,18 +11,21 @@ } }, "result": { - "item": "basepower:fluid_pipe", + "id": "basepower:fluid_pipe", "count": 3 }, - "conditions": [ - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/copper" - } - ], - "group": "basepower:fluid_pipe" + "group": "basepower:fluid_pipe", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/copper" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/fluid_pipe_copper_normal.json b/src/main/resources/data/basepower/recipes/fluid_pipe_copper_normal.json index e94d98eb..5a01c2f0 100644 --- a/src/main/resources/data/basepower/recipes/fluid_pipe_copper_normal.json +++ b/src/main/resources/data/basepower/recipes/fluid_pipe_copper_normal.json @@ -11,18 +11,21 @@ } }, "result": { - "item": "basepower:fluid_pipe", + "id": "basepower:fluid_pipe", "count": 6 }, - "conditions": [ - { - "type": "basepower:recipe_mode", - "mode": "NORMAL" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/copper" - } - ], - "group": "basepower:fluid_pipe" + "group": "basepower:fluid_pipe", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:recipe_mode", + "mode": "NORMAL" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/copper" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/fluid_pipe_iron_apocalyptic.json b/src/main/resources/data/basepower/recipes/fluid_pipe_iron_apocalyptic.json index 36d5c6b9..41c45631 100644 --- a/src/main/resources/data/basepower/recipes/fluid_pipe_iron_apocalyptic.json +++ b/src/main/resources/data/basepower/recipes/fluid_pipe_iron_apocalyptic.json @@ -11,18 +11,21 @@ } }, "result": { - "item": "basepower:fluid_pipe", + "id": "basepower:fluid_pipe", "count": 3 }, - "conditions": [ - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/iron" - } - ], - "group": "basepower:fluid_pipe" + "group": "basepower:fluid_pipe", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/iron" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/fluid_pipe_iron_normal.json b/src/main/resources/data/basepower/recipes/fluid_pipe_iron_normal.json index 86a88671..74d3213f 100644 --- a/src/main/resources/data/basepower/recipes/fluid_pipe_iron_normal.json +++ b/src/main/resources/data/basepower/recipes/fluid_pipe_iron_normal.json @@ -11,18 +11,21 @@ } }, "result": { - "item": "basepower:fluid_pipe", + "id": "basepower:fluid_pipe", "count": 6 }, - "conditions": [ - { - "type": "basepower:recipe_mode", - "mode": "NORMAL" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/iron" - } - ], - "group": "basepower:fluid_pipe" + "group": "basepower:fluid_pipe", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:recipe_mode", + "mode": "NORMAL" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/iron" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/fluid_pipe_iron_tech.json b/src/main/resources/data/basepower/recipes/fluid_pipe_iron_tech.json index 8d6d6ce8..fa0bc1c4 100644 --- a/src/main/resources/data/basepower/recipes/fluid_pipe_iron_tech.json +++ b/src/main/resources/data/basepower/recipes/fluid_pipe_iron_tech.json @@ -11,18 +11,21 @@ } }, "result": { - "item": "basepower:fluid_pipe", + "id": "basepower:fluid_pipe", "count": 6 }, - "conditions": [ - { - "type": "basepower:recipe_mode", - "mode": "TECH_PROGRESSION" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/iron" - } - ], - "group": "basepower:fluid_pipe" + "group": "basepower:fluid_pipe", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:recipe_mode", + "mode": "TECH_PROGRESSION" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/iron" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/fluid_pipe_lead_apocalyptic.json b/src/main/resources/data/basepower/recipes/fluid_pipe_lead_apocalyptic.json index b6a655a8..42751b1d 100644 --- a/src/main/resources/data/basepower/recipes/fluid_pipe_lead_apocalyptic.json +++ b/src/main/resources/data/basepower/recipes/fluid_pipe_lead_apocalyptic.json @@ -11,18 +11,21 @@ } }, "result": { - "item": "basepower:fluid_pipe", + "id": "basepower:fluid_pipe", "count": 3 }, - "conditions": [ - { - "type": "basepower:recipe_mode", - "mode": "APOCALYPTIC" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/lead" - } - ], - "group": "basepower:fluid_pipe" + "group": "basepower:fluid_pipe", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:recipe_mode", + "mode": "APOCALYPTIC" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/lead" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/fluid_storage_tank.json b/src/main/resources/data/basepower/recipes/fluid_storage_tank.json index fd205290..f9d9633e 100644 --- a/src/main/resources/data/basepower/recipes/fluid_storage_tank.json +++ b/src/main/resources/data/basepower/recipes/fluid_storage_tank.json @@ -14,17 +14,20 @@ } }, "result": { - "item": "basepower:fluid_storage_tank" + "id": "basepower:fluid_storage_tank" }, "group": "basepower:fluid_storage_tank", - "conditions": [ - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/plastic" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:pipes" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/plastic" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:pipes" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/fluid_switch.json b/src/main/resources/data/basepower/recipes/fluid_switch.json index 2e3b9dc0..91a47304 100644 --- a/src/main/resources/data/basepower/recipes/fluid_switch.json +++ b/src/main/resources/data/basepower/recipes/fluid_switch.json @@ -16,17 +16,20 @@ } }, "result": { - "item": "basepower:fluid_switch" + "id": "basepower:fluid_switch" }, "group": "basepower:fluid_switch", - "conditions": [ - { - "type": "basepower:tag_not_empty", - "tag": "forge:frames/steel" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:pipes" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:tag_not_empty", + "tag": "forge:frames/steel" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:pipes" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/item_conveyor.json b/src/main/resources/data/basepower/recipes/item_conveyor.json index ea0c68d7..a636d2de 100644 --- a/src/main/resources/data/basepower/recipes/item_conveyor.json +++ b/src/main/resources/data/basepower/recipes/item_conveyor.json @@ -17,18 +17,21 @@ } }, "result": { - "item": "basepower:item_conveyor", + "id": "basepower:item_conveyor", "count": 5 }, "group": "basepower:item_conveyor", - "conditions": [ - { - "type": "basepower:tag_not_empty", - "tag": "forge:plates/steel" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:sprockets" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:tag_not_empty", + "tag": "forge:plates/steel" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:sprockets" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/item_filter_block.json b/src/main/resources/data/basepower/recipes/item_filter_block.json index 3d80c30a..ae6d3aaf 100644 --- a/src/main/resources/data/basepower/recipes/item_filter_block.json +++ b/src/main/resources/data/basepower/recipes/item_filter_block.json @@ -18,7 +18,7 @@ } }, "result": { - "item": "basepower:item_filter_block" + "id": "basepower:item_filter_block" }, "group": "basepower:item_filter_block" } diff --git a/src/main/resources/data/basepower/recipes/item_filter_food.json b/src/main/resources/data/basepower/recipes/item_filter_food.json index 794a1a07..ab62dcd1 100644 --- a/src/main/resources/data/basepower/recipes/item_filter_food.json +++ b/src/main/resources/data/basepower/recipes/item_filter_food.json @@ -17,7 +17,7 @@ } }, "result": { - "item": "basepower:item_filter_food" + "id": "basepower:item_filter_food" }, "group": "basepower:item_filter_food" } diff --git a/src/main/resources/data/basepower/recipes/item_filter_fuel.json b/src/main/resources/data/basepower/recipes/item_filter_fuel.json index 423d8a50..114feda3 100644 --- a/src/main/resources/data/basepower/recipes/item_filter_fuel.json +++ b/src/main/resources/data/basepower/recipes/item_filter_fuel.json @@ -18,7 +18,7 @@ } }, "result": { - "item": "basepower:item_filter_fuel" + "id": "basepower:item_filter_fuel" }, "group": "basepower:item_filter_fuel" } diff --git a/src/main/resources/data/basepower/recipes/item_filter_inventory.json b/src/main/resources/data/basepower/recipes/item_filter_inventory.json index 915d055a..f7a7bc83 100644 --- a/src/main/resources/data/basepower/recipes/item_filter_inventory.json +++ b/src/main/resources/data/basepower/recipes/item_filter_inventory.json @@ -17,7 +17,7 @@ } }, "result": { - "item": "basepower:item_filter_inventory" + "id": "basepower:item_filter_inventory" }, "group": "basepower:item_filter_inventory" } diff --git a/src/main/resources/data/basepower/recipes/item_filter_ore.json b/src/main/resources/data/basepower/recipes/item_filter_ore.json index 2cc51d2a..b5279c3f 100644 --- a/src/main/resources/data/basepower/recipes/item_filter_ore.json +++ b/src/main/resources/data/basepower/recipes/item_filter_ore.json @@ -17,13 +17,11 @@ } }, "result": { - "item": "basepower:item_filter_ore" + "id": "basepower:item_filter_ore" }, "group": "basepower:item_filter_ore", - "conditions": [ - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/gold" - } - ] + "forge:condition": { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/gold" + } } diff --git a/src/main/resources/data/basepower/recipes/item_filter_overflow.json b/src/main/resources/data/basepower/recipes/item_filter_overflow.json index 8b6bf07f..aadcac98 100644 --- a/src/main/resources/data/basepower/recipes/item_filter_overflow.json +++ b/src/main/resources/data/basepower/recipes/item_filter_overflow.json @@ -13,7 +13,7 @@ } }, "result": { - "item": "basepower:item_filter_overflow" + "id": "basepower:item_filter_overflow" }, "group": "basepower:item_filter_overflow" } diff --git a/src/main/resources/data/basepower/recipes/item_filter_plant.json b/src/main/resources/data/basepower/recipes/item_filter_plant.json index 1d0761b5..2ade8127 100644 --- a/src/main/resources/data/basepower/recipes/item_filter_plant.json +++ b/src/main/resources/data/basepower/recipes/item_filter_plant.json @@ -17,7 +17,7 @@ } }, "result": { - "item": "basepower:item_filter_plant" + "id": "basepower:item_filter_plant" }, "group": "basepower:item_filter_plant" } diff --git a/src/main/resources/data/basepower/recipes/item_filter_smelt.json b/src/main/resources/data/basepower/recipes/item_filter_smelt.json index 71df57a1..cf0383dc 100644 --- a/src/main/resources/data/basepower/recipes/item_filter_smelt.json +++ b/src/main/resources/data/basepower/recipes/item_filter_smelt.json @@ -17,7 +17,7 @@ } }, "result": { - "item": "basepower:item_filter_smelt" + "id": "basepower:item_filter_smelt" }, "group": "basepower:item_filter_smelt" } diff --git a/src/main/resources/data/basepower/recipes/rotator_tool.json b/src/main/resources/data/basepower/recipes/rotator_tool.json index 70b03cd7..8f4b8f7b 100644 --- a/src/main/resources/data/basepower/recipes/rotator_tool.json +++ b/src/main/resources/data/basepower/recipes/rotator_tool.json @@ -14,17 +14,20 @@ } }, "result": { - "item": "basepower:rotator_tool" + "id": "basepower:rotator_tool" }, "group": "basepower:rotator_tool", - "conditions": [ - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/iron" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:sprockets" - } - ] + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/iron" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:sprockets" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/sprocket_bronze_nugget_tech_no_rod.json b/src/main/resources/data/basepower/recipes/sprocket_bronze_nugget_tech_no_rod.json index 8fc3c678..e642b9df 100644 --- a/src/main/resources/data/basepower/recipes/sprocket_bronze_nugget_tech_no_rod.json +++ b/src/main/resources/data/basepower/recipes/sprocket_bronze_nugget_tech_no_rod.json @@ -14,26 +14,29 @@ } }, "result": { - "item": "basepower:sprocket", + "id": "basepower:sprocket", "count": 4 }, - "conditions": [ - { - "type": "basepower:recipe_mode", - "mode": "TECH_PROGRESSION" - }, - { - "type": "basepower:tag_empty", - "tag": "forge:rods" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/steel" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:nuggets/bronze" - } - ], - "group": "basepower:sprocket" + "group": "basepower:sprocket", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:recipe_mode", + "mode": "TECH_PROGRESSION" + }, + { + "type": "basepower:tag_empty", + "tag": "forge:rods" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/steel" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:nuggets/bronze" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/sprocket_rod_normal.json b/src/main/resources/data/basepower/recipes/sprocket_rod_normal.json index cb030cbe..1adaae36 100644 --- a/src/main/resources/data/basepower/recipes/sprocket_rod_normal.json +++ b/src/main/resources/data/basepower/recipes/sprocket_rod_normal.json @@ -14,22 +14,25 @@ } }, "result": { - "item": "basepower:sprocket", + "id": "basepower:sprocket", "count": 4 }, - "conditions": [ - { - "type": "basepower:recipe_mode", - "mode": "NORMAL" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/steel" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:rods" - } - ], - "group": "basepower:sprocket" + "group": "basepower:sprocket", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:recipe_mode", + "mode": "NORMAL" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/steel" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:rods" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/sprocket_rod_tech.json b/src/main/resources/data/basepower/recipes/sprocket_rod_tech.json index 2f361a78..c9454d7c 100644 --- a/src/main/resources/data/basepower/recipes/sprocket_rod_tech.json +++ b/src/main/resources/data/basepower/recipes/sprocket_rod_tech.json @@ -14,22 +14,25 @@ } }, "result": { - "item": "basepower:sprocket", + "id": "basepower:sprocket", "count": 4 }, - "conditions": [ - { - "type": "basepower:recipe_mode", - "mode": "TECH_PROGRESSION" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/steel" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:rods" - } - ], - "group": "basepower:sprocket" + "group": "basepower:sprocket", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:recipe_mode", + "mode": "TECH_PROGRESSION" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/steel" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:rods" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/sprocket_stickwood_normal.json b/src/main/resources/data/basepower/recipes/sprocket_stickwood_normal.json index d0a47360..60cdda34 100644 --- a/src/main/resources/data/basepower/recipes/sprocket_stickwood_normal.json +++ b/src/main/resources/data/basepower/recipes/sprocket_stickwood_normal.json @@ -14,22 +14,25 @@ } }, "result": { - "item": "basepower:sprocket", + "id": "basepower:sprocket", "count": 4 }, - "conditions": [ - { - "type": "basepower:recipe_mode", - "mode": "NORMAL" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:ingots/steel" - }, - { - "type": "basepower:tag_not_empty", - "tag": "forge:rods/wooden" - } - ], - "group": "basepower:sprocket" + "group": "basepower:sprocket", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "basepower:recipe_mode", + "mode": "NORMAL" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:ingots/steel" + }, + { + "type": "basepower:tag_not_empty", + "tag": "forge:rods/wooden" + } + ] + } } diff --git a/src/main/resources/data/basepower/recipes/starch_from_potato.json b/src/main/resources/data/basepower/recipes/starch_from_potato.json index cabc1111..f06fbdff 100644 --- a/src/main/resources/data/basepower/recipes/starch_from_potato.json +++ b/src/main/resources/data/basepower/recipes/starch_from_potato.json @@ -2,10 +2,17 @@ "type": "minecraft:crafting_shapeless", "ingredients": [ [ - { "item": "minecraft:potato" }, - { "item": "minecraft:poisonous_potato" } + { + "item": "minecraft:potato" + }, + { + "item": "minecraft:poisonous_potato" + } ] ], - "result": { "item": "basepower:starch", "count": 1 }, + "result": { + "id": "basepower:starch", + "count": 1 + }, "group": "basepower:starch" } diff --git a/src/main/resources/data/basepower/recipes/steel_frame.json b/src/main/resources/data/basepower/recipes/steel_frame.json index a9c6b2ea..b2cbe868 100644 --- a/src/main/resources/data/basepower/recipes/steel_frame.json +++ b/src/main/resources/data/basepower/recipes/steel_frame.json @@ -11,13 +11,11 @@ } }, "result": { - "item": "basepower:steel_frame" + "id": "basepower:steel_frame" }, "group": "basepower:steel_frame", - "conditions": [ - { - "type": "basepower:tag_not_empty", - "tag": "forge:bars/steel" - } - ] + "forge:condition": { + "type": "basepower:tag_not_empty", + "tag": "forge:bars/steel" + } } diff --git a/src/main/resources/data/basepower/recipes/still.json b/src/main/resources/data/basepower/recipes/still.json index 46e0a417..7ef75161 100644 --- a/src/main/resources/data/basepower/recipes/still.json +++ b/src/main/resources/data/basepower/recipes/still.json @@ -16,13 +16,11 @@ } }, "result": { - "item": "basepower:still" + "id": "basepower:still" }, "group": "basepower:still", - "conditions": [ - { - "type": "basepower:tag_not_empty", - "tag": "forge:pipes" - } - ] + "forge:condition": { + "type": "basepower:tag_not_empty", + "tag": "forge:pipes" + } } diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index 6b0927b0..857da2ef 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,8 +1,7 @@ { "pack": { "description": "Power Advantage resources", - "pack_format": 15, - "forge:resource_pack_format": 15, - "forge:data_pack_format": 15 + "pack_format": 32, + "supported_formats": { "min_inclusive": 32, "max_inclusive": 41 } } } diff --git a/src/test/java/zone/moddev/mc/basepower/machines/conveyors/ConveyorTransferTest.java b/src/test/java/zone/moddev/mc/basepower/machines/conveyors/ConveyorTransferTest.java index 7d0e9878..fa1c8768 100644 --- a/src/test/java/zone/moddev/mc/basepower/machines/conveyors/ConveyorTransferTest.java +++ b/src/test/java/zone/moddev/mc/basepower/machines/conveyors/ConveyorTransferTest.java @@ -30,13 +30,14 @@ public class ConveyorTransferTest { for (int n = 0; n < 6; n++) assertTrue(TileEntityConveyor.transferItem(InventoryWrapper.wrap(source), Direction.EAST, InventoryWrapper.wrap(target), Direction.WEST)); assertTrue(source.isEmpty()); - assertEquals(4, target.getItem(0).getCount()); assertEquals("A", target.getItem(0).getTag().getString("Witness")); - assertEquals(3, target.getItem(1).getCount()); assertEquals("B", target.getItem(1).getTag().getString("Witness")); + assertEquals(4, target.getItem(0).getCount()); assertEquals("A", zone.moddev.mc.basepower.util.ItemData.customData(target.getItem(0)).getString("Witness")); + assertEquals(3, target.getItem(1).getCount()); assertEquals("B", zone.moddev.mc.basepower.util.ItemData.customData(target.getItem(1)).getString("Witness")); } private static ItemStack tagged(int count, String value) { ItemStack stack = new ItemStack(Items.DIRT, count); - stack.getOrCreateTag().putString("Witness", value); + net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA, + stack, tag -> tag.putString("Witness", value)); return stack; } } diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntitiesTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntitiesTest.java index 137af57e..cf778deb 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntitiesTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntitiesTest.java @@ -3,7 +3,6 @@ import net.minecraft.util.datafix.DataFixTypes; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.NbtUtils; import net.minecraft.nbt.StringTag; import net.minecraft.util.datafix.DataFixers; import net.minecraft.SharedConstants; @@ -215,9 +214,16 @@ private static CompoundTag convert(CompoundTag root, int version) { root = LegacyWorldDataHook.finalizeLegacyChunk(root); CompoundTag actual = root.getList("entities", 10).getCompound(0) .getList("Items", 10).getCompound(0); - assertEquals("basemetals:steel_pickaxe", actual.getString("id")); - assertEquals(1, actual.getInt("Count")); - assertEquals(17, actual.getCompound("tag").getInt("Damage")); + String order = (baseFirst ? "Base Metals first: " : "basePower first: ") + actual; + assertEquals(order, "basemetals:steel_pickaxe", actual.getString("id")); + assertEquals(order, 1, actual.getInt("count")); + assertFalse(order, actual.contains("Count")); + assertTrue(order, actual.contains("Slot", 1)); + assertEquals(order, 0, actual.getByte("Slot")); + assertEquals(order, 17, actual.getCompound("components").getInt("minecraft:damage")); + int current = SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + root.putInt("DataVersion", current); + assertEquals(order, root, convert(root.copy(), current)); } } 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 8f03fe4e..1b570db6 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntityAttributeAliasesTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntityAttributeAliasesTest.java @@ -6,10 +6,7 @@ import com.mojang.datafixers.DataFixer; import com.mojang.datafixers.schemas.Schema; import com.mojang.serialization.Dynamic; -import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.util.UUID; -import java.util.function.Supplier; import java.util.function.UnaryOperator; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.animal.Wolf; @@ -18,14 +15,11 @@ import net.minecraft.nbt.NbtUtils; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.StringTag; -import net.minecraft.util.profiling.InactiveProfiler; -import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.SharedConstants; import net.minecraft.util.datafix.DataFixers; import net.minecraft.util.datafix.DataFixTypes; import net.minecraft.util.datafix.fixes.References; import net.minecraft.server.Bootstrap; -import net.minecraft.world.level.Level; import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.ForgeMod; import org.junit.BeforeClass; @@ -34,7 +28,10 @@ public class LegacyEntityAttributeAliasesTest { private static final UUID MODIFIER = new UUID(17L, 19L); - @BeforeClass public static void bootstrap() { Bootstrap.bootStrap(); } + @BeforeClass public static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } @Test public void changesOnlyExactNameAndPreservesUnknownFieldsAndUuidModifiers() { CompoundTag entity = entity("example:vehicle", true); @@ -225,28 +222,23 @@ public class LegacyEntityAttributeAliasesTest { @Test public void actualNativeWolfRetainsCustomizedBaseTwoAndNonemptyModifier() throws Exception { registerForgeAttributes(); - // Keep constructor-free test setup without linking restricted JDK types. - Class allocator = Class.forName("sun.misc.Unsafe"); - Field unsafeField = allocator.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); - ServerLevel world = (ServerLevel) allocator.getMethod("allocateInstance", Class.class) - .invoke(unsafeField.get(null), ServerLevel.class); - set(Level.class, world, "dimension", Level.NETHER); - set(Level.class, world, "profiler", (Supplier) () -> InactiveProfiler.INSTANCE); + ServerLevel world = NativeWolfTestWorld.create(); CompoundTag source = entity("minecraft:wolf", true); source.putFloat("Health", 8F); CompoundTag red = (CompoundTag) DataFixers.getDataFixer().update(References.ENTITY, new Dynamic<>(NbtOps.INSTANCE, source.copy()), 1343, SharedConstants.getCurrentVersion().getDataVersion().getVersion()).getValue(); Wolf before = new Wolf(EntityType.WOLF, world); before.load(red); - assertEquals(1D, before.getAttribute(ForgeMod.SWIM_SPEED.get()).getBaseValue(), 0D); + var swimSpeed = ForgeMod.SWIM_SPEED.getHolder().orElseThrow(); + assertEquals(1D, before.getAttribute(swimSpeed).getBaseValue(), 0D); CompoundTag root = chunk(source); LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); Wolf after = new Wolf(EntityType.WOLF, world); after.load(root.getCompound("Level").getList("Entities", 10).getCompound(0)); - assertEquals(2D, after.getAttribute(ForgeMod.SWIM_SPEED.get()).getBaseValue(), 0D); - assertNotNull(after.getAttribute(ForgeMod.SWIM_SPEED.get()).getModifier(MODIFIER)); - assertEquals(0.75D, after.getAttribute(ForgeMod.SWIM_SPEED.get()).getModifier(MODIFIER).getAmount(), 0D); - assertEquals(2.75D, after.getAttributeValue(ForgeMod.SWIM_SPEED.get()), 0D); + assertEquals(2D, after.getAttribute(swimSpeed).getBaseValue(), 0D); + assertNotNull(after.getAttribute(swimSpeed).getModifier(MODIFIER)); + assertEquals(0.75D, after.getAttribute(swimSpeed).getModifier(MODIFIER).amount(), 0D); + assertEquals(2.75D, after.getAttributeValue(swimSpeed), 0D); assertEquals(8F, after.getHealth(), 0F); } @@ -293,9 +285,6 @@ private static void registerForgeAttributes() throws Exception { LegacyForgeAttributeRegistryTest.registerForgeAttributes(); } - private static void set(Class owner, Object instance, String name, Object value) throws Exception { - Field field = owner.getDeclaredField(name); field.setAccessible(true); field.set(instance, value); - } private static final class RecordingFixer implements DataFixer { private final UnaryOperator update; 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 07bf1889..ade93971 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyForgeAttributeRegistryTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyForgeAttributeRegistryTest.java @@ -3,29 +3,22 @@ import static org.junit.Assert.*; import com.mojang.serialization.Dynamic; -import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.util.UUID; -import java.util.function.Supplier; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.ai.attributes.AttributeInstance; import net.minecraft.world.entity.animal.Wolf; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NbtOps; -import net.minecraft.util.profiling.InactiveProfiler; -import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.SharedConstants; import net.minecraft.util.datafix.DataFixers; import net.minecraft.util.datafix.DataFixTypes; import net.minecraft.util.datafix.fixes.References; import net.minecraft.server.Bootstrap; -import net.minecraft.world.level.Level; import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.ForgeMod; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.IForgeRegistry; import org.junit.BeforeClass; import org.junit.Test; import zone.moddev.mc.advantageworks.unit.ForgeUnitRegistries; @@ -34,11 +27,12 @@ public class LegacyForgeAttributeRegistryTest { private static final String[][] ALIASES = { {"forge.swimSpeed", "forge:swim_speed"}, {"forge.nameTagDistance", "forge:nametag_distance"}, - {"forge.entity_gravity", "forge:entity_gravity"}, - {"generic.reachDistance", "forge:reach_distance"} + {"forge.entity_gravity", "minecraft:generic.gravity"}, + {"generic.reachDistance", "minecraft:player.block_interaction_range"} }; @BeforeClass public static void bootstrap() throws Exception { + SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); registerForgeAttributes(); } @@ -47,11 +41,6 @@ static void registerForgeAttributes() throws ReflectiveOperationException { ForgeUnitRegistries.registerVanillaFluidTypes(); if (ForgeMod.SWIM_SPEED.isPresent()) return; ForgeUnitRegistries.registerForgeEntries("ATTRIBUTES", ForgeRegistries.Keys.ATTRIBUTES, ForgeRegistries.ATTRIBUTES); - Method alias = ForgeMod.class.getDeclaredMethod("addAlias", IForgeRegistry.class, - ResourceLocation.class, ResourceLocation.class); - alias.setAccessible(true); - alias.invoke(null, ForgeRegistries.ATTRIBUTES, - new ResourceLocation("forge:reach_distance"), new ResourceLocation("forge:block_reach")); } @@ -69,27 +58,22 @@ static void registerForgeAttributes() throws ReflectiveOperationException { @Test public void nativeLivingEntitiesLoadCustomizedValuesAndModifiersForAllThreeAttributes() throws Exception { double[] values = {2D, 12.5D, 0.15D}; - Class allocator = Class.forName("sun.misc.Unsafe"); - Field unsafe = allocator.getDeclaredField("theUnsafe"); unsafe.setAccessible(true); - ServerLevel world = (ServerLevel) allocator.getMethod("allocateInstance", Class.class) - .invoke(unsafe.get(null), ServerLevel.class); - set(Level.class, world, "dimension", Level.NETHER); - set(Level.class, world, "profiler", (Supplier) () -> InactiveProfiler.INSTANCE); + ServerLevel world = NativeWolfTestWorld.create(); for (int index = 0; index < 3; index++) { CompoundTag source = entity(ALIASES[index][0], values[index]); source.putString("id", "minecraft:wolf"); CompoundTag raw = (CompoundTag) DataFixers.getDataFixer().update(References.ENTITY, new Dynamic<>(NbtOps.INSTANCE, source.copy()), 1343, SharedConstants.getCurrentVersion().getDataVersion().getVersion()).getValue(); Wolf negative = new Wolf(EntityType.WOLF, world); negative.load(raw); - AttributeInstance old = negative.getAttribute(ForgeRegistries.ATTRIBUTES.getValue( - new net.minecraft.resources.ResourceLocation(ALIASES[index][1]))); + AttributeInstance old = negative.getAttribute(ForgeRegistries.ATTRIBUTES.getHolder( + new net.minecraft.resources.ResourceLocation(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)).getAmount(), 0D); + assertEquals(0.125D, actual.getModifier(new UUID(17L, 19L)).amount(), 0D); assertEquals(values[index] + 0.125D, actual.getValue(), 0D); } } @@ -99,14 +83,15 @@ static void registerForgeAttributes() throws ReflectiveOperationException { CompoundTag actual = LegacyPlayerDataFixes.update(DataFixers.getDataFixer(), DataFixTypes.PLAYER, source, 1343); CompoundTag value = actual.getList("Attributes", 10).getCompound(0); - assertEquals("forge:reach_distance", value.getString("Name")); - assertSame(ForgeMod.BLOCK_REACH.get(), ForgeRegistries.ATTRIBUTES.getValue( + assertEquals("minecraft:player.block_interaction_range", value.getString("Name")); + 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")))); - AttributeInstance nativeAttribute = new AttributeInstance(ForgeMod.BLOCK_REACH.get(), unused -> {}); + 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)).getAmount(), 0D); + assertEquals(0.125D, nativeAttribute.getModifier(new UUID(17L, 19L)).amount(), 0D); assertEquals(before, source); } @@ -190,7 +175,4 @@ private static CompoundTag chunk(CompoundTag entity) { private static CompoundTag fixedEntity(CompoundTag root) { return root.getCompound("Level").getList("Entities", 10).getCompound(0); } - private static void set(Class owner, Object instance, String name, Object value) throws Exception { - Field field = owner.getDeclaredField(name); field.setAccessible(true); field.set(instance, value); - } } diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java index f76ba907..463532dd 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java @@ -9,10 +9,10 @@ import java.util.function.Consumer; import java.util.stream.Stream; import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.NbtUtils; import net.minecraft.nbt.StringTag; import net.minecraft.SharedConstants; import net.minecraft.util.datafix.DataFixers; @@ -37,20 +37,24 @@ public class LegacyMachineItemsTest { @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.load(tile); + conveyor.loadWithComponents(tile, registries()); assertSame(Items.RED_WOOL, conveyor.getItem(0).getItem()); assertEquals(7, conveyor.getItem(0).getCount()); } + private static net.minecraft.core.RegistryAccess registries() { + return net.minecraft.core.RegistryAccess.fromRegistryOfRegistries(net.minecraft.core.registries.BuiltInRegistries.REGISTRY); + } + @Test public void preservesVanillaMetadataVariantsInMachineSlots() { for (String[] row : new String[][] {{"minecraft:coal", "1", "minecraft:charcoal"}, {"minecraft:dye", "4", "minecraft:lapis_lazuli"}, {"minecraft:planks", "1", "minecraft:spruce_planks"}}) { CompoundTag item = convertedTile(stack(row[0], Integer.parseInt(row[1]), 7)) .getList("Items", 10).getCompound(0); - assertEquals(row[2], item.getString("id")); - assertEquals(7, item.getInt("Count")); - assertEquals(0, item.getInt("Slot")); + assertStack(row[2], 7, item); + assertEquals(0, item.getByte("Slot")); + assertTrue("Missing inventory slot in " + item, item.contains("Slot", 1)); } } @@ -63,12 +67,19 @@ public class LegacyMachineItemsTest { enchantment.putShort("lvl", (short) 3); enchantments.add(enchantment); tag.put("ench", enchantments); + CompoundTag display = new CompoundTag(); + display.putString("Name", "Survey Pick"); + tag.put("display", display); pick.put("tag", tag); CompoundTag actual = convertedTile(pick).getList("Items", 10).getCompound(0); - assertEquals(17, actual.getCompound("tag").getInt("Damage")); - assertEquals("minecraft:efficiency", actual.getCompound("tag") - .getList("Enchantments", 10).getCompound(0).getString("id")); - assertEquals(3, actual.getCompound("tag").getList("Enchantments", 10).getCompound(0).getInt("lvl")); + assertStack("minecraft:diamond_pickaxe", 1, actual); + assertEquals(17, actual.getCompound("components").getInt("minecraft:damage")); + assertEnchantment(actual, "minecraft:enchantments", "minecraft:efficiency", 3); + ItemStack loaded = ItemStack.parseOptional(registries(), actual); + assertSame(Items.DIAMOND_PICKAXE, loaded.getItem()); + assertEquals(1, loaded.getCount()); + assertEquals(17, loaded.getDamageValue()); + assertEquals("Survey Pick", loaded.getHoverName().getString()); } @Test public void nestedStoredItemsAreConvertedWithoutChangingMachineState() { @@ -79,7 +90,8 @@ public class LegacyMachineItemsTest { tile.putFloat("Energy", 19.5F); tile.putString("CustomName", "keep"); CompoundTag actual = convert(chunk(tile), 512).getList("block_entities", 10).getCompound(0); - assertEquals("minecraft:lapis_lazuli", actual.getCompound("Recipe").getCompound("Reference").getString("id")); + assertStack("minecraft:lapis_lazuli", 1, actual.getCompound("Recipe").getCompound("Reference")); + assertStack("minecraft:red_wool", 7, actual.getList("Items", 10).getCompound(0)); assertEquals(19.5F, actual.getFloat("Energy"), 0F); assertEquals("keep", actual.getString("CustomName")); } @@ -95,9 +107,22 @@ public class LegacyMachineItemsTest { } @Test public void currentVersionMachineDataIsNotRewritten() { - CompoundTag root = chunk(tile(stack("minecraft:red_wool", 0, 7))); + CompoundTag currentItem = new CompoundTag(); + currentItem.putString("id", "minecraft:diamond_pickaxe"); + currentItem.putInt("count", 1); + currentItem.putByte("Slot", (byte) 0); + CompoundTag components = new CompoundTag(); + components.putInt("minecraft:damage", 17); + currentItem.put("components", components); + CompoundTag currentTile = tile(currentItem); + currentTile.putString("id", "basepower:item_conveyor"); + ListTag tiles = new ListTag(); + tiles.add(currentTile); + CompoundTag root = new CompoundTag(); + root.put("block_entities", tiles); root.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion()); CompoundTag expected = root.copy(); + LegacyWorldDataHook.prepareLegacyChunk(root); LegacyWorldDataHook.prepareLegacyEntities(root); assertEquals(expected, root); } @@ -107,10 +132,13 @@ public class LegacyMachineItemsTest { LegacyWorldDataHook.prepareLegacyChunk(root); LegacyWorldDataHook.prepareLegacyEntities(root); CompoundTag once = root.copy(); + LegacyWorldDataHook.prepareLegacyChunk(root); LegacyWorldDataHook.prepareLegacyEntities(root); assertEquals(once, root); - assertEquals(17, root.getCompound("Level").getList("TileEntities", 10).getCompound(0) - .getList("Items", 10).getCompound(0).getCompound("tag").getInt("Damage")); + CompoundTag item = root.getCompound("Level").getList("TileEntities", 10).getCompound(0) + .getList("Items", 10).getCompound(0); + assertStack("minecraft:diamond_pickaxe", 1, item); + assertEquals(17, item.getCompound("components").getInt("minecraft:damage")); } @Test public void namedBookEnchantmentsAreNotReinterpretedAsProtection() { @@ -124,7 +152,8 @@ public class LegacyMachineItemsTest { tag.put("StoredEnchantments", list); book.put("tag", tag); CompoundTag actual = convertedTile(book).getList("Items", 10).getCompound(0); - assertEquals(list, actual.getCompound("tag").getList("StoredEnchantments", 10)); + assertStack("minecraft:enchanted_book", 1, actual); + assertEnchantment(actual, "minecraft:stored_enchantments", "steamadvantage:steam_capacity", 3); } @Test public void nativeNestedContainerItemsAreFixedExactlyOnce() { @@ -133,16 +162,21 @@ public class LegacyMachineItemsTest { blockEntity.putString("id", "Chest"); ListTag contents = new ListTag(); contents.add(stack("minecraft:diamond_pickaxe", 17, 1)); - contents.add(stack("minecraft:wool", 14, 7)); + CompoundTag wool = stack("minecraft:wool", 14, 7); + wool.putByte("Slot", (byte) 1); + contents.add(wool); blockEntity.put("Items", contents); CompoundTag tag = new CompoundTag(); tag.put("BlockEntityTag", blockEntity); chest.put("tag", tag); CompoundTag actual = convertedTile(chest).getList("Items", 10).getCompound(0); - ListTag fixed = actual.getCompound("tag").getCompound("BlockEntityTag").getList("Items", 10); - assertEquals(17, fixed.getCompound(0).getCompound("tag").getInt("Damage")); - assertEquals("minecraft:red_wool", fixed.getCompound(1).getString("id")); - assertEquals(7, fixed.getCompound(1).getInt("Count")); + assertStack("minecraft:chest", 1, actual); + ListTag fixed = actual.getCompound("components").getList("minecraft:container", 10); + assertEquals("Container slots: " + actual, 2, fixed.size()); + CompoundTag pick = itemInContainerSlot(fixed, 0); + assertStack("minecraft:diamond_pickaxe", 1, pick); + assertEquals(17, pick.getCompound("components").getInt("minecraft:damage")); + assertStack("minecraft:red_wool", 7, itemInContainerSlot(fixed, 1)); } @Test public void foreignItemPayloadInsideAnOwnedMachineRemainsVerbatim() { @@ -166,16 +200,57 @@ public class LegacyMachineItemsTest { CompoundTag root = chunk(tile(carrier(book))); root.putInt("DataVersion", 1343); CompoundTag actual = convert(root, 1343).getList("block_entities", 10) - .getCompound(0).getList("Items", 10).getCompound(0).getCompound("tag") - .getCompound("BlockEntityTag").getList("Items", 10).getCompound(0); - assertEquals(enchantments, actual.getCompound("tag").getList("StoredEnchantments", 10)); + .getCompound(0).getList("Items", 10).getCompound(0); + ListTag fixed = actual.getCompound("components").getList("minecraft:container", 10); + assertEquals("Container slots: " + actual, 1, fixed.size()); + CompoundTag fixedBook = itemInContainerSlot(fixed, 0); + assertStack("minecraft:enchanted_book", 1, fixedBook); + assertEnchantment(fixedBook, "minecraft:stored_enchantments", "steamadvantage:steam_capacity", 3); } @Test public void nestedForeignItemsRetainTheirPrivateMetadata() { - CompoundTag foreign = stack("other:component", 9, 1); - CompoundTag actual = convertedTile(carrier(foreign)).getList("Items", 10).getCompound(0) - .getCompound("tag").getCompound("BlockEntityTag").getList("Items", 10).getCompound(0); - assertEquals(foreign, actual); + CompoundTag foreign = stack("other:component", 9, 3); + foreign.putByte("Slot", (byte) 7); + CompoundTag privateData = new CompoundTag(); + privateData.put("Private", stack("minecraft:wool", 14, 7)); + privateData.putIntArray("PrivateArray", new int[] {19, 23}); + foreign.put("tag", privateData); + CompoundTag before = foreign.copy(); + + CompoundTag chest = convertedTile(carrier(foreign)).getList("Items", 10).getCompound(0); + assertStack("minecraft:chest", 1, chest); + ListTag slots = chest.getCompound("components").getList("minecraft:container", 10); + assertEquals("Container slots: " + chest, 1, slots.size()); + CompoundTag actual = itemInContainerSlot(slots, 7); + assertStack("other:component", 3, actual); + assertEquals(before.get("Damage"), actual.get("Damage")); + assertEquals(before.getCompound("tag"), actual.getCompound("components").getCompound("minecraft:custom_data")); + } + + @Test public void registeredForeignToolKeepsWearInDirectAndNestedMachineSlots() { + String id = "advantageworks_unit:legacy_foreign_tool"; + net.minecraftforge.registries.GameData.unfreezeData(); + var tool = new net.minecraft.world.item.Item(new net.minecraft.world.item.Item.Properties().durability(250)); + net.minecraftforge.registries.ForgeRegistries.ITEMS.register(net.minecraft.resources.ResourceLocation.parse(id), tool); + CompoundTag foreign = stack(id, 9, 1); + CompoundTag payload = new CompoundTag(); + payload.putInt("Fixture", 31); + foreign.put("tag", payload); + CompoundTag original = foreign.copy(); + + var direct = zone.moddev.mc.basepower.util.ItemData.readStack(registries(), foreign); + CompoundTag chest = convertedTile(carrier(foreign)).getList("Items", 10).getCompound(0); + var carried = ItemStack.parseOptional(registries(), chest) + .get(net.minecraft.core.component.DataComponents.CONTAINER).nonEmptyItems().iterator().next(); + for (var loaded : List.of(direct, carried)) { + assertSame(tool, loaded.getItem()); + assertEquals(1, loaded.getCount()); + assertEquals(9, loaded.getDamageValue()); + assertEquals(payload, zone.moddev.mc.basepower.util.ItemData.customData(loaded)); + assertEquals(9, ItemStack.parseOptional(registries(), + (CompoundTag) loaded.save(registries())).getDamageValue()); + } + assertEquals(original, foreign); } @Test public void positiveNativeContainerConversionsDoNotEmitErrors() { @@ -312,6 +387,26 @@ private static CompoundTag carrier(CompoundTag item) { return chest; } + private static void assertStack(String id, int count, CompoundTag item) { + assertEquals(id, item.getString("id")); + assertEquals("Stack count: " + item, count, item.getInt("count")); + assertFalse("Legacy count remains in " + item, item.contains("Count")); + } + + private static void assertEnchantment(CompoundTag item, String component, String name, int level) { + CompoundTag levels = item.getCompound("components").getCompound(component).getCompound("levels"); + assertEquals("Enchantment levels: " + item, java.util.Set.of(name), levels.getAllKeys()); + assertEquals(level, levels.getInt(name)); + } + + private static CompoundTag itemInContainerSlot(ListTag slots, int slot) { + for (int i = 0; i < slots.size(); i++) { + CompoundTag entry = slots.getCompound(i); + if (entry.contains("slot", 3) && entry.getInt("slot") == slot) return entry.getCompound("item"); + } + throw new AssertionError("Missing slot " + slot + " in " + slots); + } + private static CompoundTag convertedTile(CompoundTag item) { return convert(chunk(tile(item)), 512).getList("block_entities", 10).getCompound(0); } diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixesTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixesTest.java index 363e4ce2..405ce22a 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixesTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerDataFixesTest.java @@ -5,9 +5,9 @@ import com.mojang.datafixers.schemas.Schema; import com.mojang.serialization.Dynamic; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; @@ -30,8 +30,10 @@ public class LegacyPlayerDataFixesTest { Collections.singletonMap(32, name))); CompoundTag source = player(inventory, book), before = source.copy(); CompoundTag result = convert(source, 512); - assertEquals(name, enchantment(item(result, inventory, 0), "StoredEnchantments").getString("id")); - assertEquals(3, enchantment(item(result, inventory, 0), "StoredEnchantments").getInt("lvl")); + CompoundTag actual = item(result, inventory, 0); + assertStack("minecraft:enchanted_book", 1, actual); + assertSlot(actual, 0); + assertComponentEnchantment(actual, "minecraft:stored_enchantments", name, 3); assertEquals(before, source); } } @@ -40,16 +42,20 @@ public class LegacyPlayerDataFixesTest { @Test public void namedOldEnchAndAlreadyModernEnchantmentsSurvive() { for (String key : new String[] {"ench", "Enchantments"}) { CompoundTag result = convert(player("Inventory", enchanted("minecraft:diamond_pickaxe", key, true)), 512); - assertEquals("steamadvantage:steam_capacity", enchantment(item(result, "Inventory", 0), "Enchantments").getString("id")); - assertEquals(17, item(result, "Inventory", 0).getCompound("tag").getInt("Damage")); + CompoundTag actual = item(result, "Inventory", 0); + assertStack("minecraft:diamond_pickaxe", 1, actual); + assertComponentEnchantment(actual, "minecraft:enchantments", "steamadvantage:steam_capacity", 3); + assertEquals(17, actual.getCompound("components").getInt("minecraft:damage")); } } @Test public void numericEnchantmentsStillUseTheNativeMapping() { for (String key : new String[] {"ench", "StoredEnchantments"}) { CompoundTag result = convert(player("Inventory", enchanted("minecraft:enchanted_book", key, false)), 512); - String outputKey = key.equals("ench") ? "Enchantments" : key; - assertEquals("minecraft:efficiency", enchantment(item(result, "Inventory", 0), outputKey).getString("id")); + String component = key.equals("ench") ? "minecraft:enchantments" : "minecraft:stored_enchantments"; + CompoundTag actual = item(result, "Inventory", 0); + assertStack("minecraft:enchanted_book", 1, actual); + assertComponentEnchantment(actual, component, "minecraft:efficiency", 3); } } @@ -61,31 +67,62 @@ public class LegacyPlayerDataFixesTest { contents.add(enchanted("minecraft:diamond_pickaxe", "ench", true)); CompoundTag wool = stack("minecraft:wool", 14); wool.putByte("Slot", (byte) 1); + wool.putByte("Count", (byte) 7); contents.add(wool); CompoundTag book = enchanted("minecraft:enchanted_book", "StoredEnchantments", true); book.putByte("Slot", (byte) 2); contents.add(book); + CompoundTag foreign = stack("other:component", 0); + foreign.putByte("Slot", (byte) 7); + foreign.putByte("Count", (byte) 3); + CompoundTag privateData = new CompoundTag(); + privateData.put("Private", stack("minecraft:wool", 14)); + privateData.putIntArray("PrivateArray", new int[] {19, 23}); + foreign.put("tag", privateData); + contents.add(foreign); blockEntity.put("Items", contents); CompoundTag tag = new CompoundTag(); tag.put("BlockEntityTag", blockEntity); chest.put("tag", tag); CompoundTag source = player("Inventory", chest), before = source.copy(); - ListTag fixed = item(convert(source, 512), "Inventory", 0).getCompound("tag") - .getCompound("BlockEntityTag").getList("Items", 10); - assertEquals(17, fixed.getCompound(0).getCompound("tag").getInt("Damage")); - assertEquals("minecraft:red_wool", fixed.getCompound(1).getString("id")); - assertEquals("steamadvantage:steam_capacity", enchantment(fixed.getCompound(0), "Enchantments").getString("id")); - assertEquals("steamadvantage:steam_capacity", enchantment(fixed.getCompound(2), "StoredEnchantments").getString("id")); + CompoundTag result = convert(source, 512); + CompoundTag fixedChest = item(result, "Inventory", 0); + assertStack("minecraft:chest", 1, fixedChest); + assertSlot(fixedChest, 0); + ListTag fixed = fixedChest.getCompound("components").getList("minecraft:container", 10); + assertEquals("Container slots: " + fixedChest, 4, fixed.size()); + CompoundTag pick = itemInContainerSlot(fixed, 0); + assertStack("minecraft:diamond_pickaxe", 1, pick); + assertEquals(17, pick.getCompound("components").getInt("minecraft:damage")); + assertComponentEnchantment(pick, "minecraft:enchantments", "steamadvantage:steam_capacity", 3); + assertStack("minecraft:red_wool", 7, itemInContainerSlot(fixed, 1)); + CompoundTag fixedBook = itemInContainerSlot(fixed, 2); + assertStack("minecraft:enchanted_book", 1, fixedBook); + assertComponentEnchantment(fixedBook, "minecraft:stored_enchantments", "steamadvantage:steam_capacity", 3); + CompoundTag fixedForeign = itemInContainerSlot(fixed, 7); + assertStack("other:component", 3, fixedForeign); + assertEquals(privateData, fixedForeign.getCompound("components").getCompound("minecraft:custom_data")); + int current = SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + assertEquals(result, convert(result, current)); assertEquals(before, source); } @Test public void foreignItemPayloadIsNotRestoredOverNativeChanges() { CompoundTag foreign = enchanted("other:component", "StoredEnchantments", true); foreign.getCompound("tag").putInt("Private", 19); - CompoundTag actual = item(convert(player("Inventory", foreign), 512), "Inventory", 0); + CompoundTag source = player("Inventory", foreign); + CompoundTag before = source.copy(); + CompoundTag actual = item(convert(source, 512), "Inventory", 0); + assertStack("other:component", 1, actual); + assertSlot(actual, 0); assertFalse("Native damage removal must not be undone by a whole-stack overlay", actual.contains("Damage")); - assertEquals(19, actual.getCompound("tag").getInt("Private")); - assertEquals("steamadvantage:steam_capacity", enchantment(actual, "StoredEnchantments").getString("id")); + CompoundTag components = actual.getCompound("components"); + assertFalse("Legacy subtype is not vanilla item wear", components.contains("minecraft:damage")); + assertFalse("The non-book's private data must not become a stored enchantment", + components.contains("minecraft:stored_enchantments")); + assertEquals(item(before, "Inventory", 0).getCompound("tag"), + components.getCompound("minecraft:custom_data")); + assertEquals(before, source); } @Test public void restorationChangesOnlyTheSavedName() { @@ -96,7 +133,7 @@ public class LegacyPlayerDataFixesTest { enchantment(book, "StoredEnchantments").putString("id", "minecraft:protection"); enchantment(book, "StoredEnchantments").putShort("lvl", (short) 5); book.getCompound("tag").putInt("NativeField", 42); - }); + }, true); CompoundTag result = LegacyPlayerDataFixes.update(fixer, DataFixTypes.PLAYER, source, 704); CompoundTag book = item(result, "Inventory", 0); assertEquals("other:native_result", book.getString("id")); @@ -121,20 +158,24 @@ public class LegacyPlayerDataFixesTest { assertEquals(before, source); } - @Test public void usesThePassedFixerAndNativePlayerTypeWithAnExplicit704Boundary() { + @Test public void usesThePassedFixerAndNativePlayerTypeWithExplicitSchemaBoundaries() { int current = SharedConstants.getCurrentVersion().getDataVersion().getVersion(); - for (int source : new int[] {-1, 512, 703, 704, 1343, 2230}) { + for (int source : new int[] {-1, 512, 703, 704, 1343, 2230, 3464, 3465, 3466}) { RecordingFixer fixer = new RecordingFixer(root -> {}); LegacyPlayerDataFixes.update(fixer, DataFixTypes.PLAYER, player("Inventory", stack("minecraft:stone", 0)), source); - List expected = source < 704 ? Arrays.asList(source + "->704", "704->" + current) - : Collections.singletonList(source + "->" + current); + List expected = new ArrayList<>(); + if (source < 704) expected.add(source + "->704"); + if (source < 3465) expected.add(Math.max(source, 704) + "->3465"); + expected.add(Math.max(source, 3465) + "->" + current); assertEquals(expected, fixer.calls); } } @Test public void currentOrNewerDataIsCopiedWithoutOldVersionDfu() { + CompoundTag modern = convert(player("Inventory", stack("minecraft:diamond_pickaxe", 17)), 512); for (int version : new int[] {SharedConstants.getCurrentVersion().getDataVersion().getVersion(), Integer.MAX_VALUE}) { - CompoundTag source = player("Inventory", stack("minecraft:diamond_pickaxe", 17)); + CompoundTag source = modern.copy(); + source.putInt("DataVersion", version); RecordingFixer fixer = new RecordingFixer(root -> fail("Current data must not be reinterpreted")); CompoundTag result = LegacyPlayerDataFixes.update(fixer, DataFixTypes.PLAYER, source, version); assertEquals(source, result); @@ -173,7 +214,10 @@ public class LegacyPlayerDataFixesTest { for (int version : new int[] {704, 1343, 1519, 2230}) { CompoundTag source = player("EnderItems", enchanted("minecraft:enchanted_book", "StoredEnchantments", true)); CompoundTag result = convert(source, version); - assertEquals("steamadvantage:steam_capacity", enchantment(item(result, "EnderItems", 0), "StoredEnchantments").getString("id")); + CompoundTag actual = item(result, "EnderItems", 0); + assertStack("minecraft:enchanted_book", 1, actual); + assertSlot(actual, 0); + assertComponentEnchantment(actual, "minecraft:stored_enchantments", "steamadvantage:steam_capacity", 3); } } @@ -241,14 +285,44 @@ static CompoundTag enchanted(String id, String key, boolean named) { static CompoundTag item(CompoundTag root, String inventory, int index) { return root.getList(inventory, 10).getCompound(index); } static CompoundTag enchantment(CompoundTag item, String key) { return item.getCompound("tag").getList(key, 10).getCompound(0); } + private static void assertStack(String id, int count, CompoundTag item) { + assertEquals(id, item.getString("id")); + assertEquals("Stack count: " + item, count, item.getInt("count")); + assertFalse("Legacy count remains in " + item, item.contains("Count")); + } + + private static void assertSlot(CompoundTag item, int slot) { + assertTrue("Missing inventory slot in " + item, item.contains("Slot", 1)); + assertEquals(slot, item.getByte("Slot")); + } + + private static void assertComponentEnchantment(CompoundTag item, String component, String name, int level) { + CompoundTag levels = item.getCompound("components").getCompound(component).getCompound("levels"); + assertEquals("Enchantment levels: " + item, Set.of(name), levels.getAllKeys()); + assertEquals(level, levels.getInt(name)); + } + + private static CompoundTag itemInContainerSlot(ListTag slots, int slot) { + for (int i = 0; i < slots.size(); i++) { + CompoundTag entry = slots.getCompound(i); + if (entry.contains("slot", 3) && entry.getInt("slot") == slot) return entry.getCompound("item"); + } + throw new AssertionError("Missing slot " + slot + " in " + slots); + } + private static final class RecordingFixer implements DataFixer { private final Consumer mutation; + private final boolean legacyStageOnly; final List calls = new ArrayList<>(); - RecordingFixer(Consumer mutation) { this.mutation = mutation; } + RecordingFixer(Consumer mutation) { this(mutation, false); } + RecordingFixer(Consumer mutation, boolean legacyStageOnly) { + this.mutation = mutation; + this.legacyStageOnly = legacyStageOnly; + } @Override public Dynamic update(DSL.TypeReference type, Dynamic input, int from, int to) { assertSame(References.PLAYER, type); calls.add(from + "->" + to); - mutation.accept((CompoundTag) input.getValue()); + if (!legacyStageOnly || to == 3465) mutation.accept((CompoundTag) input.getValue()); return input; } @Override public Schema getSchema(int version) { throw new AssertionError("Not used by the wrapper"); } diff --git a/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java b/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java index 6572bdb6..4e05859f 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java @@ -1,223 +1,332 @@ package zone.moddev.mc.basepower.migration; +import com.mojang.datafixers.DSL; +import com.mojang.datafixers.DataFixer; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.serialization.Dynamic; +import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; -import java.lang.reflect.Constructor; -import java.lang.reflect.Modifier; -import java.nio.charset.StandardCharsets; -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; -import javax.script.ScriptException; +import java.lang.reflect.Field; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtAccounter; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.Tag; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.fixes.References; +import net.minecraft.world.level.validation.DirectoryValidator; +import net.minecraftforge.fml.ModList; +import org.junit.Rule; import org.junit.Test; -import org.objectweb.asm.ClassReader; +import org.junit.rules.TemporaryFolder; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.util.datafix.DataFixers; import static org.junit.Assert.*; public class LegacyPlayerTransformerTest { private static final String NATIVE = "net/minecraft/util/datafix/DataFixTypes"; private static final String WRAPPER = "zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes"; - private static final String DESC = "(Lcom/mojang/datafixers/DataFixer;" - + "Lnet/minecraft/nbt/CompoundTag;I)Lnet/minecraft/nbt/CompoundTag;"; - private static final String WRAPPER_DESC = "(L" + NATIVE + ";" + DESC.substring(1); - private static final String[][] TARGETS = {{"PlayerData", "legacy_player_file"}, {"ServerWorldInfo", "legacy_embedded_player"}}; + private static final String STORAGE = "net/minecraft/world/level/storage/"; + private static final String TAG = "Lnet/minecraft/nbt/CompoundTag;"; + private static final String DYNAMIC = "Lcom/mojang/serialization/Dynamic;"; + private static final String FIXER = "Lcom/mojang/datafixers/DataFixer;"; + private static final String READER_DESC = "(Ljava/nio/file/Path;" + FIXER + ")" + DYNAMIC; + private static final PlayerHook LOOSE = new PlayerHook("PlayerDataStorage", "legacy_player_file", TAG, "updateNativePlayer"); + private static final PlayerHook EMBEDDED = new PlayerHook("LevelStorageSource", "legacy_world_info", DYNAMIC, + "updateNativeEmbeddedPlayer"); + private static final List TARGETS = List.of(LOOSE, EMBEDDED); + + @Rule public final TemporaryFolder temp = new TemporaryFolder(); @Test public void exactNativeLoosePlayerLoadAndEmbeddedUpdateArePatched() throws Exception { - for (String[] target : TARGETS) { - ClassNode node = nativeClass(target[0]); - MethodNode containing = containing(node); - assertEquals(target[0].equals("PlayerData") ? "load" : "updatePlayerTag", containing.name); - assertEquals(1, calls(node, NATIVE)); - transform(node, target[1]); - assertEquals(0, calls(node, NATIVE)); - assertEquals(1, calls(node, WRAPPER)); - assertSame(containing, containing(node)); + for (PlayerHook target : TARGETS) { + ClassNode node = target.read(); + MethodNode containing = nativePlayerMethod(node, target); + assertTrue(containing.name, containing.name.startsWith(target == LOOSE ? "lambda$load$" : "lambda$readLevelDataTagFixed$")); + int otherNativeUpdates = target.nativeCalls(node).size() - 1; + target.transform(node); + assertEquals(otherNativeUpdates, target.nativeCalls(node).size()); + assertEquals(1, LegacyTransformerOrderTest.calls(node, WRAPPER, target.wrapperName(), target.wrapperDesc())); + assertEquals(1, LegacyTransformerOrderTest.calls(containing, WRAPPER, target.wrapperName(), target.wrapperDesc())); } } - @Test public void embeddedGetterStillReachesThePatchedNativeUpdate() throws Exception { - ClassNode node = nativeClass("ServerWorldInfo"); - MethodNode update = containing(node); - transform(node, "legacy_embedded_player"); - boolean getterCallsUpdate = false, saveCallsUpdate = false; - for (MethodNode method : node.methods) for (AbstractInsnNode instruction : method.instructions.toArray()) { - if (!(instruction instanceof MethodInsnNode)) continue; - MethodInsnNode call = (MethodInsnNode) instruction; - if (call.owner.equals(node.name) && call.name.equals(update.name) && call.desc.equals(update.desc)) { - getterCallsUpdate |= method.name.equals("getLoadedPlayerTag"); - saveCallsUpdate |= method.name.equals("createTag"); - } - } - assertTrue("Embedded getter must use the patched update", getterCallsUpdate); - assertTrue("Embedded save must use the same patched update", saveCallsUpdate); - } + @Test public void transformedNativeEmbeddedReaderExecutesAndDoesNotReconvertCurrentPlayer() throws Exception { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + ClassNode node = EMBEDDED.read(); + EMBEDDED.transform(node); + byte[] transformed = bytes(node); + String name = node.name.replace('/', '.'); + Class storage = new ClassLoader(getClass().getClassLoader()) { + Class define() { return defineClass(name, transformed, 0, transformed.length); } - @Test public void transformedNativeEmbeddedGetterExecutesAndDoesNotReconvert() throws Exception { - net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); - ClassNode node = nativeClass("ServerWorldInfo"); - transform(node, "legacy_embedded_player"); - ClassWriter writer = new ClassWriter(0); - node.accept(writer); - final byte[] bytes = writer.toByteArray(); - Class type = new ClassLoader(getClass().getClassLoader()) { - Class define() { return defineClass(null, bytes, 0, bytes.length); } + @Override protected Class loadClass(String requested, boolean resolve) throws ClassNotFoundException { + // Keep the native access object's nest in the transformed reader's loader. + if (!requested.startsWith(name + "$")) return super.loadClass(requested, resolve); + synchronized (getClassLoadingLock(requested)) { + Class type = findLoadedClass(requested); + if (type == null) { + try (InputStream input = LegacyTransformerOrderTest.resource("/" + requested.replace('.', '/') + ".class")) { + byte[] data = input.readAllBytes(); + type = defineClass(requested, data, 0, data.length); + } catch (IOException e) { + throw new ClassNotFoundException(requested, e); + } + } + if (resolve) resolveClass(type); + return type; + } + } }.define(); - for (int version : new int[] {512, 1343}) { - CompoundTag source = LegacyPlayerDataFixesTest.player("Inventory", LegacyPlayerDataFixesTest - .enchanted("minecraft:enchanted_book", "StoredEnchantments", true)); - CompoundTag before = source.copy(); - Object info = embeddedInfo(type, source, version); - CompoundTag result = (CompoundTag) type.getMethod("getLoadedPlayerTag").invoke(info); - assertEquals("steamadvantage:steam_capacity", LegacyPlayerDataFixesTest.enchantment( - LegacyPlayerDataFixesTest.item(result, "Inventory", 0), "StoredEnchantments").getString("id")); - assertSame("The native upgraded flag must prevent repeat conversion", result, - type.getMethod("getLoadedPlayerTag").invoke(info)); - assertEquals(before, source); - } - } + Path saves = temp.newFolder("saves").toPath(); + RecordingFixer fixer = new RecordingFixer(DataFixers.getDataFixer()); + Object validator = storage.getMethod("parseValidator", Path.class).invoke(null, saves.resolve("allowed_symlinks.txt")); + Object source = storage.getConstructor(Path.class, Path.class, DirectoryValidator.class, DataFixer.class) + .newInstance(saves, saves.resolveSibling("backups"), validator, fixer); - @Test public void renamedNativeAndSrgLikeCallNamesDoNotAffectMatching() throws Exception { - for (String[] target : TARGETS) { - ClassNode node = nativeClass(target[0]); - for (MethodNode method : node.methods) { - if (!method.name.startsWith("<")) method.name = "func_test_" + method.name; - for (AbstractInsnNode instruction : method.instructions.toArray()) { - if (instruction instanceof MethodInsnNode) { - MethodInsnNode call = (MethodInsnNode) instruction; - if (call.owner.equals(NATIVE)) call.name = "func_244999_a"; - } + // An empty native ModList supplies the metadata hook's normal loader context in plain JUnit. + ModList previous = ModList.get(); + if (previous == null) ModList.of(List.of(), List.of()); + try { + int current = SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + for (int old : new int[] {512, 1343}) { + String world = "legacy-" + old; + Path levelDat = saves.resolve(world).resolve("level.dat"); + CompoundTag original = levelData(old, legacyPlayer()); + CompoundTag migrated; + try (AutoCloseable access = openWorld(storage, source, world)) { + NbtIo.writeCompressed(original, levelDat); + fixer.playerCalls.clear(); + migrated = readPlayer(access); + assertPlayerContents(migrated); + int boundary = 3465; // Minecraft 1.20.1, before item components. + assertEquals(old < 704 ? List.of(old + "->704", "704->" + boundary, boundary + "->" + current) + : List.of(old + "->" + boundary, boundary + "->" + current), fixer.playerCalls); + assertEquals(original, NbtIo.readCompressed(levelDat, NbtAccounter.unlimitedHeap())); } + CompoundTag saved = levelData(current, migrated); + NbtIo.writeCompressed(saved, levelDat); + try (AutoCloseable access = openWorld(storage, source, world)) { + fixer.playerCalls.clear(); + assertEquals(migrated, readPlayer(access)); + assertTrue("Current player data must not re-enter legacy DFU", fixer.playerCalls.isEmpty()); + assertEquals(saved, NbtIo.readCompressed(levelDat, NbtAccounter.unlimitedHeap())); + } + } + } finally { + if (previous == null) { + Field instance = ModList.class.getDeclaredField("INSTANCE"); + instance.setAccessible(true); + instance.set(null, null); } - transform(node, target[1]); - assertEquals(1, calls(node, WRAPPER)); - assertEquals(0, calls(node, NATIVE)); + } + } + + @Test public void renamedPlayerCallAndNativeLambdaNamesDoNotAffectMatching() throws Exception { + for (PlayerHook target : TARGETS) { + ClassNode node = target.read(); + MethodNode method = nativePlayerMethod(node, target); + nativePlayerCall(node, target).name = "renamedNativePlayerUpdate"; + method.name = "renamedPlayerLambda"; + target.transform(node); + assertEquals(1, LegacyTransformerOrderTest.calls(method, WRAPPER, target.wrapperName(), target.wrapperDesc())); } } @Test public void absentCallFailsClosedWithoutPatchingOtherOwners() throws Exception { - for (String[] target : TARGETS) { - ClassNode node = nativeClass(target[0]); - nativeCall(node).owner = "other/UnrelatedNBTUtil"; - rejected(node, target[1]); - assertEquals(0, calls(node, WRAPPER)); + for (PlayerHook target : TARGETS) { + ClassNode node = target.read(); + nativePlayerCall(node, target).owner = "other/UnrelatedNBTUtil"; + rejectedUnchanged(node, target); } } @Test public void duplicateCallsFailClosedBeforeEitherIsRewritten() throws Exception { - for (String[] target : TARGETS) { - ClassNode node = nativeClass(target[0]); - MethodInsnNode call = nativeCall(node); - containing(node).instructions.insertBefore(call, new MethodInsnNode(call.getOpcode(), call.owner, call.name, call.desc, call.itf)); - rejected(node, target[1]); - assertEquals(2, calls(node, NATIVE)); - assertEquals(0, calls(node, WRAPPER)); + for (PlayerHook target : TARGETS) { + ClassNode node = target.read(); + MethodNode method = nativePlayerMethod(node, target); + MethodInsnNode call = nativePlayerCall(node, target); + method.instructions.insertBefore(call, new MethodInsnNode(call.getOpcode(), call.owner, call.name, call.desc, call.itf)); + rejectedUnchanged(node, target); } } - @Test public void wrongOverloadIsNotMistakenForTheFourArgumentCall() throws Exception { - for (String[] target : TARGETS) { - ClassNode node = nativeClass(target[0]); - nativeCall(node).desc = DESC.replace(";I)", ";II)"); - rejected(node, target[1]); - assertEquals(0, calls(node, WRAPPER)); - } + @Test public void duplicateEmbeddedPlayerSitesFailClosedBeforeAnyReaderIsRewritten() throws Exception { + ClassNode node = EMBEDDED.read(); + MethodNode original = nativePlayerMethod(node, EMBEDDED); + MethodNode extra = new MethodNode(original.access, "anotherPlayerReader", original.desc, original.signature, null); + original.accept(extra); + node.methods.add(extra); + rejectedUnchanged(node, EMBEDDED); } - private static MethodNode containing(ClassNode node) { - for (MethodNode method : node.methods) for (AbstractInsnNode instruction : method.instructions.toArray()) { - if (instruction instanceof MethodInsnNode) { - MethodInsnNode call = (MethodInsnNode) instruction; - if ((call.owner.equals(NATIVE) && call.desc.equals(DESC)) - || (call.owner.equals(WRAPPER) && call.desc.equals(WRAPPER_DESC))) return method; - } + @Test public void wrongOverloadIsNotMistakenForTheThreeArgumentCall() throws Exception { + for (PlayerHook target : TARGETS) { + ClassNode node = target.read(); + nativePlayerCall(node, target).desc = target.desc().replace(";I)", ";II)"); + rejectedUnchanged(node, target); } - throw new AssertionError("No player update call"); } - private static MethodInsnNode nativeCall(ClassNode node) { - for (AbstractInsnNode instruction : containing(node).instructions.toArray()) { - if (instruction instanceof MethodInsnNode && ((MethodInsnNode) instruction).owner.equals(NATIVE) - && ((MethodInsnNode) instruction).desc.equals(DESC)) return (MethodInsnNode) instruction; + @Test public void embeddedScanCannotBorrowAnotherDataFixTypeAfterPlayer() throws Exception { + ClassNode node = EMBEDDED.read(); + MethodNode method = nativePlayerMethod(node, EMBEDDED); + AbstractInsnNode playerType = Arrays.stream(method.instructions.toArray()) + .filter(LegacyPlayerTransformerTest::loadsPlayerType).findFirst().orElseThrow(); + InsnList changeType = new InsnList(); + changeType.add(new InsnNode(Opcodes.POP)); + changeType.add(new FieldInsnNode(Opcodes.GETSTATIC, NATIVE, "LEVEL", "L" + NATIVE + ";")); + method.instructions.insert(playerType, changeType); + rejectedUnchanged(node, EMBEDDED); + } + + @Test public void publicEmbeddedReadStillDelegatesToThePatchedNativeReader() throws Exception { + ClassNode node = EMBEDDED.read(); + EMBEDDED.transform(node); + ClassNode access = LegacyTransformerOrderTest.nativeClass(STORAGE + "LevelStorageSource$LevelStorageAccess"); + assertEquals(1, LegacyTransformerOrderTest.calls(access, node.name, "readLevelDataTagFixed", READER_DESC)); + assertEquals(1, LegacyTransformerOrderTest.calls(node, WRAPPER, EMBEDDED.wrapperName(), EMBEDDED.wrapperDesc())); + } + + 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()); + return methods.getFirst(); + } + + private static MethodInsnNode nativePlayerCall(ClassNode node, PlayerHook target) { + return target.nativeCalls(nativePlayerMethod(node, target)).getFirst(); + } + + private static boolean loadsPlayerType(AbstractInsnNode instruction) { + return instruction instanceof FieldInsnNode field && field.getOpcode() == Opcodes.GETSTATIC + && field.owner.equals(NATIVE) && field.name.equals("PLAYER"); + } + + private static void rejectedUnchanged(ClassNode node, PlayerHook target) throws Exception { + byte[] before = bytes(node); + LegacyTransformerOrderTest.expectRejection(() -> target.transform(node), + target == LOOSE ? "exactly one native player" : "embedded player"); + assertArrayEquals("Rejection must precede any bytecode edit", before, bytes(node)); + } + + private static byte[] bytes(ClassNode node) { + ClassWriter writer = new ClassWriter(0); + node.accept(writer); + return writer.toByteArray(); + } + + private static CompoundTag levelData(int version, CompoundTag player) { + CompoundTag root = new CompoundTag(); + CompoundTag data = new CompoundTag(); + data.putInt("DataVersion", version); + data.put("Player", player.copy()); + root.put("Data", data); + return root; + } + + private static CompoundTag legacyPlayer() { + CompoundTag book = legacyStack("minecraft:enchanted_book", 4, 1, 0); + CompoundTag enchantment = new CompoundTag(); + enchantment.putString("id", "steamadvantage:steam_capacity"); + enchantment.putShort("lvl", (short) 3); + ListTag enchantments = new ListTag(); + enchantments.add(enchantment); + CompoundTag tag = new CompoundTag(); + tag.put("StoredEnchantments", enchantments); + book.put("tag", tag); + ListTag inventory = new ListTag(); + inventory.add(book); + inventory.add(legacyStack("minecraft:wool", 7, 5, 14)); + CompoundTag player = new CompoundTag(); + player.put("Inventory", inventory); + return player; + } + + private static CompoundTag legacyStack(String id, int slot, int count, int damage) { + CompoundTag stack = new CompoundTag(); + stack.putString("id", id); + stack.putByte("Slot", (byte) slot); + stack.putByte("Count", (byte) count); + stack.putShort("Damage", (short) damage); + return stack; + } + + private static CompoundTag readPlayer(AutoCloseable access) throws Exception { + Dynamic fixed = (Dynamic) access.getClass().getMethod("getDataTag").invoke(access); + return ((CompoundTag) fixed.getValue()).getCompound("Player"); + } + + private static AutoCloseable openWorld(Class storage, Object source, String world) throws Exception { + return (AutoCloseable) storage.getMethod("createAccess", String.class).invoke(source, world); + } + + private static void assertPlayerContents(CompoundTag player) { + ListTag items = player.getList("Inventory", Tag.TAG_COMPOUND); + assertEquals(2, items.size()); + CompoundTag book = items.getCompound(0); + assertEquals("minecraft:enchanted_book", book.getString("id")); + assertEquals(4, book.getByte("Slot")); + assertEquals(1, book.getInt("count")); + CompoundTag levels = book.getCompound("components").getCompound("minecraft:stored_enchantments").getCompound("levels"); + assertEquals(3, levels.getInt("steamadvantage:steam_capacity")); + CompoundTag wool = items.getCompound(1); + assertEquals("minecraft:red_wool", wool.getString("id")); + assertEquals(7, wool.getByte("Slot")); + assertEquals(5, wool.getInt("count")); + } + + private record PlayerHook(String nativeName, String coremodName, String payload, String wrapperName) { + String desc() { return "(" + FIXER + payload + "I)" + payload; } + String wrapperDesc() { return "(L" + NATIVE + ";" + desc().substring(1); } + + ClassNode read() throws IOException { + return LegacyTransformerOrderTest.nativeClass(STORAGE + nativeName); } - throw new AssertionError("Missing native call"); - } - - private static int calls(ClassNode node, String owner) { - int count = 0; - for (MethodNode method : node.methods) for (AbstractInsnNode instruction : method.instructions.toArray()) { - if (instruction instanceof MethodInsnNode) { - MethodInsnNode call = (MethodInsnNode) instruction; - boolean wrapper = owner.equals(WRAPPER); - if (call.owner.equals(owner) && call.desc.equals(wrapper ? WRAPPER_DESC : DESC)) { - assertEquals(wrapper ? Opcodes.INVOKESTATIC : Opcodes.INVOKEVIRTUAL, call.getOpcode()); - if (wrapper) assertEquals("updateNativePlayer", call.name); - count++; - } - } + + void transform(ClassNode node) throws Exception { + LegacyTransformerOrderTest.transform(node, coremodName, false); } - return count; - } - private static ClassNode nativeClass(String name) throws Exception { - ClassNode node = new ClassNode(); - String nativeName = name.equals("PlayerData") ? "PlayerDataStorage" : "PrimaryLevelData"; - try (InputStream input = resource("/net/minecraft/world/level/storage/" + nativeName + ".class")) { - new ClassReader(input).accept(node, 0); + List nativeCalls(MethodNode method) { + return Arrays.stream(method.instructions.toArray()) + .filter(insn -> insn instanceof MethodInsnNode call && call.getOpcode() == Opcodes.INVOKEVIRTUAL + && call.owner.equals(NATIVE) && call.desc.equals(desc())) + .map(MethodInsnNode.class::cast).toList(); } - return node; - } - - private static Object embeddedInfo(Class type, CompoundTag source, int version) throws Exception { - // The native constructor also uses LevelSettings to attach the lifecycle. - for (Constructor constructor : type.getDeclaredConstructors()) { - if (!Modifier.isPrivate(constructor.getModifiers())) continue; - Class[] types = constructor.getParameterTypes(); - Object[] args = new Object[types.length]; - for (int i = 0; i < types.length; i++) { - if (types[i] == int.class) args[i] = 0; - if (types[i] == boolean.class) args[i] = false; - if (types[i] == long.class) args[i] = 0L; - if (types[i] == float.class) args[i] = 0F; - if (types[i] == com.mojang.serialization.Lifecycle.class) args[i] = com.mojang.serialization.Lifecycle.stable(); - if (types[i] == net.minecraft.world.level.LevelSettings.class) { - args[i] = new net.minecraft.world.level.LevelSettings("Test", net.minecraft.world.level.GameType.SURVIVAL, - false, net.minecraft.world.Difficulty.NORMAL, false, new net.minecraft.world.level.GameRules(), - net.minecraft.world.level.WorldDataConfiguration.DEFAULT); - } - } - args[0] = DataFixers.getDataFixer(); - args[1] = version; - args[2] = source; - constructor.setAccessible(true); - return constructor.newInstance(args); + + List nativeCalls(ClassNode node) { + return node.methods.stream().flatMap(method -> nativeCalls(method).stream()).toList(); } - throw new AssertionError("Missing native embedded-player constructor"); } - private static void rejected(ClassNode node, String target) throws Exception { - try { transform(node, target); fail("Missing or ambiguous native player calls must abort transformation"); } - catch (ScriptException expected) { assertTrue(expected.getMessage(), expected.getMessage().contains("exactly one")); } - } + private static final class RecordingFixer implements DataFixer { + private final DataFixer nativeFixer; + private final List playerCalls = new ArrayList<>(); + + RecordingFixer(DataFixer nativeFixer) { this.nativeFixer = nativeFixer; } - private static void transform(ClassNode node, String target) throws Exception { - ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); - assertNotNull("Transformer tests require Forge's bundled Nashorn engine", engine); - try (InputStreamReader reader = new InputStreamReader(resource("/coremods/poweradvantage_118_migration.js"), StandardCharsets.UTF_8)) { - engine.eval(reader); + @Override public Dynamic update(DSL.TypeReference type, Dynamic input, int from, int to) { + if (type == References.PLAYER) playerCalls.add(from + "->" + to); + return nativeFixer.update(type, input, from, to); } - engine.put("targetClass", node); - assertSame(node, engine.eval("initializeCoreMod().poweradvantage_" + target + ".transformer(targetClass)")); - } - private static InputStream resource(String name) { - InputStream input = LegacyPlayerTransformerTest.class.getResourceAsStream(name); - assertNotNull("Missing pinned native resource " + name, input); - return input; + @Override public Schema getSchema(int version) { return nativeFixer.getSchema(version); } } } 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 8e28764e..982a2ec7 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyRegistryArchiveTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyRegistryArchiveTest.java @@ -20,7 +20,7 @@ public class LegacyRegistryArchiveTest { Path world = temporary.newFolder().toPath(); Path level = world.resolve("level.dat"); CompoundTag source = registrySnapshot(); - NbtIo.writeCompressed(source, level.toFile()); + NbtIo.writeCompressed(source, level); assertEquals(level.toFile(), LegacyWorldDataHook.legacyRegistrySource(level.toFile())); byte[] before = Files.readAllBytes(level); LegacyWorldDataHook.preserveLegacyRegistry(level.toFile(), source); @@ -28,16 +28,16 @@ public class LegacyRegistryArchiveTest { CompoundTag nativeSave = new CompoundTag(); nativeSave.put("fml", new CompoundTag()); - NbtIo.writeCompressed(nativeSave, level.toFile()); - NbtIo.writeCompressed(nativeSave, world.resolve("level.dat_old").toFile()); + NbtIo.writeCompressed(nativeSave, level); + NbtIo.writeCompressed(nativeSave, world.resolve("level.dat_old")); var retained = LegacyWorldDataHook.legacyRegistrySource(level.toFile()); assertNotNull(retained); assertNotEquals(level.toFile(), retained); - var archive = NbtIo.readCompressed(retained); + var archive = NbtIo.readCompressed(retained.toPath(), net.minecraft.nbt.NbtAccounter.unlimitedHeap()); assertEquals(source.getCompound("FML"), archive.getCompound("FML")); assertFalse(archive.contains("Data")); LegacyWorldDataHook.preserveLegacyRegistry(level.toFile(), nativeSave); - assertEquals(archive, NbtIo.readCompressed(retained)); + assertEquals(archive, NbtIo.readCompressed(retained.toPath(), net.minecraft.nbt.NbtAccounter.unlimitedHeap())); } @Test public void terrainGuardProtectsOnlySavedDimensionsAndOldHeights() throws Exception { 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 a89212d8..f1c84674 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyTransformerOrderTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyTransformerOrderTest.java @@ -1,100 +1,229 @@ package zone.moddev.mc.basepower.migration; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; +import javax.script.ScriptException; import org.junit.Test; import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.JumpInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import static org.junit.Assert.*; -/** Applies both real migration scripts to the exact native Forge classes. */ +/** Applies both real migration scripts to the exact native Forge 50 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"; + private static final String ENTITIES = "net/minecraft/world/level/chunk/storage/EntityStorage"; + private static final String REGION = "net/minecraft/world/level/chunk/storage/SimpleRegionStorage"; + private static final String WORLDGEN = "net/minecraft/server/level/WorldGenRegion"; + private static final String POWER = "zone/moddev/mc/basepower/migration/LegacyWorldDataHook"; + private static final String METALS = "zone/moddev/mc/basemetals/migration/LegacyWorldDataHook"; + private static final String METADATA = "zone/moddev/mc/basepower/migration/LegacyWorldMetadata"; + private static final String PLAYER = "zone/moddev/mc/basepower/migration/LegacyPlayerDataFixes"; + private static final String NBT = "Lnet/minecraft/nbt/CompoundTag;"; + private static final String DYNAMIC = "Lcom/mojang/serialization/Dynamic;"; + private static final String FIXER = "Lcom/mojang/datafixers/DataFixer;"; + private static final String WORLD_READ = "(Ljava/nio/file/Path;" + FIXER + ")" + DYNAMIC; + private static final String CHUNK_UPGRADE = "(Lnet/minecraft/resources/ResourceKey;" + + "Ljava/util/function/Supplier;" + NBT + "Ljava/util/Optional;)" + NBT; + @Test public void advantageThenBaseMetalsPreservesBothHooks() throws Exception { check(false, false); } @Test public void baseMetalsThenAdvantagePreservesBothHooks() throws Exception { check(true, false); } - @Test public void productionMethodNamesDoNotChangeEitherOrdering() throws Exception { + + @Test public void descriptorBasedChunkHooksDoNotDependOnMethodNamesInEitherOrder() throws Exception { check(false, true); check(true, true); } - private static void check(boolean baseFirst, boolean productionNames) throws Exception { - String[][] targets = { - {"net/minecraft/world/level/storage/LevelStorageSource", "legacy_world_info", "prepareLegacyWorld"}, - {"net/minecraft/world/level/chunk/storage/ChunkStorage", "legacy_chunk_status", "prepareLegacyChunk"}, - {"net/minecraft/server/level/WorldGenRegion", "legacy_worldgen_guard", "shouldBlockWorldgenWrite"} - }; - for (String[] target : targets) { - ClassNode node = new ClassNode(); - try (InputStream input = getResource("/" + target[0] + ".class")) { new ClassReader(input).accept(node, 0); } - for (MethodNode method : node.methods) { - // Plain JUnit has no Forge name-mapping service for this SRG target. - if (method.name.equals("ensureCanWrite")) method.name = "m_180807_"; - else if (productionNames && !method.name.startsWith("<")) method.name = "test_srg_" + method.name; - } - transform(node, target[1], baseFirst); - transform(node, target[1], !baseFirst); - assertEquals(target[0], 1, calls(node, "basepower", target[2])); - assertEquals(target[0], 1, calls(node, "basemetals", target[2])); - if (target[1].equals("legacy_chunk_status")) { - assertEquals(1, calls(node, "basepower", "finalizeLegacyChunk")); - assertEquals(1, calls(node, "basemetals", "finalizeLegacyChunk")); - assertEquals(1, calls(node, "basepower", "copyChunkReadForMigration")); - assertEquals(1, calls(node, "basepower", "prepareLegacyEntities")); - assertEntityPreparationOrder(node); - } + @Test public void separateEntityStorageStillUsesTheNativeRegionConverterThroughThePowerHook() throws Exception { + ClassNode node = nativeClass(ENTITIES); + String desc = "(" + NBT + "I)" + NBT; + assertEquals(1, nativeEntityCalls(node, desc).size()); + transform(node, "separate_entity_storage", false); + assertEquals(0, nativeEntityCalls(node, desc).size()); + assertEquals(1, calls(node, POWER, "upgradeEntityChunk", "(L" + REGION + ";" + desc.substring(1))); + + ClassNode missing = nativeClass(ENTITIES); + nativeEntityCalls(missing, desc).getFirst().desc = "(" + NBT + "J)" + NBT; + expectRejection(() -> transform(missing, "separate_entity_storage", false), "exactly one native entity-chunk"); + assertEquals(0, calls(missing, POWER, "upgradeEntityChunk", "(L" + REGION + ";" + desc.substring(1))); + } + + private static void check(boolean baseFirst, boolean renamedChunk) throws Exception { + ClassNode world = nativeClass(WORLD); + applyBoth(world, "legacy_world_info", baseFirst); + MethodNode fixed = method(world, "readLevelDataTagFixed", WORLD_READ); + List readers = methods(fixed); + assertEquals(List.of("java/nio/file/Path", lastApplied(baseFirst), "java/nio/file/Path", + firstApplied(baseFirst), WORLD), readers.subList(0, 5).stream().map(call -> call.owner).toList()); + assertEquals("readLevelDataTagRaw", readers.get(4).name); + assertBoth(world, "prepareLegacyWorld", "(Ljava/io/File;)V"); + assertEquals(1, calls(method(world, "readLevelDataTagRaw", "(Ljava/nio/file/Path;)" + NBT), + METADATA, "remapRaw", "(" + NBT + ")" + NBT)); + String tag = "Lnet/minecraft/nbt/Tag;"; + assertEquals(1, calls(method(world, "readLightweightData", "(Ljava/nio/file/Path;)" + tag), + METADATA, "remapLightweight", "(" + tag + ")" + tag)); + assertEquals(1, calls(world, PLAYER, "updateNativeEmbeddedPlayer", + "(Lnet/minecraft/util/datafix/DataFixTypes;" + FIXER + DYNAMIC + "I)" + DYNAMIC)); + + ClassNode chunk = nativeClass(CHUNK); + MethodNode upgrade = method(chunk, "upgradeChunkTag", CHUNK_UPGRADE); + MethodNode read = method(chunk, "read", "(Lnet/minecraft/world/level/ChunkPos;)Ljava/util/concurrent/CompletableFuture;"); + if (renamedChunk) { + renameChunkMethod(chunk, upgrade); + renameChunkMethod(chunk, read); + renameChunkMethod(chunk, method(chunk, "getVersion", "(" + NBT + ")I")); + } + applyBoth(chunk, "legacy_chunk_status", baseFirst); + assertBoth(chunk, "prepareLegacyChunk", "(" + NBT + ")V"); + assertBoth(chunk, "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;")); + assertChunkOrder(upgrade, baseFirst); + + ClassNode worldgen = nativeClass(WORLDGEN); + applyBoth(worldgen, "legacy_worldgen_guard", baseFirst); + String guardDesc = "(L" + WORLDGEN + ";Lnet/minecraft/core/BlockPos;)Z"; + assertBoth(worldgen, "shouldBlockWorldgenWrite", guardDesc); + MethodNode guard = method(worldgen, "ensureCanWrite", "(Lnet/minecraft/core/BlockPos;)Z"); + List guardCalls = methods(guard); + assertGuard(guardCalls.get(0), lastApplied(baseFirst)); + assertGuard(guardCalls.get(1), firstApplied(baseFirst)); + } + + private static void assertChunkOrder(MethodNode upgrade, boolean baseFirst) { + List entries = methods(upgrade).subList(0, 4); + assertEquals(List.of("prepareLegacyChunk", "prepareLegacyChunk", "prepareLegacyEntities", "getVersion"), + List.of(entries.get(0).name, entries.get(1).name, entries.get(2).name, + entries.get(3).name.replace("renamed_", ""))); + assertEquals(List.of(lastApplied(baseFirst), firstApplied(baseFirst), POWER, CHUNK), + entries.stream().map(call -> call.owner).toList()); + assertEquals("(" + NBT + ")I", entries.get(3).desc); + for (AbstractInsnNode instruction : upgrade.instructions.toArray()) { + if (instruction.getOpcode() != Opcodes.ARETURN) continue; + assertTrue(instruction.getPrevious() instanceof MethodInsnNode); + MethodInsnNode last = (MethodInsnNode) instruction.getPrevious(); + assertTrue(last.getPrevious() instanceof MethodInsnNode); + MethodInsnNode before = (MethodInsnNode) last.getPrevious(); + assertEquals("finalizeLegacyChunk", before.name); + assertEquals("finalizeLegacyChunk", last.name); + assertEquals(firstApplied(baseFirst), before.owner); + assertEquals(lastApplied(baseFirst), last.owner); } } - private static void assertEntityPreparationOrder(ClassNode node) { - for (MethodNode method : node.methods) { - int preparations = 0; - boolean entitiesPrepared = false; - for (AbstractInsnNode instruction : method.instructions.toArray()) { - if (!(instruction instanceof MethodInsnNode)) continue; - MethodInsnNode call = (MethodInsnNode) instruction; - if (call.name.equals("prepareLegacyChunk")) { - assertFalse("All mod entry hooks must precede entity DFU", entitiesPrepared); - preparations++; - } - if (call.name.equals("prepareLegacyEntities")) { - assertEquals("Both mod item migrations must precede entity DFU", 2, preparations); - entitiesPrepared = true; - } - if (call.owner.equals(node.name) && call.desc.equals("(Lnet/minecraft/nbt/CompoundTag;)I")) { - assertTrue("Entity staging must precede native version handling", entitiesPrepared); - } + private static void assertGuard(MethodInsnNode call, String owner) { + assertEquals(owner, call.owner); + assertEquals("shouldBlockWorldgenWrite", call.name); + assertEquals(Opcodes.IFEQ, call.getNext().getOpcode()); + assertEquals(Opcodes.ICONST_0, call.getNext().getNext().getOpcode()); + AbstractInsnNode denied = call.getNext().getNext().getNext(); + assertEquals(Opcodes.IRETURN, denied.getOpcode()); + assertSame(((JumpInsnNode) call.getNext()).label, denied.getNext()); + } + + private static String firstApplied(boolean baseFirst) { return baseFirst ? METALS : POWER; } + private static String lastApplied(boolean baseFirst) { return baseFirst ? POWER : METALS; } + + private static List nativeEntityCalls(ClassNode node, String desc) { + return methods(node).stream().filter(call -> call.getOpcode() == Opcodes.INVOKEVIRTUAL + && call.owner.equals(REGION) && call.name.equals("upgradeChunkTag") && call.desc.equals(desc)).toList(); + } + + private static void assertBoth(ClassNode node, String name, String desc) { + assertEquals(node.name, 1, calls(node, POWER, name, desc)); + assertEquals(node.name, 1, calls(node, METALS, name, desc)); + } + + private static void renameChunkMethod(ClassNode node, MethodNode target) { + for (MethodInsnNode call : methods(node)) { + if (call.owner.equals(node.name) && call.name.equals(target.name) && call.desc.equals(target.desc)) { + call.name = "renamed_" + call.name; } } + target.name = "renamed_" + target.name; + } + + private static void applyBoth(ClassNode node, String target, boolean baseFirst) throws Exception { + transform(node, target, baseFirst); + transform(node, target, !baseFirst); } - private static void transform(ClassNode node, String target, boolean base) throws Exception { + static void transform(ClassNode node, String target, boolean base) throws Exception { String mod = base ? "basemetals" : "poweradvantage"; String path = base ? "/coremods/basemetals_118_compatibility.js" : "/coremods/poweradvantage_118_migration.js"; ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); assertNotNull("Transformer tests require Forge's bundled Nashorn engine", engine); - try (InputStreamReader reader = new InputStreamReader(getResource(path), StandardCharsets.UTF_8)) { engine.eval(reader); } + try (InputStreamReader reader = new InputStreamReader(resource(path), StandardCharsets.UTF_8)) { engine.eval(reader); } engine.put("targetClass", node); - assertSame(node, engine.eval("initializeCoreMod()." + mod + "_" + target + ".transformer(targetClass)")); + String entry = "initializeCoreMod()." + mod + "_" + target; + assertEquals("CLASS", engine.eval(entry + ".target.type")); + assertEquals(node.name.replace('/', '.'), engine.eval(entry + ".target.name")); + assertSame(node, engine.eval(entry + ".transformer(targetClass)")); } - private static InputStream getResource(String name) { + static ClassNode nativeClass(String internalName) throws IOException { + ClassNode node = new ClassNode(); + try (InputStream input = resource("/" + internalName + ".class")) { + new ClassReader(input).accept(node, 0); + } + return node; + } + + static InputStream resource(String name) { InputStream input = LegacyTransformerOrderTest.class.getResourceAsStream(name); assertNotNull("Missing pinned native dependency resource " + name, input); return input; } - private static int calls(ClassNode node, String mod, String name) { - int calls = 0; - for (MethodNode method : node.methods) for (AbstractInsnNode instruction : method.instructions.toArray()) { - if (!(instruction instanceof MethodInsnNode)) continue; - MethodInsnNode call = (MethodInsnNode) instruction; - if (call.owner.equals("zone/moddev/mc/" + mod + "/migration/LegacyWorldDataHook") && call.name.equals(name)) calls++; + static void expectRejection(TransformerCall call, String message) throws Exception { + try { + call.run(); + fail("An unsupported native reader must abort transformation"); + } catch (ScriptException expected) { + assertTrue(expected.getMessage(), expected.getMessage().contains(message)); } - return calls; + } + + @FunctionalInterface interface TransformerCall { void run() throws Exception; } + + static int calls(ClassNode node, String owner, String name, String desc) { + return node.methods.stream().mapToInt(method -> calls(method, owner, name, desc)).sum(); + } + + static int calls(MethodNode method, String owner, String name, String desc) { + return (int) methods(method).stream().filter(call -> { + if (!call.owner.equals(owner) || !call.name.equals(name) || !call.desc.equals(desc)) return false; + assertEquals(Opcodes.INVOKESTATIC, call.getOpcode()); + return true; + }).count(); + } + + private static List methods(ClassNode node) { + List all = new ArrayList<>(); + node.methods.forEach(method -> all.addAll(methods(method))); + return all; + } + + private static List methods(MethodNode method) { + return Arrays.stream(method.instructions.toArray()).filter(MethodInsnNode.class::isInstance) + .map(MethodInsnNode.class::cast).toList(); + } + + private static MethodNode method(ClassNode node, String name, String desc) { + return node.methods.stream().filter(method -> method.name.equals(name) && method.desc.equals(desc)) + .findFirst().orElseThrow(() -> new AssertionError("Missing native method " + node.name + "." + name + desc)); } } diff --git a/src/test/java/zone/moddev/mc/basepower/migration/NativeWolfTestWorld.java b/src/test/java/zone/moddev/mc/basepower/migration/NativeWolfTestWorld.java new file mode 100644 index 00000000..3361caca --- /dev/null +++ b/src/test/java/zone/moddev/mc/basepower/migration/NativeWolfTestWorld.java @@ -0,0 +1,46 @@ +package zone.moddev.mc.basepower.migration; + +import com.mojang.serialization.Lifecycle; +import java.lang.reflect.Field; +import java.util.List; +import java.util.function.Supplier; +import net.minecraft.core.HolderSet; +import net.minecraft.core.MappedRegistry; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.profiling.InactiveProfiler; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.entity.animal.WolfVariant; +import net.minecraft.world.entity.animal.WolfVariants; +import net.minecraft.world.level.Level; + +final class NativeWolfTestWorld { + private NativeWolfTestWorld() {} + + static ServerLevel create() throws ReflectiveOperationException { + Class allocator = Class.forName("sun.misc.Unsafe"); + Field unsafe = allocator.getDeclaredField("theUnsafe"); + unsafe.setAccessible(true); + ServerLevel world = (ServerLevel) allocator.getMethod("allocateInstance", Class.class) + .invoke(unsafe.get(null), ServerLevel.class); + set(world, "dimension", Level.NETHER); + set(world, "profiler", (Supplier) () -> InactiveProfiler.INSTANCE); + // The native wolf constructor now resolves its default variant in the world's registry. + var variants = new MappedRegistry<>(Registries.WOLF_VARIANT, Lifecycle.stable()); + Registry.register(variants, WolfVariants.PALE, new WolfVariant( + ResourceLocation.parse("minecraft:entity/wolf/wolf"), + ResourceLocation.parse("minecraft:entity/wolf/wolf_tame"), + ResourceLocation.parse("minecraft:entity/wolf/wolf_angry"), HolderSet.direct())); + set(world, "registryAccess", new RegistryAccess.ImmutableRegistryAccess(List.of(variants)).freeze()); + return world; + } + + private static void set(ServerLevel world, String name, Object value) throws ReflectiveOperationException { + Field field = Level.class.getDeclaredField(name); + field.setAccessible(true); + field.set(world, value); + } +} 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 9ec9bf12..fae60cd4 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryMappingEventTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryMappingEventTest.java @@ -89,6 +89,17 @@ public class VanillaRegistryMappingEventTest { } } + @Test public void retiresOnlyTheRemovedVanillaEmptyPotionIndex() { + IForgeRegistry active = registry(null, false, "minecraft:potion"); + for (String id : new String[] {"minecraft:empty", "minecraft:water", "othermod:empty"}) { + var mapping = new MissingMappingsEvent.Mapping<>(active, active, ResourceLocation.parse(id), 42); + VanillaRegistryRemaps.remap(event(active, mapping), active); + assertEquals(id, id.equals("minecraft:empty") ? 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)); } 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 ed49356b..aab126b2 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemapsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemapsTest.java @@ -18,6 +18,15 @@ public class VanillaRegistryRemapsTest { assertNull(VanillaRegistryRemaps.replacement(id("entity_type"), id("grass_path"))); } + @Test public void retainsVanillaBlockItemAndEnchantmentRenamesThroughForge50() { + expect("block", "grass", "short_grass"); + expect("item", "grass", "short_grass"); + expect("item", "scute", "turtle_scute"); + expect("enchantment", "sweeping", "sweeping_edge"); + assertNull(VanillaRegistryRemaps.replacement(id("block"), id("scute"))); + assertNull(VanillaRegistryRemaps.replacement(id("item"), id("sweeping"))); + } + @Test public void retainsTheRenamedBerryPickingSound() { expect("sound_event", "item.sweet_berries.pick_from_bush", "block.sweet_berry_bush.pick_berries"); assertNull(VanillaRegistryRemaps.replacement(id("item"), id("item.sweet_berries.pick_from_bush"))); diff --git a/src/test/java/zone/moddev/mc/basepower/recipe/PartsLootTest.java b/src/test/java/zone/moddev/mc/basepower/recipe/PartsLootTest.java index b83c8057..8714197c 100644 --- a/src/test/java/zone/moddev/mc/basepower/recipe/PartsLootTest.java +++ b/src/test/java/zone/moddev/mc/basepower/recipe/PartsLootTest.java @@ -10,31 +10,33 @@ public class PartsLootTest { LootTable table = LootTable.lootTable().build(); ResourceLocation chest = new ResourceLocation("minecraft", "chests/simple_dungeon"); PartsLoot.addPools(table, chest, true, mod -> true); - Object original = table.getPool("basesteam:legacy_parts"); + var original = table.removePool("basesteam:legacy_parts"); assertNotNull(original); + table.addPool(original); PartsLoot.addPools(table, chest, true, mod -> true); - assertSame(original, table.getPool("basesteam:legacy_parts")); - assertNotNull(table.getPool("basepower:legacy_parts")); - assertNotNull(table.getPool("baseelectric:legacy_parts")); + assertSame(original, table.removePool("basesteam:legacy_parts")); + assertNull(table.removePool("basesteam:legacy_parts")); + assertNotNull(table.removePool("basepower:legacy_parts")); + assertNotNull(table.removePool("baseelectric:legacy_parts")); } @Test public void keepsEndCityExceptionAndAbsentMods() { LootTable table = LootTable.lootTable().build(); PartsLoot.addPools(table, new ResourceLocation("minecraft", "chests/end_city_treasure"), true, mod -> !mod.equals("basesteam")); - assertNull(table.getPool("basepower:legacy_parts")); - assertNull(table.getPool("basesteam:legacy_parts")); - assertNotNull(table.getPool("baseelectric:legacy_parts")); + assertNull(table.removePool("basepower:legacy_parts")); + assertNull(table.removePool("basesteam:legacy_parts")); + assertNotNull(table.removePool("baseelectric:legacy_parts")); } @Test public void leavesUnrelatedTablesAndDisabledLootUnchanged() { for (String id : new String[] {"minecraft:chests/desert_pyramid", "othermod:chests/simple_dungeon"}) { LootTable table = LootTable.lootTable().build(); PartsLoot.addPools(table, new ResourceLocation(id), true, mod -> true); - assertNull(table.getPool("basepower:legacy_parts")); + assertNull(table.removePool("basepower:legacy_parts")); } LootTable table = LootTable.lootTable().build(); PartsLoot.addPools(table, new ResourceLocation("minecraft", "chests/simple_dungeon"), false, mod -> true); - assertNull(table.getPool("basepower:legacy_parts")); + assertNull(table.removePool("basepower:legacy_parts")); } } diff --git a/src/test/java/zone/moddev/mc/basepower/registry/MachineGuiTitleTest.java b/src/test/java/zone/moddev/mc/basepower/registry/MachineGuiTitleTest.java index 593c648b..de01ac93 100644 --- a/src/test/java/zone/moddev/mc/basepower/registry/MachineGuiTitleTest.java +++ b/src/test/java/zone/moddev/mc/basepower/registry/MachineGuiTitleTest.java @@ -22,7 +22,10 @@ public class MachineGuiTitleTest { @Test public void renamedMachinesKeepThePlayersFullName() { ChestBlockEntity tile = new ChestBlockEntity(net.minecraft.core.BlockPos.ZERO, Blocks.CHEST.defaultBlockState()); - tile.setCustomName(Component.literal("John's oil tank")); + var registries = net.minecraft.core.RegistryAccess.fromRegistryOfRegistries(net.minecraft.core.registries.BuiltInRegistries.REGISTRY); + var tag = new net.minecraft.nbt.CompoundTag(); + tag.putString("CustomName", Component.Serializer.toJson(Component.literal("John's oil tank"), registries)); + tile.loadWithComponents(tag, registries); assertEquals("John's oil tank", MachineGUIRegistry.menuTitle(tile, Blocks.CHEST).getString()); } diff --git a/src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java index c163926b..57da6e07 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java @@ -39,11 +39,12 @@ public class BuildWorkflowContractTest { int checkout = workflow.indexOf("- name: Check out pinned Base Metals"); assertTrue(name, policy >= 0 && checkout > policy); assertTrue(name, workflow.contains("ref: " + pins.getProperty("basemetals_source_commit"))); - assertTrue(name, workflow.contains("BaseMetals-" + pins.getProperty("basemetals_version") + "-deobf.jar")); + assertTrue(name, workflow.contains("build/libs/BaseMetals-" + pins.getProperty("basemetals_version") + ".jar")); + assertTrue(name, workflow.contains("build/libs/OreSpawn-" + pins.getProperty("orespawn_version") + ".jar")); assertTrue(name, workflow.contains("ref: " + pins.getProperty("orespawn_source_commit"))); assertTrue(name, workflow.contains("master-" + pins.getProperty("minecraft_version"))); assertTrue(name, workflow.contains("java-version: '25.0.3+9.0.LTS'")); - assertTrue(name, workflow.contains("java-version: '17.0.1+12'")); + assertTrue(name, workflow.contains("java-version: '" + pins.getProperty("java_setup_version") + ".0.LTS'")); assertTrue(name, workflow.contains("java-version: '8.0.502+7'")); assertFalse(name, workflow.contains("JAVA_HOME_16_X64")); assertTrue(name, workflow.contains("-Porg.gradle.java.installations.paths=")); 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 2d62be5a..af7943fc 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/CreativeTabInitializationTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/CreativeTabInitializationTest.java @@ -31,9 +31,13 @@ public class CreativeTabInitializationTest { // Plain JUnit runs without the ModLauncher event bus transformer. var tabs = ItemGroups.class.getDeclaredField("TABS"); tabs.setAccessible(true); - var addEntries = DeferredRegister.class.getDeclaredMethod("addEntries", RegisterEvent.class); - addEntries.setAccessible(true); - addEntries.invoke(tabs.get(null), event.newInstance(Registries.CREATIVE_MODE_TAB, null, registry)); + var dispatcher = Class.forName("net.minecraftforge.registries.DeferredRegister$EventDispatcher"); + var constructor = dispatcher.getDeclaredConstructor(DeferredRegister.class); + constructor.setAccessible(true); + var handleEvent = dispatcher.getDeclaredMethod("handleEvent", RegisterEvent.class); + handleEvent.setAccessible(true); + handleEvent.invoke(constructor.newInstance(tabs.get(null)), + event.newInstance(Registries.CREATIVE_MODE_TAB, null, registry)); ItemGroups.show(Items.IRON_INGOT); ItemGroups.show(Items.STICK); var id = new ResourceLocation("basepower:machines"); diff --git a/src/test/java/zone/moddev/mc/basepower/resources/NativeBlockEntityFactoryTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativeBlockEntityFactoryTest.java index fe96ed00..40544005 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/NativeBlockEntityFactoryTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativeBlockEntityFactoryTest.java @@ -40,7 +40,8 @@ public class NativeBlockEntityFactoryTest { assertNotNull(block.getTicker(null, state, Entities.CONVEYOR)); net.minecraft.nbt.CompoundTag saved = new net.minecraft.nbt.CompoundTag(); saved.putInt("x", 0); - tile.load(saved); + tile.loadWithComponents(saved, + net.minecraft.core.RegistryAccess.fromRegistryOfRegistries(net.minecraft.core.registries.BuiltInRegistries.REGISTRY)); assertEquals("NBT cannot replace the native factory's final position", position, tile.getBlockPos()); } assertNull(block.getTicker(null, state, BlockEntityType.CHEST)); diff --git a/src/test/java/zone/moddev/mc/basepower/resources/NativePlatformContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativePlatformContractTest.java index ac11c8b8..35b66811 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/NativePlatformContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativePlatformContractTest.java @@ -12,7 +12,6 @@ import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.TypeInsnNode; import static org.junit.Assert.*; public class NativePlatformContractTest { @@ -43,8 +42,9 @@ public class NativePlatformContractTest { if (call.name.equals("forEach")) installed = true; } } - if (instruction instanceof TypeInsnNode - && ((TypeInsnNode) instruction).desc.equals("net/minecraftforge/event/TagsUpdatedEvent")) { + if (instruction instanceof MethodInsnNode call + && call.owner.equals("net/minecraftforge/event/ForgeEventFactory") + && call.name.equals("onTagsUpdated")) { assertTrue("Bind tags before notifying observers", installed); events++; } @@ -53,20 +53,14 @@ public class NativePlatformContractTest { assertEquals(1, events); } - @Test public void nativeLootLoaderCallsForgeJsonHookAndNotTheRemovedAdvantageBridge() throws Exception { + @Test public void nativeLootLoaderCallsForgeEventAndNotTheRemovedAdvantageBridge() throws Exception { ClassNode node = read("net/minecraft/world/level/storage/loot/LootDataType"); int calls = 0; for (MethodNode method : node.methods) for (AbstractInsnNode instruction : method.instructions.toArray()) { if (instruction instanceof MethodInsnNode call) { assertNotEquals("zone/moddev/mc/basepower/recipe/PartsLoot", call.owner); - } - if (instruction instanceof org.objectweb.asm.tree.InvokeDynamicInsnNode dynamic) { - for (Object argument : dynamic.bsmArgs) { - if (!(argument instanceof org.objectweb.asm.Handle target)) continue; - assertNotEquals("zone/moddev/mc/basepower/recipe/PartsLoot", target.getOwner()); - if (target.getOwner().equals("net/minecraftforge/common/ForgeHooks") - && target.getName().equals("getLootTableDeserializer")) calls++; - } + if (call.owner.equals("net/minecraftforge/event/ForgeEventFactory") + && call.name.equals("onLoadLootTable")) calls++; } } assertEquals(1, calls); diff --git a/src/test/java/zone/moddev/mc/basepower/resources/NativeTestTags.java b/src/test/java/zone/moddev/mc/basepower/resources/NativeTestTags.java index de489e5c..444de0bf 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/NativeTestTags.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativeTestTags.java @@ -24,7 +24,7 @@ public static Set resolve(Function element, }; for (JsonElement json : files) { TagFile file = TagFile.CODEC.parse(JsonOps.INSTANCE, json) - .getOrThrow(false, message -> { throw new AssertionError(message); }); + .getOrThrow(IllegalArgumentException::new); if (file.replace()) values.clear(); for (TagEntry entry : file.entries()) { assertTrue("Unresolved required tag entry: " + entry, entry.build(lookup, values::add)); diff --git a/src/test/java/zone/moddev/mc/basepower/resources/RecipeRegistrationThreadTest.java b/src/test/java/zone/moddev/mc/basepower/resources/RecipeRegistrationThreadTest.java index 2e727047..53e1f057 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/RecipeRegistrationThreadTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/RecipeRegistrationThreadTest.java @@ -3,47 +3,45 @@ import java.io.InputStream; import org.junit.Test; import org.objectweb.asm.ClassReader; -import org.objectweb.asm.Handle; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.InvokeDynamicInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import static org.junit.Assert.*; public class RecipeRegistrationThreadTest { - @Test public void sharedForgeConditionsAreRegisteredAfterParallelSetup() throws Exception { - ClassNode node = new ClassNode(); - try (InputStream input = getClass().getResourceAsStream("/zone/moddev/mc/basepower/PowerAdvantage.class")) { - assertNotNull(input); - new ClassReader(input).accept(node, 0); - } + @Test public void sharedForgeConditionsUseTheNativeDeferredRegistry() throws Exception { String conditions = "zone/moddev/mc/basepower/recipe/RecipeConditions"; - int queued = 0; - for (MethodNode method : node.methods) { - boolean registrationRunnable = false; + int subscriptions = 0; + for (MethodNode method : read("zone/moddev/mc/basepower/PowerAdvantage").methods) { for (AbstractInsnNode instruction : method.instructions.toArray()) { - if (instruction instanceof MethodInsnNode) { - MethodInsnNode call = (MethodInsnNode) instruction; - assertFalse("Forge's condition HashMap is not safe during parallel loading", - call.owner.equals(conditions) && call.name.equals("register")); - if (call.owner.equals("net/minecraftforge/fml/event/lifecycle/FMLCommonSetupEvent") - && call.name.equals("enqueueWork") && registrationRunnable) { - assertEquals("Register conditions during common setup", "commonSetup", method.name); - queued++; - registrationRunnable = false; - } - } - if (instruction instanceof InvokeDynamicInsnNode) { - for (Object argument : ((InvokeDynamicInsnNode) instruction).bsmArgs) { - if (argument instanceof Handle && ((Handle) argument).getOwner().equals(conditions) - && ((Handle) argument).getName().equals("register")) { - registrationRunnable = true; - } - } + if (instruction instanceof MethodInsnNode call + && call.owner.equals(conditions) && call.name.equals("register")) { + assertEquals("Subscribe during mod construction, before the registry event", "", method.name); + assertEquals("(Lnet/minecraftforge/eventbus/api/IEventBus;)V", call.desc); + subscriptions++; } } } - assertEquals("Queue registration exactly once on Forge's loading thread", 1, queued); + assertEquals(1, subscriptions); + ClassNode codecs = read(conditions); + MethodNode register = codecs.methods.stream().filter(method -> method.name.equals("register")) + .findFirst().orElseThrow(); + assertTrue(java.util.Arrays.stream(register.instructions.toArray()).anyMatch(instruction -> + instruction instanceof MethodInsnNode call && call.owner.equals("net/minecraftforge/registries/DeferredRegister") + && call.name.equals("register") && call.desc.equals("(Lnet/minecraftforge/eventbus/api/IEventBus;)V"))); + assertTrue(codecs.methods.stream().flatMap(method -> java.util.Arrays.stream(method.instructions.toArray())) + .anyMatch(instruction -> instruction instanceof org.objectweb.asm.tree.FieldInsnNode field + && field.owner.equals("net/minecraftforge/registries/ForgeRegistries$Keys") + && field.name.equals("CONDITION_SERIALIZERS"))); + } + + private ClassNode read(String type) throws Exception { + ClassNode node = new ClassNode(); + try (InputStream input = getClass().getResourceAsStream("/" + type + ".class")) { + assertNotNull(input); + new ClassReader(input).accept(node, 0); + } + return node; } } 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 5106191c..cd1dab0a 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/RecipeUnlockContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/RecipeUnlockContractTest.java @@ -93,7 +93,7 @@ public class RecipeUnlockContractTest { if (!crafting(recipe)) continue; JsonElement result = recipe.get("result"); String output = result.isJsonPrimitive() ? result.getAsString() - : result.getAsJsonObject().get("item").getAsString(); + : result.getAsJsonObject().get("id").getAsString(); assertTrue("Missing output-specific group: " + file, recipe.has("group")); assertEquals("Different items must not share a recipe-book cell: " + file, output, recipe.get("group").getAsString()); @@ -113,8 +113,11 @@ private static String predicateKey(JsonObject predicate) { private static String unlockPredicateKey(JsonObject predicate) { assertEquals("One explicit native ingredient predicate is required", 1, predicate.size()); - if (predicate.has("tag")) return "tag:" + predicate.get("tag").getAsString(); - assertTrue("Minecraft 1.17 ignores the old singular item advancement field", predicate.has("items")); + assertTrue("An explicit native item or tag predicate is required", predicate.has("items")); + if (predicate.get("items").isJsonPrimitive()) { + String value = predicate.get("items").getAsString(); + return value.startsWith("#") ? "tag:" + value.substring(1) : "item:" + value; + } JsonArray items = predicate.getAsJsonArray("items"); assertEquals("Each criterion discovers exactly one recipe ingredient", 1, items.size()); return "item:" + items.get(0).getAsString(); @@ -126,11 +129,12 @@ private static String unlockPredicateKey(JsonObject predicate) { JsonObject criterion = read(DATA.resolve("advancements/recipes/starch_from_potato.json")) .getAsJsonObject("criteria").getAsJsonObject("has_ingredient_1"); JsonObject predicate = criterion.getAsJsonObject("conditions").getAsJsonArray("items").get(0).getAsJsonObject(); - net.minecraft.advancements.critereon.ItemPredicate nativePredicate = - net.minecraft.advancements.critereon.ItemPredicate.fromJson(predicate); - assertTrue(nativePredicate.matches(new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.POTATO))); + var registries = net.minecraft.core.RegistryAccess.fromRegistryOfRegistries(net.minecraft.core.registries.BuiltInRegistries.REGISTRY); + var ops = registries.createSerializationContext(com.mojang.serialization.JsonOps.INSTANCE); + var nativePredicate = net.minecraft.advancements.critereon.ItemPredicate.CODEC.parse(ops, predicate).getOrThrow(); + assertTrue(nativePredicate.test(new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.POTATO))); assertFalse("An unrelated dirt stack must not satisfy the real native predicate", - nativePredicate.matches(new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.DIRT))); + nativePredicate.test(new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.DIRT))); } private static void collect(JsonElement element, List result) { 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 4a6e7691..66f6e2de 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/StarchRecipeTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/StarchRecipeTest.java @@ -15,11 +15,11 @@ public class StarchRecipeTest { @Test public void craftingAcceptsOneOrdinaryOrPoisonousPotatoWithoutAnotherMod() throws Exception { JsonObject recipe = read("starch_from_potato"); assertEquals("minecraft:crafting_shapeless", recipe.get("type").getAsString()); - assertFalse(recipe.has("conditions")); + assertFalse(recipe.has("forge:condition")); JsonArray inputs = recipe.getAsJsonArray("ingredients"); assertEquals(1, inputs.size()); assertEquals(potatoes(), items(inputs.get(0).getAsJsonArray())); - assertEquals("basepower:starch", recipe.getAsJsonObject("result").get("item").getAsString()); + assertEquals("basepower:starch", recipe.getAsJsonObject("result").get("id").getAsString()); assertEquals(1, recipe.getAsJsonObject("result").get("count").getAsInt()); } @@ -27,7 +27,7 @@ public class StarchRecipeTest { JsonObject recipe = read("crushing_potato_starch"); assertEquals("basemetals:crushing", recipe.get("type").getAsString()); assertEquals(potatoes(), items(recipe.getAsJsonArray("ingredient"))); - assertEquals("basepower:starch", recipe.getAsJsonObject("result").get("item").getAsString()); + assertEquals("basepower:starch", recipe.getAsJsonObject("result").get("id").getAsString()); assertEquals(1, recipe.getAsJsonObject("result").get("count").getAsInt()); } 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 5eb0738b..9eb8b018 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.1".equals(result.get("minecraft").getAsString()) - && "47.4.10".equals(result.get("forge").getAsString()), "Wrong native client target"); + require("1.20.6".equals(result.get("minecraft").getAsString()) + && "50.2.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.12.120011".equals(versions.get("basepower").getAsString()), "Wrong native Power candidate"); + require("2.4.13.120061".equals(versions.get("basepower").getAsString()), "Wrong native Power candidate"); for (String id : new String[]{"basesteam", "baseelectric"}) { - if (versions.has(id)) require("2.2.13.120011".equals(versions.get(id).getAsString()), "Wrong native expansion candidate: " + id); + if (versions.has(id)) require("2.2.14.120061".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/NativeGuiProjection.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeGuiProjection.java index f5d7ef5d..dcffbc59 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeGuiProjection.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeGuiProjection.java @@ -1,27 +1,27 @@ package zone.moddev.mc.advantageworks.client; import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexSorting; import org.joml.Matrix4f; +import org.joml.Matrix4fStack; /** Scoped native shader projection for framebuffer-based screen tests. */ final class NativeGuiProjection implements AutoCloseable { private final Matrix4f projection = new Matrix4f(RenderSystem.getProjectionMatrix()); private final VertexSorting sorting = RenderSystem.getVertexSorting(); - private final PoseStack modelView = RenderSystem.getModelViewStack(); + private final Matrix4fStack modelView = RenderSystem.getModelViewStack(); NativeGuiProjection(float width, float height, float near, float far, float depth) { RenderSystem.setProjectionMatrix(new Matrix4f().setOrtho(0, width, height, 0, near, far), VertexSorting.ORTHOGRAPHIC_Z); - modelView.pushPose(); - modelView.setIdentity(); + modelView.pushMatrix(); + modelView.identity(); modelView.translate(0, 0, depth); RenderSystem.applyModelViewMatrix(); } @Override public void close() { - modelView.popPose(); + modelView.popMatrix(); RenderSystem.applyModelViewMatrix(); RenderSystem.setProjectionMatrix(projection, sorting); } 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 ce741f28..9987ce9a 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 @@ -81,8 +81,9 @@ private static Map, List>> fluidTags() { } private static void requireBucket(net.minecraft.world.item.Item bucket, Fluid expected) { - net.minecraftforge.fluids.FluidStack contents = net.minecraftforge.fluids.FluidUtil.getFluidContained(new ItemStack(bucket)) - .orElseThrow(() -> new IllegalStateException("Bucket has no native fluid capability: " + ForgeRegistries.ITEMS.getKey(bucket))); + net.minecraftforge.fluids.FluidStack contents = zone.moddev.mc.basepower.util.FluidContainerHelper + .getFluidContained(new ItemStack(bucket)) + .orElseThrow(() -> new IllegalStateException("Bucket cannot supply a fluid container: " + ForgeRegistries.ITEMS.getKey(bucket))); if (!(bucket instanceof BucketItem) || ((BucketItem) bucket).getFluid() != expected || expected.getBucket() != bucket || contents.getFluid() != expected || contents.getAmount() != 1000) { throw new IllegalStateException("Wrong bucket fluid: " + ForgeRegistries.ITEMS.getKey(bucket)); 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 ac44313a..7d5e163d 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 @@ -8,10 +8,8 @@ import java.io.File; import java.lang.reflect.Field; import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -27,19 +25,32 @@ import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.pipeline.RenderTarget; import net.minecraft.client.ClientRecipeBook; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtAccounter; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeHolder; import net.minecraft.stats.RecipeBook; import net.minecraft.world.entity.player.StackedContents; import net.minecraft.world.item.crafting.RecipeManager; -import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.protocol.game.ClientboundUpdateRecipesPacket; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.ForgeRegistry; +import net.minecraftforge.registries.GameData; +import net.minecraftforge.registries.RegistryManager; /** Decodes actual server recipe packets and exercises Minecraft's real cell grouping. */ final class RecipeBookGroupingProbe { - private static final int GL_PROJECTION = 5889, GL_MODELVIEW = 5888; private static final int GL_COLOR_BUFFER_BIT = 16384, GL_DEPTH_BUFFER_BIT = 256, GL_RGBA = 6408, GL_UNSIGNED_BYTE = 5121; static void verify(Minecraft minecraft, JsonObject report) { - Path root = Paths.get("recipe-book-fixtures"); + Path root = Path.of("recipe-book-fixtures"); if (!Files.isDirectory(root)) { report.addProperty("recipe_book_grouping_status", "NOT_RUN_NO_NATIVE_FIXTURES"); return; @@ -49,54 +60,58 @@ static void verify(Minecraft minecraft, JsonObject report) { try (Stream files = Files.list(root)) { // A separate main-menu client has not performed Forge's registry handshake. // Import only the generated server fixture's ID snapshot, never a user world. - Map registry = - net.minecraftforge.registries.RegistryManager.ACTIVE.takeSnapshot(false); - net.minecraft.nbt.CompoundTag saved; - try (java.io.InputStream input = Files.newInputStream(root.resolve("server-registries.dat"))) { - net.minecraft.nbt.CompoundTag level = net.minecraft.nbt.NbtIo.readCompressed(input); - require("1.20.1".equals(level.getCompound("Data").getCompound("Version").getString("Name")), - "Recipe fixture is not a native 1.20.1 server export"); - net.minecraft.nbt.ListTag mods = level.getCompound("fml").getList("LoadingModList", 10); - boolean nativeForge = false; - for (int i = 0; i < mods.size(); i++) { - net.minecraft.nbt.CompoundTag mod = mods.getCompound(i); - if ("forge".equals(mod.getString("ModId"))) nativeForge = "47.4.10".equals(mod.getString("ModVersion")); - } - require(nativeForge, "Recipe fixture was not saved by Forge 47.4.10"); - saved = level.getCompound("fml").getCompound("Registries"); + 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"); + 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")); } - String itemRegistry = net.minecraftforge.registries.ForgeRegistries.ITEMS.getRegistryName().toString(); - String blockRegistry = net.minecraftforge.registries.ForgeRegistries.BLOCKS.getRegistryName().toString(); - require(saved.contains(itemRegistry, 10) && saved.contains(blockRegistry, 10) - && !saved.getCompound(itemRegistry).getList("ids", 10).isEmpty() - && !saved.getCompound(blockRegistry).getList("ids", 10).isEmpty(), "Missing native server registry snapshot"); - for (String key : saved.getAllKeys()) registry.put(new net.minecraft.resources.ResourceLocation(key), - net.minecraftforge.registries.ForgeRegistry.Snapshot.read(saved.getCompound(key))); - require(net.minecraftforge.registries.GameData.injectSnapshot(registry, false, false).isEmpty(), - "Client lacks a server registry entry"); - List packets = files.filter(file -> file.toString().endsWith(".bin")).sorted().collect(Collectors.toList()); - require(packets.size() == 3, "Normal, Tech and Apocalyptic recipe-book packets are required"); + require(nativeForge, "Recipe fixture was not saved by Forge 50.2.0"); + CompoundTag saved = level.getCompound("fml").getCompound("Registries"); + String itemRegistry = ForgeRegistries.ITEMS.getRegistryName().toString(); + String blockRegistry = ForgeRegistries.BLOCKS.getRegistryName().toString(); + require(saved.contains(itemRegistry, Tag.TAG_COMPOUND) && saved.contains(blockRegistry, Tag.TAG_COMPOUND) + && !saved.getCompound(itemRegistry).getList("ids", Tag.TAG_COMPOUND).isEmpty() + && !saved.getCompound(blockRegistry).getList("ids", Tag.TAG_COMPOUND).isEmpty(), "Missing native server registry snapshot"); + for (String key : saved.getAllKeys()) registry.put(ResourceLocation.parse(key), + ForgeRegistry.Snapshot.read(saved.getCompound(key))); + require(GameData.injectSnapshot(registry, false, false).isEmpty(), "Client lacks a server registry entry"); + RegistryAccess registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + List packets = files.filter(file -> file.toString().endsWith(".bin")).sorted().toList(); + Set packetNames = packets.stream().map(file -> file.getFileName().toString()).collect(Collectors.toSet()); + require(packetNames.equals(Set.of("recipes-normal-sweep.bin", "recipes-tech-sweep.bin", "recipes-apocalyptic-sweep.bin")), + "Normal, Tech and Apocalyptic recipe-book packets are required"); for (Path file : packets) { - JsonObject manifest = new JsonParser().parse(new String(Files.readAllBytes(Paths.get( - file.toString().replace(".bin", ".json"))), StandardCharsets.UTF_8)).getAsJsonObject(); + Path manifestPath = file.resolveSibling(file.getFileName().toString().replace(".bin", ".json")); + JsonObject manifest = JsonParser.parseString(Files.readString(manifestPath)).getAsJsonObject(); require("PASS".equals(manifest.get("status").getAsString()), "Server recipe fixture did not pass"); Map expected = new HashMap<>(); manifest.getAsJsonArray("bookRecipes").forEach(row -> require(expected.put(row.getAsJsonObject().get("id").getAsString(), row.getAsJsonObject()) == null, "Duplicate recipe in packet manifest")); - FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.wrappedBuffer(Files.readAllBytes(file))); + RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf( + Unpooled.wrappedBuffer(Files.readAllBytes(file)), registries); ClientboundUpdateRecipesPacket packet; - try { packet = new ClientboundUpdateRecipesPacket(buffer); require(!buffer.isReadable(), "Trailing recipe packet bytes"); } + try { + packet = ClientboundUpdateRecipesPacket.STREAM_CODEC.decode(buffer); + require(!buffer.isReadable(), "Trailing recipe packet bytes"); + } finally { buffer.release(); } - RecipeManager manager = new RecipeManager(); + RecipeManager manager = new RecipeManager(registries); Set cellKeys = new HashSet<>(); - for (Recipe recipe : packet.getRecipes()) { - JsonObject row = expected.get(recipe.getId().toString()); - require(row != null, "Unexpected network recipe " + recipe.getId()); - String output = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(recipe.getResultItem(net.minecraft.core.RegistryAccess.EMPTY).getItem()).toString(); - require(output.equals(row.get("output").getAsString()) && recipe.getResultItem(net.minecraft.core.RegistryAccess.EMPTY).getCount() == row.get("count").getAsInt(), - "Client/server item registry mismatch for " + recipe.getId()); + for (RecipeHolder holder : packet.getRecipes()) { + Recipe recipe = holder.value(); + JsonObject row = expected.get(holder.id().toString()); + require(row != null, "Unexpected network recipe " + holder.id()); + ItemStack stack = recipe.getResultItem(registries); + String output = ForgeRegistries.ITEMS.getKey(stack.getItem()).toString(); + require(output.equals(row.get("output").getAsString()) && stack.getCount() == row.get("count").getAsInt(), + "Client/server item registry mismatch for " + holder.id()); require(recipe.getGroup().equals(row.get("group").getAsString()) && recipe.getGroup().equals(output), "Group changed in recipe packet"); - String type = net.minecraft.core.registries.BuiltInRegistries.RECIPE_TYPE.getKey(recipe.getType()).toString(); + String type = BuiltInRegistries.RECIPE_TYPE.getKey(recipe.getType()).toString(); require(type.equals(row.get("type").getAsString()), "Recipe type changed in packet"); cellKeys.add(type + "|" + output); } @@ -104,10 +119,11 @@ static void verify(Minecraft minecraft, JsonObject report) { require(manager.getRecipes().size() == expected.size(), "Missing network recipes"); ClientRecipeBook book = new ClientRecipeBook(); manager.getRecipes().forEach(book::add); - book.setupCollections(manager.getRecipes(), net.minecraft.core.RegistryAccess.EMPTY); + book.setupCollections(manager.getRecipes(), registries); int counted = 0; for (RecipeCollection cell : book.getCollections()) { - Set outputs = cell.getRecipes().stream().map(recipe -> net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(recipe.getResultItem(net.minecraft.core.RegistryAccess.EMPTY).getItem()).toString()) + Set outputs = cell.getRecipes().stream().map(holder -> ForgeRegistries.ITEMS.getKey( + holder.value().getResultItem(registries).getItem()).toString()) .collect(Collectors.toSet()); require(outputs.size() == 1 && cell.hasSingleResultItem(), "Mixed items share a native recipe-book cell: " + outputs); counted += cell.getRecipes().size(); @@ -133,11 +149,10 @@ private static void render(Minecraft minecraft, ClientRecipeBook book) throws Ex Set selected = new HashSet<>(java.util.Arrays.asList("baseelectric:blank_circuit_board", "baseelectric:integrated_circuit", "baseelectric:control_circuit", "baseelectric:psu", "basepower:item_conveyor")); for (RecipeCollection cell : book.getCollections()) { - Recipe recipe = cell.getRecipes().get(0); - String id = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(recipe.getResultItem(net.minecraft.core.RegistryAccess.EMPTY).getItem()).toString(); + String id = cellOutputId(cell); if (selected.contains(id) || id.startsWith("basepower:item_filter_")) shown.add(cell); } - shown.sort(java.util.Comparator.comparing(cell -> net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(cell.getRecipes().get(0).getResultItem(net.minecraft.core.RegistryAccess.EMPTY).getItem()).toString())); + shown.sort(java.util.Comparator.comparing(RecipeBookGroupingProbe::cellOutputId)); require(shown.size() == 13, "The thirteen reported circuit/conveyor cells must be separate"); int width = 1000, height = 180; RenderTarget target = new com.mojang.blaze3d.pipeline.TextureTarget(width, height, true, Minecraft.ON_OSX); @@ -165,7 +180,7 @@ private static void render(Minecraft minecraft, ClientRecipeBook book) throws Ex } button.setPosition(x, y); button.render(graphics, -1, -1, 1F); - graphics.drawString(minecraft.font, shown.get(i).getRecipes().get(0).getResultItem(net.minecraft.core.RegistryAccess.EMPTY).getDisplayName(), x + 30, y + 8, 0xFFFFFF, false); + graphics.drawString(minecraft.font, cellResult(shown.get(i)).getDisplayName(), x + 30, y + 8, 0xFFFFFF, false); } graphics.flush(); ByteBuffer pixels = ByteBuffer.allocateDirect(width * height * 4); @@ -187,6 +202,14 @@ private static void render(Minecraft minecraft, ClientRecipeBook book) throws Ex } } + private static ItemStack cellResult(RecipeCollection cell) { + return cell.getRecipes().get(0).value().getResultItem(cell.registryAccess()); + } + + private static String cellOutputId(RecipeCollection cell) { + return ForgeRegistries.ITEMS.getKey(cellResult(cell).getItem()).toString(); + } + private static void require(boolean condition, String message) { if (!condition) throw new IllegalStateException(message); } diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java index 8da44007..7227a662 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java @@ -2,9 +2,11 @@ import com.google.gson.JsonObject; import cyano.poweradvantage.api.simple.SimpleMachineGUI; +import io.netty.buffer.Unpooled; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; import com.mojang.blaze3d.platform.GlStateManager; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.pipeline.RenderTarget; @@ -14,7 +16,12 @@ import net.minecraft.core.Direction; import net.minecraft.resources.ResourceLocation; import net.minecraft.core.BlockPos; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; import zone.moddev.mc.basepower.gui.FilteredFluidTankGUI; import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.common.capabilities.ForgeCapabilities; @@ -28,11 +35,11 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import java.nio.ByteBuffer; /** Checks native neighbour culling and the actual fluid-bar drawing function. */ final class TankVisualProbe { - private static final int GL_PROJECTION = 5889, GL_MODELVIEW = 5888; private static final int GL_COLOR_BUFFER_BIT = 16384, GL_RGBA = 6408, GL_UNSIGNED_BYTE = 5121; private TankVisualProbe() {} @@ -52,14 +59,16 @@ static void verify(Minecraft minecraft, JsonObject result) { "Native capability did not fill the tank"); MetalTankTileEntity clientTank = new MetalTankTileEntity(BlockPos.ZERO, zone.moddev.mc.basepower.init.Blocks.metal_storage_tank.defaultBlockState()); - clientTank.onDataPacket(null, serverTank.getUpdatePacket()); + RegistryAccess registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + PacketLevel packetLevel = PacketLevel.withRegistries(registries); + syncTank(serverTank, clientTank, packetLevel, registries); result.addProperty("synced_tank_amount", clientTank.getTank().getFluidAmount()); require(clientTank.getTank().getFluidAmount() == 10000, "Filled tank packet lost water"); + int rgb = renderWater(minecraft, clientTank); require(handler.drain(10000, FluidAction.EXECUTE).getAmount() == 10000, "Native capability did not drain the tank"); - clientTank.onDataPacket(null, serverTank.getUpdatePacket()); + syncTank(serverTank, clientTank, packetLevel, registries); require(clientTank.getTank().getFluidAmount() == 0, "Empty tank packet retained old water"); - int rgb = renderWater(minecraft, clientTank); result.addProperty("water_bar_rgb", String.format("%06X", rgb)); int red = (rgb >>> 16) & 255; int green = (rgb >>> 8) & 255; @@ -68,6 +77,52 @@ static void verify(Minecraft minecraft, JsonObject result) { require(blue > red + 20 && blue > green + 10, "Water gauge is not blue: " + String.format("%06X", rgb)); } + private static void syncTank(MetalTankTileEntity server, MetalTankTileEntity client, PacketLevel packetLevel, + RegistryAccess registries) { + RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), registries); + // Forge 50 obtains the packet's registry access from the sending block entity's level. + server.setLevel(packetLevel); + try { + ClientboundBlockEntityDataPacket.STREAM_CODEC.encode(buffer, server.getUpdatePacket()); + ClientboundBlockEntityDataPacket packet = ClientboundBlockEntityDataPacket.STREAM_CODEC.decode(buffer); + require(!buffer.isReadable(), "Trailing tank update packet bytes"); + require(packet.getPos().equals(client.getBlockPos()) && packet.getType() == client.getType(), + "Tank update addressed the wrong client block entity"); + client.onDataPacket(null, packet, registries); + } finally { + server.setLevel(null); + buffer.release(); + } + } + + /** Provides registries for the native packet factory without opening a world. */ + private static final class PacketLevel extends ClientLevel { + private RegistryAccess registries; + + private PacketLevel() { + super(null, null, Level.OVERWORLD, null, 0, 0, + () -> net.minecraft.util.profiling.InactiveProfiler.INSTANCE, null, false, 0); + throw new AssertionError("Allocate only a registry context, not a connected client world"); + } + + @Override public RegistryAccess registryAccess() { return registries; } + + private static PacketLevel withRegistries(RegistryAccess registries) { + try { + // Like the layout probe, bypass ClientLevel's world and connection setup. + Class allocator = Class.forName("sun.misc.Unsafe"); + Field singleton = allocator.getDeclaredField("theUnsafe"); + singleton.setAccessible(true); + PacketLevel level = (PacketLevel) allocator.getMethod("allocateInstance", Class.class) + .invoke(singleton.get(null), PacketLevel.class); + level.registries = registries; + return level; + } catch (ReflectiveOperationException failure) { + throw new IllegalStateException("Could not create tank packet registry context", failure); + } + } + } + private static boolean floorVisible(final BlockState tank) { BlockGetter world = new BlockGetter() { @Override public int getHeight() { return 256; } @@ -82,7 +137,8 @@ private static boolean floorVisible(final BlockState tank) { } private static int renderWater(Minecraft minecraft, MetalTankTileEntity tile) { - FilteredFluidTankGUI definition = new FilteredFluidTankGUI(new ResourceLocation("basepower", "textures/gui/container/fluid_metal_tank_gui.png")); + FilteredFluidTankGUI definition = new FilteredFluidTankGUI(ResourceLocation.fromNamespaceAndPath( + "basepower", "textures/gui/container/fluid_metal_tank_gui.png")); final int width = 176, height = 240; Inventory inventory = new Inventory(null); SimpleMachineGUI.Container container = definition.createMenu(1, inventory, tile); @@ -95,7 +151,6 @@ void renderTank(net.minecraft.client.gui.GuiGraphics graphics) { }; screen.init(minecraft, width, height); require(screen.getMinecraft() == minecraft, "Native tank screen was not initialized"); - tile.getTank().setFluid(new cyano.poweradvantage.api.fluid.FluidStack(net.minecraft.world.level.material.Fluids.WATER, 10000)); RenderTarget framebuffer = new com.mojang.blaze3d.pipeline.TextureTarget(width, height, true, Minecraft.ON_OSX); framebuffer.bindWrite(true); NativeGuiProjection projection = new NativeGuiProjection(width, height, 1000, 3000, -2000); diff --git a/test-rig/client-resources/resources/META-INF/mods.toml b/test-rig/client-resources/resources/META-INF/mods.toml index 8b829092..224f4b95 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="[47,48)" +loaderVersion="[50,51)" 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.12.120011]" +versionRange="[2.4.13.120061]" ordering="AFTER" side="CLIENT" [[dependencies.advantageclientresources]] modId="forge" mandatory=true -versionRange="[47.4.10]" +versionRange="[50.2.0]" ordering="NONE" side="CLIENT" [[dependencies.advantageclientresources]] modId="minecraft" mandatory=true -versionRange="[1.20.1]" +versionRange="[1.20.6]" ordering="NONE" side="CLIENT" diff --git a/test-rig/client-resources/resources/pack.mcmeta b/test-rig/client-resources/resources/pack.mcmeta index c7bc948a..491ce5fa 100644 --- a/test-rig/client-resources/resources/pack.mcmeta +++ b/test-rig/client-resources/resources/pack.mcmeta @@ -1,6 +1,6 @@ { "pack": { "description": "Build-only Advantage client resource checks", - "pack_format": 7 + "pack_format": 32 } } diff --git a/test-rig/client-resources/run.ps1 b/test-rig/client-resources/run.ps1 index 2fde213e..6c7080be 100644 --- a/test-rig/client-resources/run.ps1 +++ b/test-rig/client-resources/run.ps1 @@ -30,22 +30,22 @@ function Assert-NativeMetadata([string]$Metadata, [string]$Id) { # Match the known single-mod header, not dependency modId/versionRange fields. $header = [regex]::Match($Metadata, '(?ms)^\[\[mods\]\]\s*(.*?)(?=^\[|\z)').Groups[1].Value if ($header -notmatch ('(?m)^modId\s*=\s*"' + [regex]::Escape($Id) + '"[ \t]*(?:#[^\r\n]*)?\r?$') -or - $header -notmatch '(?m)^version\s*=\s*"[^"\r\n]*\.120011"[ \t]*(?:#[^\r\n]*)?\r?$' -or - $Metadata -notmatch '(?m)^loaderVersion\s*=\s*"\[47,') { throw "Not a native Forge 47 candidate: $Id" } - $expected = @{ basepower = '2.4.12.120011'; basesteam = '2.2.13.120011'; baseelectric = '2.2.13.120011' } + $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' } 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.1' -or $Environment.FORGE_VERSION -ne '47.4.10' -or - $Environment.MCP_MAPPINGS -ne 'official_1.20.1' -or $Environment.MCP_VERSION -ne '20230612.114412' -or - $Environment.target -ne 'fmluserdevclient') { throw 'Regenerate the native 1.20.1 / Forge 47.4.10 official client launch.' } + 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.' } $program = $Attributes['org.eclipse.jdt.launching.PROGRAM_ARGUMENTS'] - if ($program -notmatch '1\.20\.1-47\.4\.10' -or - $program -notmatch '--main cpw\.mods\.bootstraplauncher\.BootstrapLauncher\b' -or - $program -notmatch '--launchTarget forgeclientuserdev\b' -or - $program -match '1\.(?:13\.2|14\.4|15\.2|16\.5|17\.1|18\.2|19\.4)|--(?:server|port|world)\b' -or + if ($program -notmatch '1\.20\.6-50\.2\.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 @([regex]::Matches($program, '--gameDir\s+\.(?=\s|$)')).Count -ne 1 -or @([regex]::Matches($program, '--gameDir\b')).Count -ne 1 -or $Attributes['org.eclipse.jdt.launching.MAIN_TYPE'] -ne 'net.minecraftforge.launcher.Main') { @@ -53,21 +53,21 @@ function Assert-NativeLaunch([hashtable]$Environment, [hashtable]$Attributes) { } } function Assert-NativeArchive([string]$Path) { - if ($Path -match '(?:forge|client-extra)-1\.(?:13\.2|14\.4|15\.2|16\.5|17\.1|18\.2|19\.4)|[\\/]PA11[3456789][\\/]') { throw "Legacy runtime archive: $Path" } + if ($Path -match '(?:forge|client-extra)-1\.(?:13\.2|14\.4|15\.2|16\.5|17\.1|18\.2|19\.4)|[\\/]PA(?:11[3456789]|1201)[\\/]') { throw "Legacy runtime archive: $Path" } } function Quote-Argument([string]$Value) { '"' + [regex]::Replace([regex]::Replace($Value, '(\\*)"', '$1$1\"'), '(\\+)$', '$1$1') + '"' } $probe = Join-Path $project 'build/client-resource-probe/advantageclientresources-deobf.jar' $runtime = Join-Path $project ('run-client-resource-runs/' + [DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N').Substring(0, 8)) -$inputReport = [ordered]@{ minecraft = '1.20.1'; forge = '47.4.10'; mappings = 'official_1.20.1'; mcp = '20230612.114412'; qualification = 'UNQUALIFIED'; output_source = $OutputSource; launch_profile = $LaunchProfile; require_extensions = [bool]$RequireExtensions; child_renderer_geometry = [bool]$ChildRendererGeometry } +$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.machine_gui_layouts = [bool]$MachineGuiLayouts $phase = 'launch-preflight' New-Item -ItemType Directory -Path $runtime | Out-Null try { if ($ChildRendererGeometry -and -not $RequireExtensions) { throw 'Child renderer geometry requires -RequireExtensions.' } -if (-not (Test-Path -LiteralPath $java)) { throw "Java 17 runtime not found: $java" } -if ((Get-Content -LiteralPath (Join-Path $JavaHome 'release') -Raw) -notmatch '(?m)^JAVA_VERSION="17(?:\.|\+|")') { throw 'Supply Java 17 for the native client.' } +if (-not (Test-Path -LiteralPath $java)) { throw "Java 21 runtime not found: $java" } +if ((Get-Content -LiteralPath (Join-Path $JavaHome 'release') -Raw) -notmatch '(?m)^JAVA_VERSION="21(?:\.|\+|")') { throw 'Supply Java 21 for the native client.' } if (-not (Test-Path -LiteralPath $probe)) { throw 'Run gradlew.bat clientResourceProbeJar verifyEclipseProductionClasspath first.' } [xml]$launch = Get-Content -LiteralPath (Join-Path $project $LaunchProfile) -Raw $attributes = @{} @@ -76,8 +76,8 @@ $inputReport.java = Get-Record $java $inputReport.launch = Get-Record (Join-Path $project $LaunchProfile) $inputReport.probe = Get-Record $probe $probeMetadata = Read-JarMetadata $probe -if ($probeMetadata -notmatch 'loaderVersion="\[47,48\)"' -or $probeMetadata -notmatch 'versionRange="\[1\.20\.1\]"' -or - $probeMetadata -notmatch 'versionRange="\[47\.4\.10\]"' -or $probeMetadata -notmatch 'versionRange="\[2\.4\.12\.120011\]"') { throw 'Rebuild the native 1.20.1 probe; do not substitute a legacy 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.' } Copy-Item -LiteralPath (Join-Path $project $LaunchProfile) -Destination (Join-Path $runtime 'launch.xml') $environment = @{} foreach ($map in $launch.launchConfiguration.mapAttribute) { @@ -108,7 +108,7 @@ $outputFiles = @(Get-ChildItem -LiteralPath $modOutput -Recurse -File | Sort-Obj if (@($outputFiles | Where-Object { $_.path -like '*.class' }).Count -eq 0) { throw 'Selected output has no compiled production classes.' } $outputFiles | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $runtime 'production-files.json') -Encoding utf8 $inputReport.production_files = Get-Record (Join-Path $runtime 'production-files.json') -if (-not $PowerCandidate) { $PowerCandidate = Join-Path $project 'build/libs-dev/basePower-2.4.12.120011-deobf.jar' } +if (-not $PowerCandidate) { $PowerCandidate = Join-Path $project 'build/libs/basePower-2.4.13.120061.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\.1-47\.4\.10\.jar$')) { throw 'Missing actual native mapped Forge runtime.' } +if (-not ($archives -match 'forge-1\.20\.6-50\.2\.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.1' -or $result.forge -ne '47.4.10') { throw 'Result is not native 1.20.1 / Forge 47.4.10 evidence.' } +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 ($MachineGuiLayouts -and $result.machine_gui_layout_status -ne 'PASS') { throw 'Machine GUI layouts did not pass; inspect the retained images and JSON.' } $expectedGuiTextChecks = if ($RequireExtensions) { 351 } else { 126 } if ($MachineGuiLayouts -and ($result.machine_gui_text_spec -ne 2 -or $result.machine_gui_text_status -ne 'PASS' -or diff --git a/test-rig/client-resources/test-launcher.ps1 b/test-rig/client-resources/test-launcher.ps1 index 04b54611..f8e06339 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="[47,)" # native Forge +loaderVersion="[50,)" # native Forge [[mods]] # mandatory header modId="basepower" # inline comment -version="2.4.12.120011" # native candidate +version="2.4.13.120061" # native candidate [[dependencies.basepower]] modId="minecraft" mandatory=true -versionRange="[1.20.1]" +versionRange="[1.20.6]" '@ Assert-NativeMetadata $native 'basepower' $checks++ -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.12.120011','2.4.4.113021')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.12.120011','2.4.5.114041')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.12.120011','2.4.5.115021')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('[47,)','[45,)')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.12.120011','2.4.7.116051')) 'basepower' } -Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.12.120011','${version}')) 'basepower' } +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 'baseelectric' } Assert-Throws { Assert-NativeMetadata '' 'basepower' } foreach ($id in 'basesteam','baseelectric') { - $expansion = $native.Replace('basepower', $id).Replace('2.4.12.120011','2.2.13.120011') + $expansion = $native.Replace('basepower', $id).Replace('2.4.13.120061','2.2.14.120061') Assert-NativeMetadata $expansion $id $checks++ - Assert-Throws { Assert-NativeMetadata ($expansion.Replace('2.2.13.120011','2.2.6.115021')) $id } + Assert-Throws { Assert-NativeMetadata ($expansion.Replace('2.2.14.120061','2.2.6.115021')) $id } } -$environment = @{ MC_VERSION = '1.20.1'; FORGE_VERSION = '47.4.10'; MCP_MAPPINGS = 'official_1.20.1'; MCP_VERSION = '20230612.114412'; target = 'fmluserdevclient' } -$attributes = @{ 'org.eclipse.jdt.launching.PROGRAM_ARGUMENTS' = '--main cpw.mods.bootstraplauncher.BootstrapLauncher --cache D:\native\1.20.1-47.4.10 -- --launchTarget forgeclientuserdev --gameDir .'; 'org.eclipse.jdt.launching.MAIN_TYPE' = 'net.minecraftforge.launcher.Main' } +$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' } 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.1-47.4.10.jar' +Assert-NativeArchive 'D:\native\forge-1.20.6-50.2.0.jar' $checks++ foreach ($path in 'D:\PA114\client.jar','D:\PA113\client.jar','D:\PA116\client.jar','D:\native\forge-1.16.5-36.2.34.jar','D:\native\forge-1.14.4-28.2.26.jar','D:\native\client-extra-1.13.2.jar') { Assert-Throws { Assert-NativeArchive $path } @@ -135,7 +135,7 @@ if ($StaticOnly) { return } $project = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path -$power = Join-Path $project 'build/libs-dev/basePower-2.4.12.120011-deobf.jar' +$power = Join-Path $project 'build/libs/basePower-2.4.13.120061.jar' Assert-NativeMetadata (Read-JarMetadata $power) 'basepower' $checks++ [xml]$launch = Get-Content -LiteralPath (Join-Path $project 'runClientAllAdvantage.launch') -Raw diff --git a/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java b/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java index fd8b8375..3eaae5db 100644 --- a/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java +++ b/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java @@ -2,10 +2,16 @@ import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; +import java.lang.reflect.Proxy; import java.security.ProtectionDomain; +import java.util.List; +import java.util.Optional; /** Runs Forge's own token and event transforms for plain JUnit. */ public final class ForgeCapabilityBootstrap { + private static final String MOD_LOADER = "net/minecraftforge/fml/ModLoader"; + private static final String AGENT = "zone/moddev/mc/advantageworks/unit/ForgeCapabilityBootstrap"; + public static void premain(String options, Instrumentation instrumentation) { instrumentation.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(ClassLoader loader, String name, Class type, @@ -13,20 +19,25 @@ public static void premain(String options, Instrumentation instrumentation) { if (loader == null || name == null) return null; boolean event = name.startsWith("net/minecraftforge/network/NetworkEvent") || name.equals("net/minecraftforge/registries/MissingMappingsEvent"); + boolean modLoader = MOD_LOADER.equals(name); String token = "net/minecraftforge/common/capabilities/CapabilityToken"; - if (!event && !(name.contains("$") || name.equals(token))) return null; + if (!event && !modLoader && !(name.contains("$") || name.equals(token))) return null; + if (!event && !modLoader && !token.equals(name) + && loader.getResource("org/objectweb/asm/ClassReader.class") == null) return null; try { Class readerType = Class.forName("org.objectweb.asm.ClassReader", true, loader); Object reader = readerType.getConstructor(byte[].class).newInstance((Object)bytes); String parent = (String)readerType.getMethod("getSuperName").invoke(reader); - if (!event && !token.equals(name) && !token.equals(parent)) return null; + if (!event && !modLoader && !token.equals(name) && !token.equals(parent)) return null; Class nodeType = Class.forName("org.objectweb.asm.tree.ClassNode", true, loader); Object node = nodeType.getConstructor().newInstance(); Class visitorType = Class.forName("org.objectweb.asm.ClassVisitor", true, loader); readerType.getMethod("accept", visitorType, int.class).invoke(reader, node, 0); Class asmType = Class.forName("org.objectweb.asm.Type", true, loader); Object asmName = asmType.getMethod("getObjectType", String.class).invoke(null, name); - if (event) { + if (modLoader) { + addLoaderFixture(loader, nodeType, node, asmName); + } else if (event) { Class transformer = Class.forName("net.minecraftforge.eventbus.EventSubclassTransformer", true, loader); var transformed = (java.util.Optional) transformer.getMethod("transform", nodeType, asmType) .invoke(transformer.getConstructor().newInstance(), node, asmName); @@ -46,9 +57,78 @@ public static void premain(String options, Instrumentation instrumentation) { nodeType.getMethod("accept", visitorType).invoke(node, writer); return (byte[])writerType.getMethod("toByteArray").invoke(writer); } catch (ReflectiveOperationException failure) { + System.err.println("Forge unit transform failed for " + name + ": " + failure); throw new IllegalStateException("Cannot run Forge's native unit transform for " + name, failure); } } }); } + + private static void addLoaderFixture(ClassLoader loader, Class nodeType, Object node, Object asmName) + throws ReflectiveOperationException { + Class insn = Class.forName("org.objectweb.asm.tree.AbstractInsnNode", true, loader); + Class list = Class.forName("org.objectweb.asm.tree.InsnList", true, loader); + Class ldc = Class.forName("org.objectweb.asm.tree.LdcInsnNode", true, loader); + Class var = Class.forName("org.objectweb.asm.tree.VarInsnNode", true, loader); + Class method = Class.forName("org.objectweb.asm.tree.MethodInsnNode", true, loader); + for (Object ctor : (List) nodeType.getField("methods").get(node)) { + Class type = ctor.getClass(); + if (!"".equals(type.getField("name").get(ctor)) || !"()V".equals(type.getField("desc").get(ctor))) continue; + Object instructions = type.getField("instructions").get(ctor); + Object entry = list.getConstructor().newInstance(); + var add = list.getMethod("add", insn); + var call = method.getConstructor(int.class, String.class, String.class, String.class, boolean.class); + add.invoke(entry, ldc.getConstructor(Object.class).newInstance(asmName)); + add.invoke(entry, call.newInstance(184, AGENT, "beforeModLoader", "(Ljava/lang/Class;)V", false)); + for (Object instruction : (Object[]) list.getMethod("toArray").invoke(instructions)) { + if ((int) insn.getMethod("getOpcode").invoke(instruction) != 177) continue; // RETURN + Object exit = list.getConstructor().newInstance(); + add.invoke(exit, var.getConstructor(int.class, int.class).newInstance(25, 0)); // ALOAD 0 + add.invoke(exit, call.newInstance(184, AGENT, "afterModLoader", "(Ljava/lang/Object;)V", false)); + list.getMethod("insertBefore", insn, list).invoke(instructions, instruction, exit); + } + list.getMethod("insert", list).invoke(instructions, entry); + return; + } + throw new NoSuchMethodException("Forge 50 ModLoader()"); + } + + public static void beforeModLoader(Class owner) throws ReflectiveOperationException { + ClassLoader loader = owner.getClassLoader(); + Class fml = Class.forName("net.minecraftforge.fml.loading.FMLLoader", true, loader); + if (fml.getMethod("getLoadingModList").invoke(null) != null) { + throw new IllegalStateException("The JUnit agent must not run inside a real Forge launch"); + } + Class loading = Class.forName("net.minecraftforge.fml.loading.LoadingModList", true, loader); + Class earlyError = Class.forName("net.minecraftforge.fml.loading.EarlyLoadingException", true, loader); + Object emptyDiscovery = loading.getMethod("of", List.class, List.class, earlyError) + .invoke(null, List.of(), List.of(), null); + loading.getMethod("setBrokenFiles", List.class).invoke(emptyDiscovery, List.of()); + var listField = fml.getDeclaredField("loadingModList"); + listField.setAccessible(true); + listField.set(null, emptyDiscovery); + Class layers = Class.forName("cpw.mods.modlauncher.api.IModuleLayerManager", true, loader); + Object classpathLayers = Proxy.newProxyInstance(loader, new Class[] {layers}, (proxy, method, args) -> { + if (method.getName().equals("getLayer")) return Optional.of(ModuleLayer.boot()); + throw new UnsupportedOperationException(method.toString()); + }); + var layerField = fml.getDeclaredField("moduleLayerManager"); + layerField.setAccessible(true); + layerField.set(null, classpathLayers); + } + + public static void afterModLoader(Object loader) throws ReflectiveOperationException { + Class owner = loader.getClass(); + Class mods = Class.forName("net.minecraftforge.fml.ModList", true, owner.getClassLoader()); + Object emptyMods = mods.getMethod("of", List.class, List.class).invoke(null, List.of(), List.of()); + var setLoaded = mods.getDeclaredMethod("setLoadedMods", List.class); + setLoaded.setAccessible(true); + setLoaded.invoke(emptyMods, List.of()); + var modList = owner.getDeclaredField("modList"); + modList.setAccessible(true); + modList.set(loader, emptyMods); + var valid = owner.getDeclaredField("loadingStateValid"); + valid.setAccessible(true); + valid.setBoolean(loader, true); + } } diff --git a/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/Forge50UnitControl.java b/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/Forge50UnitControl.java new file mode 100644 index 00000000..62225446 --- /dev/null +++ b/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/Forge50UnitControl.java @@ -0,0 +1,37 @@ +package zone.moddev.mc.advantageworks.unit; + +import net.minecraft.SharedConstants; +import net.minecraft.core.component.DataComponents; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.common.ForgeMod; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.registries.ForgeRegistries; + +/** Proves the test-only fixture reaches Forge's native item, registry, and capability paths. */ +public final class Forge50UnitControl { + public static void main(String[] args) throws Exception { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + if (new ItemStack(Items.DIRT, 16).get(DataComponents.MAX_STACK_SIZE) != 64 + || !net.minecraftforge.fml.ModLoader.isLoadingStateValid() + || net.minecraftforge.fml.ModList.get().size() != 0) { + throw new AssertionError("Native item component gathering did not complete in the empty loader"); + } + for (int run = 0; run < 2; run++) { + ForgeUnitRegistries.registerVanillaFluidTypes(); + ForgeUnitRegistries.registerForgeEntries("ATTRIBUTES", ForgeRegistries.Keys.ATTRIBUTES, ForgeRegistries.ATTRIBUTES); + if (Fluids.WATER.getFluidType() != ForgeMod.WATER_TYPE.get() + || !ForgeMod.SWIM_SPEED.isPresent()) { + throw new AssertionError("Native Forge registry objects did not bind"); + } + } + if (ForgeCapabilities.FLUID_HANDLER == null || ForgeCapabilities.ITEM_HANDLER == null + || (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"); + } +} diff --git a/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/ForgeUnitRegistries.java b/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/ForgeUnitRegistries.java index f23cf57b..ba0a9063 100644 --- a/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/ForgeUnitRegistries.java +++ b/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/ForgeUnitRegistries.java @@ -3,6 +3,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.List; import net.minecraft.core.Registry; import net.minecraft.resources.ResourceKey; import net.minecraftforge.common.ForgeMod; @@ -12,34 +13,51 @@ import net.minecraftforge.registries.IForgeRegistry; import net.minecraftforge.registries.NewRegistryEvent; import net.minecraftforge.registries.RegisterEvent; +import net.minecraftforge.registries.RegistryObject; /** Runs the narrow Forge registry phases needed by native test entities outside ModLauncher. */ public final class ForgeUnitRegistries { private ForgeUnitRegistries() {} public static synchronized void registerVanillaFluidTypes() throws ReflectiveOperationException { - if (ForgeRegistries.FLUID_TYPES.get() != null) return; - NewRegistryEvent event = new NewRegistryEvent(); - Field types = ForgeRegistries.class.getDeclaredField("DEFERRED_FLUID_TYPES"); - types.setAccessible(true); - Method create = DeferredRegister.class.getDeclaredMethod("createRegistry", NewRegistryEvent.class); - create.setAccessible(true); - create.invoke(types.get(null), event); - Method fill = NewRegistryEvent.class.getDeclaredMethod("fill"); - fill.setAccessible(true); - fill.invoke(event); + if (ForgeRegistries.FLUID_TYPES.get() == null) { + Field registries = ForgeRegistries.class.getDeclaredField("registries"); + registries.setAccessible(true); + DeferredRegister types = ((List) registries.get(null)).stream().map(DeferredRegister.class::cast) + .filter(entry -> ForgeRegistries.Keys.FLUID_TYPES.equals(entry.getRegistryKey())) + .findFirst().orElseThrow(() -> new IllegalStateException("Missing native fluid-type registry")); + NewRegistryEvent event = new NewRegistryEvent(); + dispatch(types, "createRegistry", NewRegistryEvent.class, event); + Method fill = NewRegistryEvent.class.getDeclaredMethod("fill"); + fill.setAccessible(true); + fill.invoke(event); + } registerForgeEntries("VANILLA_FLUID_TYPES", ForgeRegistries.Keys.FLUID_TYPES, ForgeRegistries.FLUID_TYPES.get()); } - public static void registerForgeEntries(String name, ResourceKey key, IForgeRegistry registry) + public static synchronized void registerForgeEntries(String name, ResourceKey key, IForgeRegistry registry) throws ReflectiveOperationException { Field field = ForgeMod.class.getDeclaredField(name); field.setAccessible(true); - Method register = DeferredRegister.class.getDeclaredMethod("addEntries", RegisterEvent.class); - register.setAccessible(true); + DeferredRegister register = (DeferredRegister) field.get(null); + if (!register.getRegistryKey().equals(key) || registry == null) + throw new IllegalArgumentException("Wrong registry for native entries " + name); + if (register.getEntries().stream().allMatch(RegistryObject::isPresent)) return; Constructor ctor = RegisterEvent.class.getDeclaredConstructor( ResourceKey.class, ForgeRegistry.class, Registry.class); ctor.setAccessible(true); - register.invoke(field.get(null), ctor.newInstance(key, registry, null)); + dispatch(register, "handleEvent", RegisterEvent.class, ctor.newInstance(key, registry, null)); + if (!register.getEntries().stream().allMatch(RegistryObject::isPresent)) + throw new IllegalStateException("Native Forge entries did not bind: " + name); + } + + private static void dispatch(DeferredRegister register, String name, Class eventType, Object event) + throws ReflectiveOperationException { + Class dispatcher = Class.forName(DeferredRegister.class.getName() + "$EventDispatcher"); + Constructor ctor = dispatcher.getDeclaredConstructor(DeferredRegister.class); + ctor.setAccessible(true); + Method handle = dispatcher.getDeclaredMethod(name, eventType); + handle.setAccessible(true); + handle.invoke(ctor.newInstance(register), event); } } diff --git a/tools/generate-recipe-unlocks.js b/tools/generate-recipe-unlocks.js index 4c3e37cb..b5fbdc1b 100644 --- a/tools/generate-recipe-unlocks.js +++ b/tools/generate-recipe-unlocks.js @@ -31,7 +31,7 @@ for (const file of fs.readdirSync(recipes).filter(name => name.endsWith('.json') unique.forEach((ingredient, index) => { criteria[`has_ingredient_${index + 1}`] = { trigger: 'minecraft:inventory_changed', - conditions: {items: [ingredient.item ? {items: [ingredient.item]} : ingredient]} + conditions: {items: [{items: ingredient.item ? [ingredient.item] : '#' + ingredient.tag}]} }; }); criteria.has_recipe = {trigger: 'minecraft:recipe_unlocked', conditions: {recipe: id}}; From 75297fd9f4ce679aca95c2948e81bbef7633b976 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Tue, 10 Nov 2026 12:23:22 +0000 Subject: [PATCH 2/9] Keep Advantage chest loot out of player datapack overrides --- .../moddev/mc/basepower/PowerAdvantage.java | 2 +- .../moddev/mc/basepower/recipe/PartsLoot.java | 89 ++++++++++++------- .../loot_modifiers/vanilla_chest_parts.json | 4 + .../loot_modifiers/global_loot_modifiers.json | 4 + .../mc/basepower/recipe/PartsLootTest.java | 42 ++++----- 5 files changed, 80 insertions(+), 61 deletions(-) create mode 100644 src/main/resources/data/basepower/loot_modifiers/vanilla_chest_parts.json create mode 100644 src/main/resources/data/forge/loot_modifiers/global_loot_modifiers.json diff --git a/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java b/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java index cc95c1be..abeb0358 100644 --- a/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java +++ b/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java @@ -79,7 +79,6 @@ public PowerAdvantage() { MinecraftForge.EVENT_BUS.addListener(ConduitRegistry.getInstance()::onChunkLoad); MinecraftForge.EVENT_BUS.addListener(ConduitRegistry.getInstance()::onChunkUnload); MinecraftForge.EVENT_BUS.addListener(RecipeDiscovery::onPlayerLogin); - MinecraftForge.EVENT_BUS.addListener(zone.moddev.mc.basepower.recipe.PartsLoot::onLoad); MinecraftForge.EVENT_BUS.addListener(zone.moddev.mc.basepower.init.Villages::onTrades); FMLJavaModLoadingContext.get().getModEventBus().addListener( zone.moddev.mc.basepower.recipe.MaterialTags::registerPack); @@ -88,6 +87,7 @@ public PowerAdvantage() { Fluids.init(); ItemGroups.register(FMLJavaModLoadingContext.get().getModEventBus()); RecipeConditions.register(FMLJavaModLoadingContext.get().getModEventBus()); + zone.moddev.mc.basepower.recipe.PartsLoot.register(FMLJavaModLoadingContext.get().getModEventBus()); migrateLegacyOreSpawnConfiguration(); migrateLegacyConfiguration(); 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 313b9352..c9e2ad61 100644 --- a/src/main/java/zone/moddev/mc/basepower/recipe/PartsLoot.java +++ b/src/main/java/zone/moddev/mc/basepower/recipe/PartsLoot.java @@ -1,48 +1,71 @@ package zone.moddev.mc.basepower.recipe; -import java.util.Arrays; -import java.util.HashSet; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.util.List; import java.util.Set; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.level.storage.loot.providers.number.ConstantValue; -import net.minecraft.world.level.storage.loot.LootPool; +import java.util.function.Predicate; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; -import net.minecraft.world.level.storage.loot.entries.NestedLootTable; -import net.minecraftforge.event.LootTableLoadEvent; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.storage.loot.LootContext; +import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; +import net.minecraftforge.common.loot.IGlobalLootModifier; +import net.minecraftforge.common.loot.LootModifier; +import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.fml.ModList; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; import zone.moddev.mc.basepower.PowerAdvantage; -/** Forge emits this event for bundled tables, leaving user data-pack tables untouched. */ -public final class PartsLoot { - private static final Set CHESTS = new HashSet<>(Arrays.asList( - "chests/abandoned_mineshaft", "chests/simple_dungeon", "chests/village/village_toolsmith", - "chests/stronghold_corridor", "chests/stronghold_crossing", "chests/end_city_treasure")); +/** Adds Advantage parts to the selected vanilla chest tables, not to data-pack replacements. */ +public final class PartsLoot extends LootModifier { + private static final Set CHESTS = Set.of("chests/abandoned_mineshaft", "chests/simple_dungeon", + "chests/village/village_toolsmith", "chests/stronghold_corridor", + "chests/stronghold_crossing", "chests/end_city_treasure"); + private static final List MODS = List.of("basepower", "basesteam", "baseelectric"); + private static final DeferredRegister> SERIALIZERS = + DeferredRegister.create(ForgeRegistries.Keys.GLOBAL_LOOT_MODIFIER_SERIALIZERS, PowerAdvantage.MODID); + private static final MapCodec CODEC = + RecordCodecBuilder.mapCodec(instance -> codecStart(instance).apply(instance, PartsLoot::new)); - private PartsLoot() {} + static { SERIALIZERS.register("vanilla_chest_parts", () -> CODEC); } - public static void onLoad(LootTableLoadEvent event) { - addPools(event.getTable(), event.getName(), PowerAdvantage.chestLootFactor > 0 && !hasAdditionalLootTables(), - mod -> ModList.get().isLoaded(mod)); - } + private PartsLoot(LootItemCondition[] conditions) { super(conditions); } + + public static void register(IEventBus bus) { SERIALIZERS.register(bus); } + + @Override public MapCodec codec() { return CODEC; } - static void addPools(net.minecraft.world.level.storage.loot.LootTable table, ResourceLocation name, - boolean enabled, java.util.function.Predicate loaded) { - if (!"minecraft".equals(name.getNamespace()) || !CHESTS.contains(name.getPath()) - || !enabled) return; - for (String mod : new String[] {"basepower", "basesteam", "baseelectric"}) { - if (!loaded.test(mod) - || (mod.equals("basepower") && name.getPath().equals("chests/end_city_treasure"))) continue; - String poolName = mod + ":legacy_parts"; - LootPool existing = table.removePool(poolName); - if (existing != null) { - table.addPool(existing); - continue; - } - table.addPool(LootPool.lootPool().name(poolName).setRolls(ConstantValue.exactly(1)) - .add(NestedLootTable.lootTableReference(ResourceKey.create(Registries.LOOT_TABLE, - new ResourceLocation(mod, "chests/advantage_parts")))).build()); + @Override + 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. + var resourceManager = context.getLevel().getServer().getResourceManager(); + String source = resourceManager.getResource(table.withPath(path -> "loot_tables/" + path + ".json")) + .map(Resource::sourcePackId).orElse(""); + for (ResourceLocation extra : partTables(table, source, ModList.get()::isLoaded)) { + var key = ResourceKey.create(Registries.LOOT_TABLE, extra); + context.getResolver().get(Registries.LOOT_TABLE, key) + .ifPresent(holder -> holder.value().getRandomItemsRaw(context, loot::add)); } + return loot; + } + + static List partTables(ResourceLocation table, String source, Predicate loaded) { + if (!isChest(table) || !"vanilla".equals(source)) return List.of(); + return MODS.stream() + .filter(mod -> loaded.test(mod) + && !(mod.equals("basepower") && table.getPath().equals("chests/end_city_treasure"))) + .map(mod -> ResourceLocation.fromNamespaceAndPath(mod, "chests/advantage_parts")).toList(); + } + + private static boolean isChest(ResourceLocation table) { + return table != null && "minecraft".equals(table.getNamespace()) && CHESTS.contains(table.getPath()); } private static boolean hasAdditionalLootTables() { diff --git a/src/main/resources/data/basepower/loot_modifiers/vanilla_chest_parts.json b/src/main/resources/data/basepower/loot_modifiers/vanilla_chest_parts.json new file mode 100644 index 00000000..92b3490e --- /dev/null +++ b/src/main/resources/data/basepower/loot_modifiers/vanilla_chest_parts.json @@ -0,0 +1,4 @@ +{ + "type": "basepower:vanilla_chest_parts", + "conditions": [] +} diff --git a/src/main/resources/data/forge/loot_modifiers/global_loot_modifiers.json b/src/main/resources/data/forge/loot_modifiers/global_loot_modifiers.json new file mode 100644 index 00000000..28ad1dde --- /dev/null +++ b/src/main/resources/data/forge/loot_modifiers/global_loot_modifiers.json @@ -0,0 +1,4 @@ +{ + "replace": false, + "entries": ["basepower:vanilla_chest_parts"] +} diff --git a/src/test/java/zone/moddev/mc/basepower/recipe/PartsLootTest.java b/src/test/java/zone/moddev/mc/basepower/recipe/PartsLootTest.java index 8714197c..60e4ef6a 100644 --- a/src/test/java/zone/moddev/mc/basepower/recipe/PartsLootTest.java +++ b/src/test/java/zone/moddev/mc/basepower/recipe/PartsLootTest.java @@ -1,42 +1,30 @@ package zone.moddev.mc.basepower.recipe; +import java.util.List; import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.level.storage.loot.LootTable; import org.junit.Test; import static org.junit.Assert.*; public class PartsLootTest { - @Test public void addsOnePoolPerLoadedModWithoutDuplicatingOnRepeat() { - LootTable table = LootTable.lootTable().build(); - ResourceLocation chest = new ResourceLocation("minecraft", "chests/simple_dungeon"); - PartsLoot.addPools(table, chest, true, mod -> true); - var original = table.removePool("basesteam:legacy_parts"); - assertNotNull(original); - table.addPool(original); - PartsLoot.addPools(table, chest, true, mod -> true); - assertSame(original, table.removePool("basesteam:legacy_parts")); - assertNull(table.removePool("basesteam:legacy_parts")); - assertNotNull(table.removePool("basepower:legacy_parts")); - assertNotNull(table.removePool("baseelectric:legacy_parts")); + @Test public void addsEachLoadedModOnceAndInTheExistingOrder() { + assertEquals(List.of(parts("basepower"), parts("basesteam"), parts("baseelectric")), + PartsLoot.partTables(id("minecraft:chests/simple_dungeon"), "vanilla", mod -> true)); } @Test public void keepsEndCityExceptionAndAbsentMods() { - LootTable table = LootTable.lootTable().build(); - PartsLoot.addPools(table, new ResourceLocation("minecraft", "chests/end_city_treasure"), true, - mod -> !mod.equals("basesteam")); - assertNull(table.removePool("basepower:legacy_parts")); - assertNull(table.removePool("basesteam:legacy_parts")); - assertNotNull(table.removePool("baseelectric:legacy_parts")); + assertEquals(List.of(parts("baseelectric")), PartsLoot.partTables( + id("minecraft:chests/end_city_treasure"), "vanilla", mod -> !mod.equals("basesteam"))); } - @Test public void leavesUnrelatedTablesAndDisabledLootUnchanged() { - for (String id : new String[] {"minecraft:chests/desert_pyramid", "othermod:chests/simple_dungeon"}) { - LootTable table = LootTable.lootTable().build(); - PartsLoot.addPools(table, new ResourceLocation(id), true, mod -> true); - assertNull(table.removePool("basepower:legacy_parts")); + @Test public void doesNotModifyUserPacksOtherModsOrUnrelatedTables() { + for (String source : List.of("file/chest-override", "mod:othermod", "")) { + assertTrue(PartsLoot.partTables(id("minecraft:chests/simple_dungeon"), source, mod -> true).isEmpty()); + } + for (String chest : List.of("minecraft:chests/desert_pyramid", "othermod:chests/simple_dungeon")) { + assertTrue(PartsLoot.partTables(id(chest), "vanilla", mod -> true).isEmpty()); } - LootTable table = LootTable.lootTable().build(); - PartsLoot.addPools(table, new ResourceLocation("minecraft", "chests/simple_dungeon"), false, mod -> true); - assertNull(table.removePool("basepower:legacy_parts")); } + + private static ResourceLocation parts(String mod) { return id(mod + ":chests/advantage_parts"); } + private static ResourceLocation id(String value) { return ResourceLocation.parse(value); } } From e6ecfb341006186d0adba9dcc3f296297cd34aec Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Tue, 10 Nov 2026 12:36:11 +0000 Subject: [PATCH 3/9] Use the Base Metals fix for legacy player inventories --- .github/workflows/ci.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- gradle.properties | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c2b8eb4..b2587964 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/BaseMetals - ref: 0c2cdd4d0e8c3bf2e5fa234d49e57960bbf80bde + ref: 9e706db5c85fa2bc7f36dee94f39a5f2fa8ab367 path: .ci-dependencies/BaseMetals - name: Install pinned Java 25 Mavenizer runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1e5d2415..8b363152 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/BaseMetals - ref: 0c2cdd4d0e8c3bf2e5fa234d49e57960bbf80bde + ref: 9e706db5c85fa2bc7f36dee94f39a5f2fa8ab367 path: .ci-dependencies/BaseMetals - name: Install pinned Java 25 Mavenizer runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 diff --git a/gradle.properties b/gradle.properties index b16e28bb..85cd8561 100644 --- a/gradle.properties +++ b/gradle.properties @@ -36,9 +36,9 @@ cf_optionals=additional-loot-tables cf_versions=1.20.6 basemetals_version=3.1.2.120061 -basemetals_source_commit=0c2cdd4d0e8c3bf2e5fa234d49e57960bbf80bde +basemetals_source_commit=9e706db5c85fa2bc7f36dee94f39a5f2fa8ab367 basemetals_curse_project_id=240967 -basemetals_deobf_sha256=FC6DE01771ED9CD799D946A0ED4B6782B289C47AA8CFCA7753F9E102DD0F0417 +basemetals_deobf_sha256=6186AC62C7E2E333373EA0D8AD8DE53FF859C520EBF568DFB47517C4F77C8C5D orespawn_version=4.1.0.120061 orespawn_source_commit=4085315345ff043448b316d75e03f4b8da45402d From 005c7b423559b0522814bfe45d00f425fca59175 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Tue, 10 Nov 2026 13:04:45 +0000 Subject: [PATCH 4/9] Run Advantage Works on Minecraft 1.20.6 --- test-rig/NATIVE1206.md | 91 +++++ test-rig/build.gradle | 119 ++++-- test-rig/client-resources/README.md | 22 +- test-rig/gradle.properties | 14 +- .../migration/audit_native1201_endurance.py | 31 +- .../migration/audit_native1206_endurance.py | 34 ++ test-rig/migration/native117_broad.py | 19 +- test-rig/migration/native117_recipes.py | 5 +- test-rig/migration/native1206.py | 378 ++++++++++++++++++ test-rig/migration/native1206_actors.py | 296 ++++++++++++++ test-rig/migration/native1206_broad.py | 118 ++++++ test-rig/migration/native1206_endurance.py | 225 +++++++++++ test-rig/migration/native1206_networks.py | 202 ++++++++++ test-rig/migration/native1206_recipes.py | 90 +++++ test-rig/migration/native1206_upgrade.py | 301 ++++++++++++++ .../test_audit_native1206_endurance.py | 34 ++ test-rig/migration/test_native1206.py | 165 ++++++++ .../test_native1206_actor_network_adapters.py | 107 +++++ .../NativeLeashKnotPlatformControl.java | 12 +- .../NativeWolfPlatformControl.java | 42 +- test-rig/profiles/native1206/dev-core.json | 19 + .../profiles/native1206/power-interop.json | 22 + .../moddev/mc/advantageworks/ActorProbe.java | 8 +- .../mc/advantageworks/AdvantageWorksMod.java | 3 +- .../BatteryOperationalCases.java | 2 +- .../mc/advantageworks/ConnectedProbe.java | 4 +- .../advantageworks/DimensionEntityProbe.java | 12 +- .../ElectricOperationalCases.java | 9 +- .../mc/advantageworks/LifecycleProbe.java | 8 +- .../mc/advantageworks/MachineContext.java | 12 +- .../MachineOperationalProbe.java | 6 +- .../mc/advantageworks/MigrationProbe.java | 2 +- .../mc/advantageworks/Native117Readiness.java | 2 +- .../Native117WorldgenAccess.java | 6 +- .../mc/advantageworks/NativeHarvestProbe.java | 6 +- .../mc/advantageworks/NativeInteropProbe.java | 39 +- .../advantageworks/NativeOilBucketProbe.java | 219 ++++++++++ .../advantageworks/NativeTicketGeometry.java | 2 +- .../advantageworks/PowerOperationalCases.java | 10 +- .../mc/advantageworks/ProcessProgression.java | 26 +- .../mc/advantageworks/ProgressionProbe.java | 61 +-- .../mc/advantageworks/RecipeBookProbe.java | 27 +- .../mc/advantageworks/RecipeUnlockProbe.java | 31 +- .../advantageworks/RedstoneReloadProbe.java | 6 +- .../SteamOperationalActors.java | 11 +- .../SteamOperationalSupplementalCases.java | 11 +- .../mc/advantageworks/WorksBlockData.java | 19 + .../mc/advantageworks/WorksController.java | 58 ++- .../advantageworks/WorksDefinitionLoader.java | 4 + .../mc/advantageworks/WorksItemData.java | 23 ++ .../mc/advantageworks/WorksOfflinePlayer.java | 40 ++ .../mc/advantageworks/WorksResultWriter.java | 2 +- .../src/main/resources/META-INF/mods.toml | 8 +- .../works/works-1.20.6.json | 6 + test-rig/src/main/resources/pack.mcmeta | 8 +- .../mc/advantageworks/runner/WorksRunner.java | 90 ++++- .../ConnectedProbeObservationTest.java | 32 +- .../Native117EntityContextTest.java | 7 +- .../advantageworks/Native117PlayerIoTest.java | 78 +++- .../Native117StoppedEntitiesSnapshotTest.java | 5 +- .../Native118EngineSignaturesTest.java | 7 +- .../Native1201PackMetadataTest.java | 6 +- .../Native1201TestFlatTest.java | 2 +- 63 files changed, 2950 insertions(+), 314 deletions(-) create mode 100644 test-rig/NATIVE1206.md create mode 100644 test-rig/migration/audit_native1206_endurance.py create mode 100644 test-rig/migration/native1206.py create mode 100644 test-rig/migration/native1206_actors.py create mode 100644 test-rig/migration/native1206_broad.py create mode 100644 test-rig/migration/native1206_endurance.py create mode 100644 test-rig/migration/native1206_networks.py create mode 100644 test-rig/migration/native1206_recipes.py create mode 100644 test-rig/migration/native1206_upgrade.py create mode 100644 test-rig/migration/test_audit_native1206_endurance.py create mode 100644 test-rig/migration/test_native1206.py create mode 100644 test-rig/migration/test_native1206_actor_network_adapters.py create mode 100644 test-rig/profiles/native1206/dev-core.json create mode 100644 test-rig/profiles/native1206/power-interop.json create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeOilBucketProbe.java create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksBlockData.java create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksItemData.java create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksOfflinePlayer.java create mode 100644 test-rig/src/main/resources/assets/advantageworkstest/works/works-1.20.6.json diff --git a/test-rig/NATIVE1206.md b/test-rig/NATIVE1206.md new file mode 100644 index 00000000..9ffc5848 --- /dev/null +++ b/test-rig/NATIVE1206.md @@ -0,0 +1,91 @@ +# Advantage Works on Forge 50 + +Run from this test-rig directory with a Java 21 `JAVA_HOME`. The parent +Mavenizer fixture supplies official names for compilation and runtime. The +harness manifest records the ordinary Power compile-input SHA-256, and both +launchers check it against the runtime pin. No Renamer is used. A run is +evidence for its specific profile and checks, not permission to publish. + +```powershell +../gradlew.bat jar runnerJar test verifyHarnessIsolation verifyForge50UnitBootstrap +$power = (Resolve-Path ../build/libs/basePower-2.4.13.120061.jar).Path +$powerSha = (Get-FileHash -LiteralPath $power).Hash +$harness = (Resolve-Path build/gradle/libs/AdvantageWorksTestHarness-1.1.4.120061.jar).Path +$harnessSha = (Get-FileHash -LiteralPath $harness).Hash +``` + +Use `-PpowerAdvantageReleaseJar=` for an immutable copy of the pinned +jar. To try a different Power build, also pass its +`-PworksExploratoryPowerSha256=`; that run is exploratory. + +The plain JUnit agent supplies an empty Forge loader fixture and uses Forge's +native capability and registry machinery. It is never used in a real server. +The root test project already supplies the same agent and support sources. + +## Live Power and Base Metals + +Set `$python` to a Python 3.11+ executable with `migration/requirements.txt`, +and `$seed` to the official Forge 50.2.0 runtime-server directory. Seal the +read-only seed once, then use a fresh output path and unused localhost port. + +```powershell +$inventory = "build/native1206-code-inventory.json" +& $python migration/native1206.py seal --seed $seed --inventory $inventory +$seedSha = (Get-FileHash -LiteralPath $inventory).Hash +& $python migration/native1206.py interop --seed $seed --inventory $inventory --inventory-sha256 $seedSha --java "$env:JAVA_HOME/bin/java.exe" --power $power --power-sha256 $powerSha --harness $harness --harness-sha256 $harnessSha --port 25837 --output build/native1206-interop-local +``` + +The live profile uses the local Base Metals shared-hook candidate and pinned +OreSpawn. It requires all original Forge Energy, tank, conveyor and callback +checks plus native Base Metals shared crushing before and after restart. +Unplaced callback checks do not prove physical chunk unload. The server run +does not establish client acceptance or migration qualification. + +Add `--oil-buckets` to exercise both real oil buckets through native survival +world use and tank interaction. The offline actor records a GUI fallback +without attempting to send a packet to a client that does not exist. + +## Combined Profiles + +`native1206.py profile --name ` copies the existing station and worldgen +expectations into a new pinned profile under `build`. Pass the same Power and +harness arguments, plus `--steam`, `--electric` and their SHA-256 pins. +Mineralogy profiles also need `--mineralogy` and `--mineralogy-sha256`. Pass +`-PworksProfileFile`, `-PworksServerSeed`, `-PworksServerInventory`, and +`-PworksServerInventorySha256` to the matching Gradle task. +`verifyWorksNative1206Inputs` checks custody without opening a world. + +The Python broad and endurance runners consume the same frozen family +profile. Use separate fresh output paths and non-overlapping localhost ports. + +```powershell +$common = @('--profile', $profile, '--seed', $seed, '--inventory', $inventory, '--inventory-sha256', $seedSha, '--java', "$env:JAVA_HOME/bin/java.exe") +& $python migration/native1206_broad.py @common --port 25910 --output build/native1206-broad-local +& $python migration/native1206_endurance.py @common --port 25920 --seconds 1800 --output build/native1206-endurance-local +``` + +Broad defaults to both shared and decoupled profiles, on consecutive ports. +Endurance is fresh by default. Add `--retained` and +`--retained-receipt-sha256` for a passing upgrade on the same exact family +profile and inventory; this also uses the following port. A shorter +whole-minute run reports `SMOKE_PASS_NOT_ENDURANCE`. +Creating `hold-endurance.request` in an active endurance output directory +requests normal shutdown at the next safe boundary and records `HELD`. + +## Strict Upgrade Lanes + +Use `migration/native1206_upgrade.py` with the materialized family profile, +official seed, sealed inventory, Java 21, and a new output directory. For a +modern upgrade, use `--source` pointing at a stopped 1.20.1 runtime with a +passing modern upgrade receipt, pinned by `--receipt-sha256`. For a direct +historical upgrade, use `--source` pointing at the original fixture and +`--legacy-reference` pointing at its independent 1.20.1 run. The driver +requires the historical source's full fingerprint to match that receipt. + +The upgrade gates compare every machine witness, inspect native on-disk +block and tile data, verify the turbine, restart without reseeding, and +exercise the conveyor, crusher, and furnace on a further copy. A log error +fails the run even when the captured state matches. Other historical +`native1201_*.py` drivers still target Forge 47. Forge 50 does not natively +retain opaque `ForgeData` on vanilla block entities, so connected migration +is not replaced with a permissive comparison. diff --git a/test-rig/build.gradle b/test-rig/build.gradle index ad5fa8ab..a2ba6656 100644 --- a/test-rig/build.gradle +++ b/test-rig/build.gradle @@ -2,12 +2,11 @@ import org.apache.tools.ant.filters.FixCrLfFilter plugins { id 'java' - id 'net.minecraftforge.renamer' version '1.1.5' id 'net.minecraftforge.gradle' version '7.0.34' } -ext.forge47MavenizerDefault = '../../../../MinecraftMineralogy/OS1201/OreSpawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar' -apply from: '../gradle/forge47-mavenizer.gradle' +ext.forge50MavenizerDefault = '../../../../MinecraftMineralogy/OS1206/OreSpawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar' +apply from: '../gradle/forge50-mavenizer.gradle' // Saves, pinned inputs and evidence are siblings, never Gradle cleanup targets. layout.buildDirectory = layout.projectDirectory.dir('build/gradle') @@ -26,16 +25,17 @@ version = project.harness_version base.archivesName = 'AdvantageWorksTestHarness' java { - toolchain.languageVersion = JavaLanguageVersion.of(17) + toolchain.languageVersion = JavaLanguageVersion.of(21) } tasks.withType(JavaCompile).configureEach { javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) } - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 options.encoding = 'UTF-8' + options.compilerArgs.addAll(['-Xmaxerrs', '1000']) } tasks.withType(AbstractArchiveTask).configureEach { @@ -69,8 +69,10 @@ repositories { } } -def powerCompileJar = file(project.findProperty('powerAdvantageDeobfJar') - ?: '../build/libs-dev/basePower-2.4.12.120011-deobf.jar') +def powerCompileJar = file(project.findProperty('powerAdvantageReleaseJar') + ?: '../build/libs/basePower-2.4.13.120061.jar') +def exploratoryPowerSha = providers.gradleProperty('worksExploratoryPowerSha256') +def powerCompileSha = exploratoryPowerSha.orElse(providers.gradleProperty('works_power_sha256')) dependencies { implementation minecraft.dependency( @@ -102,19 +104,19 @@ tasks.register('verifyWorksCompileInputPin') { group = 'verification' description = 'Checks the exact parent-supplied Power API artifact before harness compilation.' inputs.file(powerCompileJar) - inputs.property('sha256', project.poweradvantage_deobf_sha256) - inputs.property('sourceCommit', project.poweradvantage_source_commit) + inputs.property('powerSha256', powerCompileSha.orElse('')) doLast { - if (!powerCompileJar.isFile()) throw new GradleException("Missing 1.20.1 Power compile input: ${powerCompileJar}") + if (!powerCompileJar.isFile()) throw new GradleException("Missing 1.20.6 Power compile input: ${powerCompileJar}") + if (!(powerCompileSha.getOrElse('') ==~ /[A-Fa-f0-9]{64}/)) { + throw new GradleException('Missing or invalid Power SHA-256 pin') + } String actual = java.security.MessageDigest.getInstance('SHA-256') .digest(powerCompileJar.bytes).encodeHex().toString().toUpperCase(Locale.ROOT) - if (actual != project.poweradvantage_deobf_sha256) { + if (!actual.equalsIgnoreCase(powerCompileSha.get())) { throw new GradleException("Power compile input changed: ${actual}; obtain a new parent pin") } - if (project.poweradvantage_source_commit != '83a3f028ffad220fe2f3cc4983a9340dde2295a8' - || project.poweradvantage_pin_status != 'SOURCE_AND_ARTIFACT_PINNED') { - throw new GradleException('The harness requires the verified 1.20.1 Power source pin') - } + String status = exploratoryPowerSha.isPresent() ? 'EXPLORATORY_UNQUALIFIED' : 'Pinned' + logger.lifecycle("${status} Power compile/runtime input: ${actual}") } } @@ -141,13 +143,13 @@ tasks.named('processResources', ProcessResources) { } tasks.named('jar', Jar) { - archiveClassifier = 'deobf' - destinationDirectory = layout.buildDirectory.dir('libs-dev') manifest { attributes([ 'Implementation-Title': base.archivesName.get(), 'Implementation-Version': project.version, - 'Built-On': "${project.minecraft_version}-${project.forge_version}" + 'Built-On': "${project.minecraft_version}-${project.forge_version}", + 'Power-Input-Status': 'EXPLORATORY_UNQUALIFIED', + 'Power-Input-SHA256': powerCompileSha.getOrElse('').toUpperCase(Locale.ROOT) ]) } } @@ -158,11 +160,6 @@ tasks.named('processRunnerResources', ProcessResources) { } } -def releaseJar = renamer.classes(tasks.named('jar', Jar)) { - map.from minecraft.dependency.toSrgFile - archiveClassifier = null -} - tasks.register('runnerJar', Jar) { dependsOn tasks.named('runnerClasses') archiveClassifier = 'runner' @@ -171,21 +168,42 @@ tasks.register('runnerJar', Jar) { } tasks.named('assemble') { - dependsOn releaseJar dependsOn tasks.named('runnerJar') } +def configurePreparedSeedArgs = { JavaExec task -> + def options = [ + worksServerSeed: '--prepared-server-seed', + worksServerInventory: '--prepared-server-seed-inventory', + worksServerInventorySha256: '--prepared-server-seed-sha256' + ] + options.each { key, option -> + if (project.hasProperty(key)) { + String value = project.property(key).toString() + task.args option, key.endsWith('Sha256') ? value : file(value).absolutePath + } + } +} + def registerWorksRunner = { String taskName, String profileName, boolean automated, String taskDescription -> tasks.register(taskName, JavaExec) { group = 'verification' description = taskDescription dependsOn tasks.named('jar') - dependsOn releaseJar dependsOn tasks.named('runnerClasses') classpath = sourceSets.runner.runtimeClasspath mainClass = 'zone.moddev.mc.advantageworks.runner.WorksRunner' if (!automated) standardInput = System.in - args '--profile', file("profiles/native1201/${profileName}.json").absolutePath + 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") + File profile = file(pin) + if (new groovy.json.JsonSlurper().parse(profile).name != profileName) { + throw new GradleException("Expected profile ${profileName}: ${profile}") + } + args '--profile', profile.absolutePath + configurePreparedSeedArgs(task) + } if (automated) args '--automated' if (project.hasProperty('worksStations')) { args '--stations', project.property('worksStations').toString() @@ -200,7 +218,7 @@ def registerWorksRunner = { String taskName, String profileName, boolean automat } registerWorksRunner('runWorksDev', 'dev-core', false, - 'Builds and launches a disposable interactive 1.20.1 Advantage Works development server.') + 'Builds and launches a disposable interactive 1.20.6 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, @@ -224,22 +242,25 @@ registerWorksRunner('verifyWorksWorldgenLegacyImport', 'worldgen-legacy-import', registerWorksRunner('runWorksPackagedCurrent', 'packaged-current', false, 'Launches the complete packaged works on localhost for manual inspection.') -tasks.register('verifyWorksNative1201Inputs', JavaExec) { +tasks.register('verifyWorksNative1206Inputs', JavaExec) { group = 'verification' - description = 'Checks 1.20.1 candidate identities and hashes without creating or opening a world.' - dependsOn tasks.named('runnerClasses'), tasks.named('jar'), releaseJar + description = 'Checks 1.20.6 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' - args '--profile', file("profiles/native1201/${project.findProperty('worksProfile') ?: 'packaged-current'}.json").absolutePath, - '--validate-only' + doFirst { JavaExec task -> + String pin = providers.gradleProperty('worksProfileFile').orNull + if (!pin) throw new GradleException('Set -PworksProfileFile to a generated Forge 50 profile') + args '--profile', file(pin).absolutePath, '--validate-only' + configurePreparedSeedArgs(task) + } } tasks.register('verifyHarnessIsolation') { group = 'verification' description = 'Confirms the PowerAdvantage release contains no test-harness content.' doLast { - File powerJar = file(project.findProperty('powerAdvantageReleaseJar') - ?: '../build/libs/basePower-2.4.12.120011.jar') + File powerJar = powerCompileJar if (!powerJar.isFile()) { throw new GradleException("Build PowerAdvantage first: ${powerJar}") } @@ -261,17 +282,32 @@ tasks.named('check') { } def nativeControlClasspath = configurations.compileClasspath.minus(files(powerCompileJar)) +def compileUnitSupport = tasks.register('compileUnitSupport', JavaCompile) { + source fileTree('unit-support') + classpath = nativeControlClasspath + destinationDirectory = layout.buildDirectory.dir('unit-support/classes') +} +tasks.register('verifyForge50UnitBootstrap', JavaExec) { + group = 'verification' + dependsOn compileUnitSupport, unitBootstrapJar + classpath = files(compileUnitSupport.flatMap { it.destinationDirectory }) + nativeControlClasspath + mainClass = 'zone.moddev.mc.advantageworks.unit.Forge50UnitControl' + javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(21) } + doFirst { jvmArgs "-javaagent:${unitBootstrapJar.get().archiveFile.get().asFile}" } +} def compilePlatformControls = tasks.register('compilePlatformControls', JavaCompile) { source fileTree('platform-controls') - classpath = nativeControlClasspath + dependsOn compileUnitSupport + classpath = nativeControlClasspath + files(compileUnitSupport.flatMap { it.destinationDirectory }) destinationDirectory = layout.buildDirectory.dir('platform-controls/classes') } tasks.register('verifyNativeWolfPlatform', JavaExec) { group = 'verification' dependsOn compilePlatformControls, unitBootstrapJar - classpath = files(compilePlatformControls.flatMap { it.destinationDirectory }) + nativeControlClasspath + classpath = files(compilePlatformControls.flatMap { it.destinationDirectory }, + compileUnitSupport.flatMap { it.destinationDirectory }) + nativeControlClasspath mainClass = 'NativeWolfPlatformControl' - javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) } + javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(21) } doFirst { def output = providers.gradleProperty('nativeControlOutput').orNull if (!output || file(output).exists()) throw new GradleException('Supply a new -PnativeControlOutput directory') @@ -283,9 +319,10 @@ tasks.register('verifyNativeWolfPlatform', JavaExec) { tasks.register('verifyNativeLeashKnotPlatform', JavaExec) { group = 'verification' dependsOn compilePlatformControls, unitBootstrapJar - classpath = files(compilePlatformControls.flatMap { it.destinationDirectory }) + nativeControlClasspath + classpath = files(compilePlatformControls.flatMap { it.destinationDirectory }, + compileUnitSupport.flatMap { it.destinationDirectory }) + nativeControlClasspath mainClass = 'NativeLeashKnotPlatformControl' - javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) } + javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(21) } doFirst { def output = providers.gradleProperty('nativeControlOutput').orNull if (!output || file(output).exists()) throw new GradleException('Supply a new -PnativeControlOutput directory') diff --git a/test-rig/client-resources/README.md b/test-rig/client-resources/README.md index baa3dac2..8919f8ea 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.1 / Forge 47.4.10 client checks models, textures, +This disposable Minecraft 1.20.6 / Forge 50.2.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.1/PORT-STATUS.md) for current qualification. +See [port status](../../docs/1.20.6/PORT-STATUS.md) for current qualification. Earlier-version results do not qualify this version. -Use Java 17 for Gradle, compilation and the client and server runners. The -target is official 1.20.1 mappings with MCP `20230612.114412`. +Use Java 21 for Gradle, compilation and the client and server runners. The +target is official 1.20.6 mappings with MCP `20240429.135109`. ## 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.12.120011`, Steam `2.2.13.120011` and -Electric `2.2.13.120011`. Pin the exact rebuilt jars in `profiles/native1201` +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` before regenerating it. Omitting the combined profile retains Power-only scope. Normal Eclipse launches contain production code only. @@ -34,15 +34,15 @@ does not claim a graphical pass. The default `-OutputSource Gradle` snapshots compiled classes and processed resources. `-OutputSource Eclipse` copies `bin/main` without overlaying Gradle -resources. `-PowerCandidate` selects an explicit native deobf jar; its hash +resources. `-PowerCandidate` selects an explicit native player jar; its hash is provenance, not proof that the selected loose output equals that jar. ## Recipe Fixtures -Generate fresh exports from the packaged 1.20.1 candidates, not a historical run: +Generate fresh exports from the packaged 1.20.6 candidates, not a historical run: ```powershell - -B test-rig/migration/native1201_recipes.py --profile test-rig/profiles/native1201/packaged-current.json --seed '' --inventory '' --inventory-sha256 '' --java17 '/bin/java.exe' --port 25577 --output test-rig/build/native1201-recipes-new + -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 ``` Use an existing Python 3.11+ environment with `migration/requirements.txt`. diff --git a/test-rig/gradle.properties b/test-rig/gradle.properties index 89188da2..1215860f 100644 --- a/test-rig/gradle.properties +++ b/test-rig/gradle.properties @@ -1,12 +1,10 @@ org.gradle.jvmargs=-Xmx2G org.gradle.daemon=false -minecraft_version=1.20.1 -forge_version=47.4.10 +minecraft_version=1.20.6 +forge_version=50.2.0 mapping_channel=official -mapping_version=1.20.1 -mcp_version=20230612.114412 -harness_version=1.1.4.120011 -poweradvantage_deobf_sha256=21A2A3007BA076D219E6836707EF365E2031823A09D5E3DC2A92DD88BD67EA01 -poweradvantage_source_commit=83a3f028ffad220fe2f3cc4983a9340dde2295a8 -poweradvantage_pin_status=SOURCE_AND_ARTIFACT_PINNED +mapping_version=1.20.6 +mcp_version=20240429.135109 +harness_version=1.1.4.120061 +works_power_sha256=29C1BD5EF384F8BB2441029061615E52A4B7302B9FC5480F2CE9C84C357A3181 diff --git a/test-rig/migration/audit_native1201_endurance.py b/test-rig/migration/audit_native1201_endurance.py index 119c6dff..fb32cecd 100644 --- a/test-rig/migration/audit_native1201_endurance.py +++ b/test-rig/migration/audit_native1201_endurance.py @@ -13,11 +13,16 @@ import native1201 -def replay(run, profile, specification): - report = read_json(run / "native1201-endurance-results.json") - modes = ("fresh1201", "retained-upgrade-native1201") +def replay(run, profile, specification, *, adapter=native1201): + suffix = adapter.MINECRAFT.replace(".", "") + receipt = run / f"native{suffix}-endurance-results.json" + report = read_json(receipt) + modes = (f"fresh{suffix}", f"retained-upgrade-native{suffix}") + parse = getattr(adapter, "parse_snapshot", endurance.parse_snapshot) + verify_batch = getattr(adapter, "verify_batch", None) require = endurance.require - require(report["status"] == "PASS" and report["minecraft"] == native1201.MINECRAFT + recorded_forge = report.get("forge", native1201.FORGE if adapter is native1201 else None) + require(report["status"] == "PASS" and (report["minecraft"], recorded_forge) == (adapter.MINECRAFT, adapter.FORGE) and report["duration_seconds"] == 1800 and report["interval_seconds"] == 60 and report["retained_unchanged"] is True and set(report["profiles"]) == set(modes), "Expected a complete fresh and migrated 30-minute run") @@ -42,22 +47,22 @@ def replay(run, profile, specification): for mode in modes: runtime = run / mode station_path = runtime / "advantage-works-results" - native1201.verify_runtime(runtime) + adapter.verify_runtime(runtime) require({jar.name: digest(jar) for jar in (runtime / "mods").glob("*.jar")} == expected_jars, mode + ": staged artifacts differ from the approved profile") for boot in ("endurance", "endurance-restart"): - native1201.require_native_boot(runtime / f"console-{boot}.log") + adapter.require_native_boot(runtime / f"console-{boot}.log") rig = endurance.Rig(mode, runtime, None, None, definitions) def snapshot(station, label): document = read_json(station_path / f"{station}-checkpoint-{label}.json") - require(document["minecraft"] == native1201.MINECRAFT and document["forge"] == native1201.FORGE + require(document["minecraft"] == adapter.MINECRAFT and document["forge"] == adapter.FORGE and all(document["mods"].get(key) == value for key, value in mod_versions.items()), mode + ": checkpoint runtime differs from profile") return document setup = snapshot("E-02", "setup") - rig.static_batteries = endurance.parse_snapshot(setup)[rig.pos("E-02", (3, 1, 12))]["data"]["Items"] + rig.static_batteries = parse(setup)[rig.pos("E-02", (3, 1, 12))]["data"]["Items"] ledger = read_json(runtime / "endurance-ledger.json") require([row["label"] for row in ledger] == labels, mode + ": missing or misordered checkpoints") times = {} @@ -66,7 +71,9 @@ def snapshot(station, label): label = row["label"] for station in endurance.CHECKED: document = snapshot(station, label) - rig.documents[station], rig.snapshots[station] = document, endurance.parse_snapshot(document) + rig.documents[station], rig.snapshots[station] = document, parse(document) + if verify_batch: + verify_batch(runtime, label, rig.documents, definitions) with patch.object(endurance, "write_json"): verified = rig.batch(label, power="basepower") require(verified == row, mode + "/" + label + ": raw result differs from the reported ledger") @@ -91,10 +98,10 @@ def snapshot(station, label): results[mode] = dict(checkpoints=len(ledger), stationSnapshots=len(ledger) * len(endurance.CHECKED), setupToEndTicks=ticks, sampledTicks=sampled_ticks, sampledSeconds=sampled_seconds, sampledTicksPerSecond=sampled_ticks / sampled_seconds, outputs=dict(totals)) - return dict(status="PASS", inputReceiptSha256=digest(run / "native1201-endurance-results.json"), profiles=results) + return dict(status="PASS", inputReceiptSha256=digest(receipt), profiles=results) -def main(): +def main(*, adapter=native1201): parser = argparse.ArgumentParser(description=__doc__) for name in ("run", "profile", "specification", "output"): parser.add_argument("--" + name, required=True, type=Path) @@ -103,7 +110,7 @@ def main(): require_disjoint(args.output, path) if args.output.exists(): raise FileExistsError(args.output) - write_json(args.output, replay(args.run, args.profile, args.specification)) + write_json(args.output, replay(args.run, args.profile, args.specification, adapter=adapter)) print("PASS: both 30-minute ledgers replayed from native station snapshots") diff --git a/test-rig/migration/audit_native1206_endurance.py b/test-rig/migration/audit_native1206_endurance.py new file mode 100644 index 00000000..51a98df9 --- /dev/null +++ b/test-rig/migration/audit_native1206_endurance.py @@ -0,0 +1,34 @@ +"""Independently replay each saved checkpoint from the full Forge 50 endurance run.""" + +import endurance +import native1206 +from audit_native1201_endurance import main +from native117_broad import read_json +from native1206_endurance import measure_batch + + +class Forge50: + MINECRAFT, FORGE = native1206.MINECRAFT, native1206.FORGE + verify_runtime = staticmethod(native1206.audit_staged) + parse_snapshot = staticmethod(native1206.parse_endurance_snapshot) + + @staticmethod + def require_native_boot(path): + native1206.require_clean_log(path) + text = path.read_text(encoding="utf-8") + for marker in (f"Starting minecraft server version {native1206.MINECRAFT}", + f"Forge mod loading, version {native1206.FORGE}, for MC {native1206.MINECRAFT} " + f"with MCP {native1206.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=Forge50) diff --git a/test-rig/migration/native117_broad.py b/test-rig/migration/native117_broad.py index 05f0d308..19366565 100644 --- a/test-rig/migration/native117_broad.py +++ b/test-rig/migration/native117_broad.py @@ -51,14 +51,14 @@ def nbt(row): return plain(nbtlib.parse_nbt(row["nbt"])) if "nbt" in row else {} -def drill_state(station, document, *, initial=False): +def drill_state(station, document, *, initial=False, unpack=nbt): rows = {tuple(row["pos"]): row for row in document["snapshot"]} if len(rows) != len(document["snapshot"]): raise AssertionError("Duplicate snapshot positions") cases = drill_cases(station) stock = Counter() for row in rows.values(): - for stack in nbt(row).get("Items", []): + for stack in unpack(row).get("Items", []): marker = stack.get("tag", {}).get("AdvBayStock") if marker: if stack.get("id") != "minecraft:diamond": @@ -80,7 +80,7 @@ def drill_state(station, document, *, initial=False): raise AssertionError(case["id"] + ": initial declared rear track is missing") if "facing=" + case["facing"] not in row["state"]: raise AssertionError(case["id"] + ": block facing changed") - data = nbt(row) + data = unpack(row) if data.get("ForgeData", {}).get("AdvBayWitness") != case["id"]: raise AssertionError(case["id"] + ": persistent moved tile witness lost/replaced") if tuple(int(data[axis]) for axis in ("x", "y", "z")) != pos: @@ -236,11 +236,14 @@ class NativeBroadServer(BroadCommands, native_adapter.Server): if name != "packaged-decoupled" or pin["id"] != "basemetals"} expected_mods.update(versions) result["expectedMods"] = expected_mods + unpack = getattr(native_adapter, "drill_data", nbt) server = None try: verify() server = server_type(runtime, java, launcher, "first") server.command("advworks build all", "Advantage Works built 21 station(s)") + if hasattr(native_adapter, "drill_tickets"): + native_adapter.drill_tickets(server, runtime, stations, add=True) # Named finite stock and tile data are commissioning witnesses, not repairs # performed after operation or restart. Every destination is unchanged later. for station in stations: @@ -251,11 +254,15 @@ class NativeBroadServer(BroadCommands, native_adapter.Server): marker = json.dumps(case["id"]) tag = ('{ForgeData:{AdvBayWitness:' + marker + '},Items:[{Slot:0b,id:"minecraft:diamond",' 'Count:7b,tag:{AdvBayStock:' + marker + '}}]}') + if hasattr(native_adapter, "drill_tag"): + tag = native_adapter.drill_tag(marker) server.command(f"data merge block {pos} {tag}", "Modified block data of") initial = {} for station in stations: if station["id"] in DRILL_BAYS: - initial.update(drill_state(station, server.checkpoint(station["id"], "initial"), initial=True)) + initial.update(drill_state(station, server.checkpoint(station["id"], "initial"), initial=True, unpack=unpack)) + if hasattr(native_adapter, "drill_tickets"): + native_adapter.drill_tickets(server, runtime, stations, add=False) result["phases"]["initial"] = dict(status="PASS", drills=initial) write_json(runtime / "broad-results.json", result) for station in stations: @@ -270,7 +277,7 @@ class NativeBroadServer(BroadCommands, native_adapter.Server): before = {} for station in stations: if station["id"] in DRILL_BAYS: - before.update(drill_state(station, first[station["id"]])) + before.update(drill_state(station, first[station["id"]], unpack=unpack)) result["phases"]["operation"] = operating_state(initial, before) result["beforeRestart"] = before server.close() @@ -286,7 +293,7 @@ class NativeBroadServer(BroadCommands, native_adapter.Server): after = {} for station in stations: if station["id"] in DRILL_BAYS: - after.update(drill_state(station, second[station["id"]])) + after.update(drill_state(station, second[station["id"]], unpack=unpack)) result["afterRestart"] = after result["phases"]["persistence"] = restarted_state(before, after) server.close() diff --git a/test-rig/migration/native117_recipes.py b/test-rig/migration/native117_recipes.py index 74d28648..fb9b9d4c 100644 --- a/test-rig/migration/native117_recipes.py +++ b/test-rig/migration/native117_recipes.py @@ -41,7 +41,7 @@ def read_json(path): return json.loads(path.read_text(encoding="utf-8-sig")) -def recipe_fixture(jars): +def recipe_fixture(jars, *, component_predicates=False): recipes = [] sources = {} for path in jars: @@ -66,6 +66,9 @@ def recipe_fixture(jars): raise AssertionError(f"Unsupported inventory predicate: {advancement_name}/{key}") if "items" in items[0]: choices = items[0]["items"] + if component_predicates and isinstance(choices, str) and choices.startswith("#") and len(choices) > 1: + predicates[key] = {"tag": choices[1:]} + continue if (not isinstance(choices, list) or len(choices) != 1 or not isinstance(choices[0], str) or not choices[0]): raise AssertionError(f"Expected one explicit native item: {advancement_name}/{key}") diff --git a/test-rig/migration/native1206.py b/test-rig/migration/native1206.py new file mode 100644 index 00000000..c1f7d765 --- /dev/null +++ b/test-rig/migration/native1206.py @@ -0,0 +1,378 @@ +"""Run the Forge 50 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.20.6" +FORGE = "50.2.0" +MCP = "20240429.135109" +FORGE_VERSION = f"{MINECRAFT}-{FORGE}" +SHIM = f"forge-{FORGE_VERSION}-shim.jar" +SHIM_SHA = "7BECDDA6D36C8359EADA6018C2EDB21C5914C50028C191BE09A39FF9F516E10C" +LAUNCHER = f"libraries/net/minecraftforge/forge/{FORGE_VERSION}/" + ( + "win_args.txt" if os.name == "nt" else "unix_args.txt") +INVENTORY = "native1206-code-inventory.json" +EXPLORATORY = "EXPLORATORY_UNQUALIFIED" +BASE_PROFILE = RIG / "profiles/native1206/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.20.6 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 50 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 50 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 50 packaged profile") + mods = { + "basepower": ("2.4.13.120061", args.power, args.power_sha256), + "advantageworkstest": ("1.1.4.120061", 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(".120061") or not path.name.endswith(f"-{version}.jar"): + raise ValueError(f"Expected an ordinary Forge 50 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 50 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(".120061") 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) + if base.get("name") != args.name or base.get("minecraftVersion") != "1.20.1": + 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.20.6 jar and exact SHA-256 for {identity}") + version = path.stem.rsplit("-", 1)[-1] + if not version.endswith(".120061"): + 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-native1206", 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 50 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 / "native1206-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/native1206_actors.py b/test-rig/migration/native1206_actors.py new file mode 100644 index 00000000..2a2e0aae --- /dev/null +++ b/test-rig/migration/native1206_actors.py @@ -0,0 +1,296 @@ +"""Check sealed historical player and dimension fixtures on Forge 50.""" + +import argparse +import builtins +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 +import native117_entity_storage as entity_storage +import native117_helper_contract as helper_contract +import native117_stopped_entities as stopped +import native1206 as native + + +DATA_VERSION = 3839 +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): + return dimension_entity_view(parse_dimension_nbt(entry)) + + +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)) +helper_adapter = SimpleNamespace(CONTRACT=helper_contract.CONTRACT, 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): + # The prior no-Power wolf control belongs to an older engine and is not evidence for Forge 50. + return dict(status=strict["status"], rawStrictStatus=strict["status"], rawStrictResult=strict, + nativeDifferences=[]) + + +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, + 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("--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 / "native1206-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" + 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"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native1206_broad.py b/test-rig/migration/native1206_broad.py new file mode 100644 index 00000000..7699da71 --- /dev/null +++ b/test-rig/migration/native1206_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 native1206 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 / "native1206-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/native1206_endurance.py b/test-rig/migration/native1206_endurance.py new file mode 100644 index 00000000..31295724 --- /dev/null +++ b/test-rig/migration/native1206_endurance.py @@ -0,0 +1,225 @@ +"""Run the measured Works feed, conservation and cold-restart protocol on Forge 50.""" + +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 native1206 as native +from native1206_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 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("native1206-*-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 / "native1206-endurance-results.json" + write_json(receipt, report) + rigs = [] + try: + cases = (("fresh1206", None), ("retained-upgrade-native1206", args.retained)) if args.retained else (("fresh1206", 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/native1206_networks.py b/test-rig/migration/native1206_networks.py new file mode 100644 index 00000000..15a13285 --- /dev/null +++ b/test-rig/migration/native1206_networks.py @@ -0,0 +1,202 @@ +"""Run physical chunk unload, bridge, drill and interop oracles on Forge 50.""" + +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 native1206 as native + + +MAX_TICKET_DISTANCE = 12 +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.20.6 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"native1206-{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/native1206_recipes.py b/test-rig/migration/native1206_recipes.py new file mode 100644 index 00000000..8e2a7aaf --- /dev/null +++ b/test-rig/migration/native1206_recipes.py @@ -0,0 +1,90 @@ +"""Exercise live recipe discovery and progression against pinned Forge 50 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 native1206 + + +class RecipeServer(RecipePruning, native1206.Server): + @staticmethod + def errors(text): + return native1201.log_findings(text)[0] + + +class Engine: + version = (native1206.MINECRAFT, native1206.FORGE) + pack_format = 41 + server = native1206.Server + recipe_server = RecipeServer + result_name = "native1206-recipes-results.json" + + @staticmethod + def stage(runtime, target, port, campaign): + native1206.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 native1206.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 = native1206.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) + 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=native1206.MINECRAFT, forge=native1206.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 50 unlock campaign") + native1206.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 50 progression campaign") + native1206.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/native1206_upgrade.py b/test-rig/migration/native1206_upgrade.py new file mode 100644 index 00000000..0f6a929a --- /dev/null +++ b/test-rig/migration/native1206_upgrade.py @@ -0,0 +1,301 @@ +"""Strict shared family upgrade using immutable historical or 1.20.1 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 native1206 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 = 3839 +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.20.6 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 / "native1206-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.20.6") + 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.20.6 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 "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 1.20.1 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"native1206-{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") + 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_native1206_endurance.py b/test-rig/migration/test_audit_native1206_endurance.py new file mode 100644 index 00000000..a6747bd1 --- /dev/null +++ b/test-rig/migration/test_audit_native1206_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_native1206_endurance import Forge50 + + +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_native1206_endurance.measure_batch", return_value=measured) as measure: + Forge50.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"): + Forge50.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"): + Forge50.verify_batch(runtime, "cycle-001", documents, definitions) diff --git a/test-rig/migration/test_native1206.py b/test-rig/migration/test_native1206.py new file mode 100644 index 00000000..e47f68ca --- /dev/null +++ b/test-rig/migration/test_native1206.py @@ -0,0 +1,165 @@ +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 native1206 as native +import semantic_machine_audit +import test_semantic_machine_audit +from native117_identity import target_id +from native1206_upgrade import comparison, components +from native1206_broad import Adapter +from native1206_endurance import EnduranceHold, Rig, check_hold, measure_batch + + +class Native1206Test(unittest.TestCase): + 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.20.6 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.20.6 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("native1206_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("native1206_upgrade.parse_nbt", side_effect=[{"Items": [before]}, {"Items": [extra]}]): + self.assertEqual("FAIL", comparison([witness], [witness], modern=True)["status"]) + with patch("native1206_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.13.120061", "advantageworkstest": "1.1.4.120061", "orespawn": "4.1.0.120061"} + 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.14.120061.jar"), steam_sha256=sha, + electric=Path("baseElectric-2.2.14.120061.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.20.6", document["minecraftVersion"]) + self.assertEqual(native.EXPLORATORY, document["qualificationStatus"]) + self.assertTrue(all(entry["version"].endswith(".120061") for entry in document["mods"])) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native1206_actor_network_adapters.py b/test-rig/migration/test_native1206_actor_network_adapters.py new file mode 100644 index 00000000..5b8b0114 --- /dev/null +++ b/test-rig/migration/test_native1206_actor_network_adapters.py @@ -0,0 +1,107 @@ +"""Fail-closed controls for the Forge 50 lifecycle format adapters.""" + +import copy +from pathlib import Path +import tempfile +import unittest + +import nbtlib + +import actors +import native1206_actors as actor_driver +import native1206_networks as network_driver +from matrix import WORLD + + +class ActorAdapters(unittest.TestCase): + 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 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/platform-controls/NativeLeashKnotPlatformControl.java b/test-rig/platform-controls/NativeLeashKnotPlatformControl.java index c994a269..9ca7ab12 100644 --- a/test-rig/platform-controls/NativeLeashKnotPlatformControl.java +++ b/test-rig/platform-controls/NativeLeashKnotPlatformControl.java @@ -7,7 +7,6 @@ import java.security.MessageDigest; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Random; import java.util.UUID; import java.util.function.Supplier; import net.minecraft.SharedConstants; @@ -15,6 +14,7 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.server.Bootstrap; import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; import net.minecraft.util.profiling.InactiveProfiler; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.entity.Entity; @@ -37,14 +37,15 @@ private static String hash(Class type) throws Exception { } public static void main(String[] args) throws Exception { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); + zone.moddev.mc.advantageworks.unit.ForgeUnitRegistries.registerVanillaFluidTypes(); try { - Class.forName("zone.moddev.mc.poweradvantage.migration.LegacyEntityDataFixes"); + Class.forName("zone.moddev.mc.basepower.migration.LegacyEntityDataFixes"); throw new AssertionError("Power must be absent from the native control"); } catch (ClassNotFoundException expected) { System.out.println("POWER_LEGACY_CLASS_ABSENT true"); } Class unsafe = Class.forName("sun.misc.Unsafe"); Field instance = unsafe.getDeclaredField("theUnsafe"); instance.setAccessible(true); ServerLevel world = (ServerLevel) unsafe.getMethod("allocateInstance", Class.class).invoke(instance.get(null), ServerLevel.class); - field(world, "dimension", Level.NETHER); field(world, "random", new Random(0)); + field(world, "dimension", Level.NETHER); field(world, "random", RandomSource.create(0)); field(world, "profiler", (Supplier) () -> InactiveProfiler.INSTANCE); LeashFenceKnotEntity knot = new LeashFenceKnotEntity(world, new BlockPos(154, 100, 8)); UUID identity = knot.getUUID(); @@ -55,9 +56,10 @@ 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", "native117-leash-knot-nosave-v1"); + proof.put("format", 1); proof.put("policy", "native1206-leash-knot-nosave-v1"); proof.put("minecraftVersion", SharedConstants.getCurrentVersion().getName()); - proof.put("dataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); proof.put("forgeVersion", "37.1.1"); + proof.put("dataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion()); + proof.put("forgeVersion", net.minecraftforge.versions.forge.ForgeVersion.getVersion()); proof.put("nativeClass", knot.getClass().getName()); proof.put("registryId", EntityType.getKey(knot.getType()).toString()); proof.put("canSerialize", false); proof.put("rootWriterAccepted", false); proof.put("observationalUuidPreserved", true); proof.put("observationalStateUnchanged", true); diff --git a/test-rig/platform-controls/NativeWolfPlatformControl.java b/test-rig/platform-controls/NativeWolfPlatformControl.java index 213e7a52..dca148e7 100644 --- a/test-rig/platform-controls/NativeWolfPlatformControl.java +++ b/test-rig/platform-controls/NativeWolfPlatformControl.java @@ -1,28 +1,32 @@ import com.google.gson.Gson; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Random; +import java.util.List; import java.util.UUID; import java.util.function.Supplier; import net.minecraft.SharedConstants; +import net.minecraft.nbt.NbtAccounter; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtIo; import net.minecraft.server.Bootstrap; +import net.minecraft.server.RegistryLayer; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.repository.ServerPacksSource; +import net.minecraft.server.packs.resources.MultiPackResourceManager; +import net.minecraft.resources.RegistryDataLoader; +import net.minecraft.util.RandomSource; import net.minecraft.util.profiling.InactiveProfiler; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.animal.Wolf; import net.minecraft.world.level.Level; -import net.minecraftforge.common.ForgeMod; -import net.minecraftforge.event.RegistryEvent; -import net.minecraftforge.registries.DeferredRegister; import net.minecraftforge.registries.ForgeRegistries; +import zone.moddev.mc.advantageworks.unit.ForgeUnitRegistries; /** Native load/save control: no Advantage classes, server, entity ticks or DFU. */ public final class NativeWolfPlatformControl { @@ -39,8 +43,15 @@ private static ServerLevel context() throws Exception { ServerLevel world = (ServerLevel) unsafe.getMethod("allocateInstance", Class.class) .invoke(instance.get(null), ServerLevel.class); field(world, "dimension", Level.NETHER); - field(world, "random", new Random(0)); + field(world, "random", RandomSource.create(0)); field(world, "profiler", (Supplier) () -> InactiveProfiler.INSTANCE); + var base = RegistryLayer.createRegistryAccess(); + try (var resources = new MultiPackResourceManager(PackType.SERVER_DATA, + List.of(ServerPacksSource.createVanillaPackSource()))) { + var worldgen = RegistryDataLoader.load(resources, base.compositeAccess(), + RegistryDataLoader.WORLDGEN_REGISTRIES); + field(world, "registryAccess", base.replaceFrom(RegistryLayer.WORLDGEN, worldgen).compositeAccess()); + } return world; } @@ -51,24 +62,24 @@ private static Wolf wolf(ServerLevel world) { private static void control(Path root, ServerLevel world, boolean tame, float health) throws Exception { String label = (tame ? "tame" : "wild") + "-health-" + (int) health; Wolf first = wolf(world); - first.setTame(tame); + first.setTame(tame, true); if (tame) first.setOwnerUUID(OWNER); first.setHealth(health); CompoundTag before = first.saveWithoutId(new CompoundTag()); if (before.getFloat("Health") != health) throw new AssertionError("Incorrect source control health"); - Path input = root.resolve(label + "-before.dat"); NbtIo.writeCompressed(before, input.toFile()); - Wolf second = wolf(world); second.load(NbtIo.readCompressed(input.toFile())); + Path input = root.resolve(label + "-before.dat"); NbtIo.writeCompressed(before, input); + Wolf second = wolf(world); second.load(NbtIo.readCompressed(input, NbtAccounter.unlimitedHeap())); CompoundTag after = second.saveWithoutId(new CompoundTag()); - NbtIo.writeCompressed(after, root.resolve(label + "-after.dat").toFile()); + NbtIo.writeCompressed(after, root.resolve(label + "-after.dat")); Wolf third = wolf(world); third.load(after.copy()); CompoundTag restarted = third.saveWithoutId(new CompoundTag()); - NbtIo.writeCompressed(restarted, root.resolve(label + "-restart.dat").toFile()); + NbtIo.writeCompressed(restarted, root.resolve(label + "-restart.dat")); if (!ID.equals(second.getUUID()) || !ID.equals(third.getUUID())) throw new AssertionError("Control UUID changed"); if (tame && (!OWNER.equals(second.getOwnerUUID()) || !OWNER.equals(third.getOwnerUUID()))) { throw new AssertionError("Control owner changed"); } Map row = new LinkedHashMap<>(); - row.put("label", label); row.put("currentWorldDataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); + row.put("label", label); row.put("currentWorldDataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion()); row.put("legacyDfuInvoked", false); row.put("directNativeEntityLoadSave", true); row.put("beforeHealth", before.getFloat("Health")); row.put("afterHealth", after.getFloat("Health")); row.put("restartHealth", restarted.getFloat("Health")); @@ -78,11 +89,10 @@ private static void control(Path root, ServerLevel world, boolean tame, float he public static void main(String[] args) throws Exception { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); - Field attributes = ForgeMod.class.getDeclaredField("ATTRIBUTES"); attributes.setAccessible(true); - Method add = DeferredRegister.class.getDeclaredMethod("addEntries", RegistryEvent.Register.class); add.setAccessible(true); - add.invoke(attributes.get(null), new RegistryEvent.Register<>(ForgeRegistries.ATTRIBUTES.getRegistryName(), ForgeRegistries.ATTRIBUTES)); + ForgeUnitRegistries.registerVanillaFluidTypes(); + ForgeUnitRegistries.registerForgeEntries("ATTRIBUTES", ForgeRegistries.Keys.ATTRIBUTES, ForgeRegistries.ATTRIBUTES); try { - Class.forName("zone.moddev.mc.poweradvantage.migration.LegacyEntityDataFixes"); + Class.forName("zone.moddev.mc.basepower.migration.LegacyEntityDataFixes"); throw new AssertionError("Advantage is present on the native control classpath"); } catch (ClassNotFoundException expected) { System.out.println("POWER_LEGACY_CLASS_ABSENT true"); } Path root = Paths.get(args[0]); Files.createDirectory(root); diff --git a/test-rig/profiles/native1206/dev-core.json b/test-rig/profiles/native1206/dev-core.json new file mode 100644 index 00000000..ddc8ca4a --- /dev/null +++ b/test-rig/profiles/native1206/dev-core.json @@ -0,0 +1,19 @@ +{ + "name": "dev-core", + "mode": "development", + "minecraftVersion": "1.20.6", + "forgeVersion": "50.2.0", + "powerDeobfSha256": "29C1BD5EF384F8BB2441029061615E52A4B7302B9FC5480F2CE9C84C357A3181", + "mods": [ + { + "id": "basesteam", + "version": "2.2.14.120061", + "sha256": "5DDABC83B01A8CC8BAE3188BAAACE01BDCF6F756B7049DADF1ADAA3F9CF480FD" + }, + { + "id": "baseelectric", + "version": "2.2.14.120061", + "sha256": "E9002244F220D7DDBCDBD7B512AB0F34710D8B49276DB4CD72BA131536FE0330" + } + ] +} diff --git a/test-rig/profiles/native1206/power-interop.json b/test-rig/profiles/native1206/power-interop.json new file mode 100644 index 00000000..19b5f446 --- /dev/null +++ b/test-rig/profiles/native1206/power-interop.json @@ -0,0 +1,22 @@ +{ + "name": "power-interop", + "mode": "packaged", + "minecraftVersion": "1.20.6", + "forgeVersion": "50.2.0", + "qualificationStatus": "EXPLORATORY_UNQUALIFIED", + "baseMetalsSourceCommit": "9e706db5c85fa2bc7f36dee94f39a5f2fa8ab367", + "mods": [ + { + "id": "basemetals", + "version": "3.1.2.120061", + "path": "../../../../BaseMetals/.verification-basemetals1206/Advantage shared hook/evidence/player-pre-components-20261110/candidate/BaseMetals-3.1.2.120061.jar", + "sha256": "6186AC62C7E2E333373EA0D8AD8DE53FF859C520EBF568DFB47517C4F77C8C5D" + }, + { + "id": "orespawn", + "version": "4.1.0.120061", + "path": "../../../../BaseMetals/.verification-basemetals1206/dependencies/OreSpawn-4.1.0.120061.jar", + "sha256": "FAA7107B9FB84E2C62ED847CAFD880AA3C4238A37799BAD955EA9602A944AA26" + } + ] +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ActorProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ActorProbe.java index 577005e8..d1aa280a 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ActorProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ActorProbe.java @@ -85,7 +85,7 @@ private void snapshot(MinecraftServer server, ServerLevel world, String operatio StandardCharsets.UTF_8), JsonObject.class); // Entity updates require neighbouring chunks and a non-idle world. This // sentinel is never logged in and never reads/writes the witness player. - net.minecraftforge.common.util.FakePlayer sentinel = net.minecraftforge.common.util.FakePlayerFactory.getMinecraft(world); + WorksOfflinePlayer sentinel = WorksOfflinePlayer.sentinel(world); sentinel.setPos(128.5, 80, -16.5); if (!world.players().contains(sentinel)) world.players().add(sentinel); for (int x = 6; x <= 13; x++) { @@ -96,7 +96,7 @@ private void snapshot(MinecraftServer server, ServerLevel world, String operatio result.addProperty("frozen", frozen(world)); result.addProperty("gameTime", world.getGameTime()); try (java.io.FileInputStream input = new java.io.FileInputStream(new File("AdvantageWorksMigration/level.dat"))) { - CompoundTag data = NbtIo.readCompressed(input).getCompound("Data"); + CompoundTag data = NbtIo.readCompressed(input, net.minecraft.nbt.NbtAccounter.unlimitedHeap()).getCompound("Data"); if (data.contains("Player", 10)) result.add("embedded", encode(data.getCompound("Player"))); } boolean read = operation.equals("read"); @@ -104,8 +104,8 @@ private void snapshot(MinecraftServer server, ServerLevel world, String operatio if (read) { GameProfile profile = new GameProfile(UUID.fromString(definition.get("playerUuid").getAsString()), definition.get("playerName").getAsString()); - ServerPlayer player = new ServerPlayer(server, world, profile); - if (server.getPlayerList().load(player) == null) throw new IllegalStateException("No player file loaded"); + ServerPlayer player = new ServerPlayer(server, world, profile, net.minecraft.server.level.ClientInformation.createDefault()); + if (server.getPlayerList().load(player).isEmpty()) throw new IllegalStateException("No player file loaded"); result.add("player", encode(player.saveWithoutId(new CompoundTag()))); Native117PlayerIo.save(server.getPlayerList(), player); } 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 5c5569a5..b216b3a4 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.120011"; + public static final String VERSION = "1.1.4.120061"; public AdvantageWorksMod() { MigrationProbe.register(); @@ -19,6 +19,7 @@ public AdvantageWorksMod() { RecipeUnlockProbe.register(); ProgressionProbe.register(); NativeInteropProbe.register(); + NativeOilBucketProbe.register(); Native117Readiness.register(); NativeHarvestProbe.register(); MachineOperationalProbe.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 07be7f06..f35e7f3c 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 @@ -17,7 +17,7 @@ static void register(List cases) { c -> { c.block(7, 4, 8, "baseelectric:electric_battery_array"); c.put(7, 4, 8, 0, "baseelectric:" + chemistry + "_battery", 1); - c.inventory(7, 4, 8).getItem(0).getOrCreateTag().putFloat("energy", 10); + WorksItemData.update(c.inventory(7, 4, 8).getItem(0), tag -> tag.putFloat("energy", 10)); c.block(8, 4, 8, "baseelectric:led_bar"); }, c -> { PoweredEntity array = (PoweredEntity)c.tile(7, 4, 8); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ConnectedProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ConnectedProbe.java index a5b4734e..c5d83592 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ConnectedProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ConnectedProbe.java @@ -83,7 +83,7 @@ private void execute(ServerLevel current, String operation, String label, int ti world.getChunk(x, z); } } - net.minecraftforge.common.util.FakePlayer sentinel = net.minecraftforge.common.util.FakePlayerFactory.getMinecraft(world); + WorksOfflinePlayer sentinel = WorksOfflinePlayer.sentinel(world); sentinel.setPos(0.5, 80, 0.5); if (!world.players().contains(sentinel)) world.players().add(sentinel); } @@ -107,7 +107,7 @@ private void execute(ServerLevel current, String operation, String label, int ti entry.add("properties", properties); BlockEntity tile = world.getBlockEntity(pos); if (tile != null) { - CompoundTag tag = tile.saveWithFullMetadata(); + CompoundTag tag = WorksBlockData.save(tile); java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream(); net.minecraft.nbt.NbtIo.writeCompressed(tag, bytes); entry.addProperty("nbt", tag.toString()); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/DimensionEntityProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/DimensionEntityProbe.java index da0cee64..1a339af7 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 @@ -54,8 +54,6 @@ import net.minecraft.world.level.Level; import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.common.util.FakePlayer; -import net.minecraftforge.common.util.FakePlayerFactory; import net.minecraftforge.event.level.LevelEvent; import net.minecraftforge.event.TickEvent; import net.minecraftforge.event.RegisterCommandsEvent; @@ -271,7 +269,7 @@ private static void prepare(ServerLevel world) { } PINNED.add(world); } - FakePlayer sentinel = FakePlayerFactory.getMinecraft(world); + WorksOfflinePlayer sentinel = WorksOfflinePlayer.sentinel(world); if (sentinel.connection == null) new OfflineConnection(world.getServer(), sentinel); sentinel.setPos(144.5, 100, 8.5); if (!world.players().contains(sentinel)) world.players().add(sentinel); @@ -294,8 +292,8 @@ private static void collect(Entity entity, Set seen, List result private static ServerPlayer readPlayer(MinecraftServer server, ServerLevel world, JsonObject definition) { ServerPlayer player = new ServerPlayer(server, world, - new GameProfile(UUID.fromString(definition.get("playerUuid").getAsString()), definition.get("playerName").getAsString())); - if (server.getPlayerList().load(player) == null) throw new IllegalStateException("No player file loaded"); + new GameProfile(UUID.fromString(definition.get("playerUuid").getAsString()), definition.get("playerName").getAsString()), net.minecraft.server.level.ClientInformation.createDefault()); + if (server.getPlayerList().load(player).isEmpty()) throw new IllegalStateException("No player file loaded"); if (Native117Worlds.legacyId(player.level().dimension()) != dimension(definition) || player.level() != world) { throw new IllegalStateException("Player loaded in wrong dimension"); } @@ -344,7 +342,7 @@ private JsonObject snapshot(MinecraftServer server, ServerLevel world, JsonObjec result.add("entities", entities); BlockPos pos = new BlockPos(176, 101, 8); JsonObject generator = new JsonObject(); generator.addProperty("actualName", net.minecraftforge.registries.ForgeRegistries.BLOCKS.getKey(world.getBlockState(pos).getBlock()).toString()); - BlockEntity tile = world.getBlockEntity(pos); if (tile != null) generator.add("tile", encode(tile.saveWithFullMetadata())); + BlockEntity tile = world.getBlockEntity(pos); if (tile != null) generator.add("tile", encode(WorksBlockData.save(tile))); result.add("generator", generator); return result; } @@ -438,7 +436,7 @@ private static JsonObject spawnDiagnostics(ServerPlayer player, CompoundTag nbt) } private static final class OfflineConnection extends ServerGamePacketListenerImpl { - OfflineConnection(MinecraftServer server, ServerPlayer player) { super(server, new Connection(PacketFlow.SERVERBOUND), player); } + OfflineConnection(MinecraftServer server, ServerPlayer player) { super(server, new Connection(PacketFlow.SERVERBOUND), player, WorksOfflinePlayer.cookie(player)); } @Override public void send(Packet packet) { /* Deliberately no socket or client login. */ } @Override public void send(Packet packet, PacketSendListener listener) { // Death-message packets use this overload; no native network work is queued. 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 0e7e9a45..45fd4c29 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 @@ -21,7 +21,6 @@ import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fluids.capability.IFluidHandler; import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.common.util.FakePlayer; /** Independent operating contracts. Only setup seeds inputs; all work uses world ticks. */ public final class ElectricOperationalCases { @@ -322,7 +321,7 @@ private static void growth(List cases) { equal(c, "scarce.output", output(c, 9, 3, 6, "minecraft:sugar_cane"), 0); equal(c, "full.seed", slotCount(c, 23, 0, "minecraft:sugar_cane"), 1); equal(c, "full.output", output(c, 23, 3, 6, "minecraft:sugar_cane"), 384); - AABB bay = new AABB(c.at(0, 0, 0), c.at(31, 31, 31)); + AABB bay = WorksBlockData.bounds(c.at(0, 0, 0), c.at(31, 31, 31)); equal(c, "overflowItems", c.world.getEntitiesOfClass(ItemEntity.class, bay).size(), 0); float soil = ((Number)c.invoke(8, Y, Z, "getSoil", new Class[0])).floatValue(); close(c, "scarce.convertedGrowth", 1000 - soil, 799, 0.1f); @@ -406,7 +405,7 @@ private static void hydro(List cases) { close(c, "dry.generated", c.energy(27, Y, 22, ELECTRICITY), 0, 0.001f); int turbines = 0; for (Entity actor : c.world.getEntitiesOfClass(Entity.class, - new AABB(c.at(0, 0, 0), c.at(31, 31, 31)))) { + 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; turbines++; c.record("hydro.motion/" + actor.getUUID(), actor.getDeltaMovement().toString()); @@ -622,7 +621,7 @@ private static void turret(List cases) { } })); } - final FakePlayer[] owner = new FakePlayer[1]; + final WorksOfflinePlayer[] owner = new WorksOfflinePlayer[1]; final PlayerTeam[] team = new PlayerTeam[1]; final Husk[] victim = new Husk[1]; cases.add(new MachineScenario("electric/turret-owner-credit", @@ -633,7 +632,7 @@ private static void turret(List cases) { c.power(8, 1, Z, ELECTRICITY, 1500); UUID identity = UUID.randomUUID(); String name = "EOp" + identity.toString().substring(0, 8); - FakePlayer player = new FakePlayer(c.world, new GameProfile(identity, name)); + WorksOfflinePlayer player = new WorksOfflinePlayer(c.world, new GameProfile(identity, name)); player.moveTo(c.at(8, 1, 16).getX() + 0.5, c.at(8, 1, 16).getY(), c.at(8, 1, 16).getZ() + 0.5, 0, 0); owner[0] = player; team[0] = c.world.getScoreboard().addPlayerTeam(name); 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 4bf175e1..cb580ecb 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 @@ -335,7 +335,7 @@ private boolean roundTripPrerequisites(RoundTripRequest request) { BlockPos pos = position(fixture.to); LevelChunk chunk = world.getChunkSource().getChunkNow(pos.getX() >> 4, pos.getZ() >> 4); BlockEntity tile = existingTile(world, pos); - CompoundTag saved = tile == null ? new CompoundTag() : tile.saveWithFullMetadata(); + CompoundTag saved = tile == null ? new CompoundTag() : WorksBlockData.save(tile); boolean hasTarget = saved.contains("targetX", 99) && saved.contains("targetY", 99) && saved.contains("targetZ", 99); BlockPos target = hasTarget ? new BlockPos(saved.getInt("targetX"), saved.getInt("targetY"), saved.getInt("targetZ")) : pos; Map flags = new LinkedHashMap<>(); @@ -376,7 +376,7 @@ private void applyRoundTrip(RoundTripRequest request) { Map saved = new LinkedHashMap<>(); for (Fixture fixture : fixtures.values()) { if (fixture.to == null) continue; - CompoundTag tag = request.bindings.get(fixture.id).tile.saveWithFullMetadata(); + CompoundTag tag = WorksBlockData.save(request.bindings.get(fixture.id).tile); if (!tag.contains("targetX", 99) || !tag.contains("targetY", 99) || !tag.contains("targetZ", 99) || tag.getShort("progress") <= 0) throw new IllegalStateException("Drill must be mid-block " + fixture.id); requireInside(fixture, new int[]{tag.getInt("targetX"), tag.getInt("targetY"), tag.getInt("targetZ")}); @@ -400,7 +400,7 @@ private void applyRoundTrip(RoundTripRequest request) { Blocks.BEDROCK.defaultBlockState(), 3); } BlockEntity tile = request.bindings.get(entry.getKey()).tile; - tile.load(tag); tile.setChanged(); + tile.loadWithComponents(tag, tile.getLevel().registryAccess()); tile.setChanged(); } requireNewResult(request.label + "-initial"); Map initial = observe(request.world); @@ -545,7 +545,7 @@ private Map witness(ServerLevel world, int[] coordinates) { result.put("state", chunk.getBlockState(pos).toString()); BlockEntity tile = chunk.getBlockEntity(pos, LevelChunk.EntityCreationType.CHECK); if (tile != null) { - result.put("nbt", tile.saveWithFullMetadata().toString()); + result.put("nbt", WorksBlockData.save(tile).toString()); result.put("tileIdentity", System.identityHashCode(tile)); if (tile instanceof PoweredEntity) { PoweredEntity powered = (PoweredEntity) tile; diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/MachineContext.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/MachineContext.java index 7a018f61..cd825eef 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 @@ -175,7 +175,7 @@ void electricSupply(int x, int y, int z, float reserve) { if (!Float.isFinite(reserve) || reserve < 0 || reserve > 8000) throw new IllegalArgumentException("Invalid battery reserve"); block(x, y, z, "baseelectric:electric_battery_array"); put(x, y, z, 0, "baseelectric:lithium_battery", 1); - inventory(x, y, z).getItem(0).getOrCreateTag().putFloat("energy", reserve); + WorksItemData.update(inventory(x, y, z).getItem(0), tag -> tag.putFloat("energy", reserve)); inventory(x, y, z).setChanged(); } @@ -195,14 +195,14 @@ int fluidAmount(int x, int y, int z) { return amount; } - CompoundTag nbt(int x, int y, int z) { return tile(x, y, z).saveWithFullMetadata(); } + CompoundTag nbt(int x, int y, int z) { return WorksBlockData.save(tile(x, y, z)); } void seed(int x, int y, int z, CompoundTag patch) { mutation("nbt", at(x, y, z), patch.toString()); BlockEntity tile = tile(x, y, z); - CompoundTag nbt = tile.saveWithFullMetadata(); + CompoundTag nbt = WorksBlockData.save(tile); nbt.merge(patch.copy()); - tile.load(nbt); + tile.loadWithComponents(nbt, world.registryAccess()); tile.setChanged(); } @@ -234,7 +234,7 @@ JsonArray snapshot() { row.addProperty("block", net.minecraftforge.registries.ForgeRegistries.BLOCKS.getKey(state.getBlock()).toString()); row.addProperty("state", state.toString()); BlockEntity tile = world.getBlockEntity(pos); - if (tile != null) row.addProperty("nbt", tile.saveWithFullMetadata().toString()); + if (tile != null) row.addProperty("nbt", WorksBlockData.save(tile).toString()); rows.add(row); } return rows; @@ -242,7 +242,7 @@ JsonArray snapshot() { JsonArray entities() { JsonArray result = new JsonArray(); - net.minecraft.world.phys.AABB bounds = new net.minecraft.world.phys.AABB(at(0, -1, 0), at(31, 31, 31)).inflate(.5); + net.minecraft.world.phys.AABB bounds = WorksBlockData.bounds(at(0, -1, 0), at(31, 31, 31)).inflate(.5); for (net.minecraft.world.entity.Entity entity : world.getEntitiesOfClass(net.minecraft.world.entity.Entity.class, bounds)) { if (entity instanceof net.minecraft.world.entity.player.Player) continue; CompoundTag nbt = new CompoundTag(); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/MachineOperationalProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/MachineOperationalProbe.java index fcd2bf91..946aff6f 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/MachineOperationalProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/MachineOperationalProbe.java @@ -22,8 +22,6 @@ import net.minecraft.world.level.GameRules; import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.common.util.FakePlayer; -import net.minecraftforge.common.util.FakePlayerFactory; import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.event.TickEvent; @@ -110,7 +108,7 @@ private void authorize(ServerLevel current, String requestedLabel) { if (!MARKER.isFile() || !current.getServer().getWorldData().getLevelName().equals("AdvantageWorksMigration") || !requestedLabel.matches("[A-Za-z0-9_-]+")) throw new IllegalStateException("Unmarked or unsafe operating world"); world = current; - FakePlayer sentinel = FakePlayerFactory.getMinecraft(world); + WorksOfflinePlayer sentinel = WorksOfflinePlayer.sentinel(world); sentinel.setPos(49900.5, 80, 49900.5); if (!world.players().contains(sentinel)) world.players().add(sentinel); } @@ -281,7 +279,7 @@ private void inspectNext() { actual.addProperty("block", net.minecraftforge.registries.ForgeRegistries.BLOCKS.getKey(world.getBlockState(pos).getBlock()).toString()); actual.addProperty("state", world.getBlockState(pos).toString()); BlockEntity tile = world.getBlockEntity(pos); - if (tile != null) actual.addProperty("nbt", tile.saveWithFullMetadata().toString()); + if (tile != null) actual.addProperty("nbt", WorksBlockData.save(tile).toString()); positions.add(actual); } row.add("positions", positions); inspectionResults.add(row); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/MigrationProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/MigrationProbe.java index bf38d684..6db36029 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/MigrationProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/MigrationProbe.java @@ -84,7 +84,7 @@ private void snapshot(Level world, String label) throws Exception { BlockEntity tile = world.getBlockEntity(pos); if (tile != null) { tile.setChanged(); - CompoundTag nbt = tile.saveWithFullMetadata(); + CompoundTag nbt = WorksBlockData.save(tile); entry.addProperty("nbt", nbt.toString()); java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream(); net.minecraft.nbt.NbtIo.writeCompressed(nbt, bytes); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117Readiness.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117Readiness.java index efb5b30f..c4538947 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117Readiness.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117Readiness.java @@ -82,7 +82,7 @@ private void tick(TickEvent.LevelTickEvent event) { if (!observation.observe(world.getGameTime(), System.nanoTime(), states)) return; JsonObject report = observation.report(); report.addProperty("label", label); - report.addProperty("minecraft", "1.20.1"); + report.addProperty("minecraft", net.minecraft.SharedConstants.getCurrentVersion().getName()); report.addProperty("dimension", world.dimension().location().toString()); try { Files.createDirectories(output.toPath().getParent()); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117WorldgenAccess.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117WorldgenAccess.java index e0cb6721..5f3cce3a 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 @@ -11,7 +11,7 @@ import net.minecraft.server.level.TicketType; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.biome.Biome; -import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraftforge.registries.ForgeRegistries; @@ -34,8 +34,8 @@ final class Native117WorldgenAccess implements WorldgenSamplingJob.WorldAccess { @Override public CompletableFuture request(int x, int z) { // The public API managedBlocks on the server-thread branch, but not here. return source.getChunkFuture(x, z, ChunkStatus.FULL, true).thenApply(result -> { - net.minecraft.world.level.chunk.ChunkAccess access = result.left().orElseThrow(() -> - new IllegalStateException("Native FULL chunk unavailable: " + result.right())); + 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 || !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 562632da..cf5400cc 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 @@ -171,8 +171,8 @@ private static void run(ServerLevel world, String label) throws Exception { File output = new File("advantage-works-results", "native-harvest-" + label + ".json"); require(!output.exists(), "Refusing to overwrite harvest evidence"); ServerPlayer player = new ServerPlayer(world.getServer(), world, - new GameProfile(UUID.fromString("c791aefe-a372-4536-9251-27032c01f115"), "AdvWorksHarvest")); - player.connection = new ServerGamePacketListenerImpl(world.getServer(), new Connection(PacketFlow.SERVERBOUND), player) { + new GameProfile(UUID.fromString("c791aefe-a372-4536-9251-27032c01f115"), "AdvWorksHarvest"), net.minecraft.server.level.ClientInformation.createDefault()); + player.connection = new ServerGamePacketListenerImpl(world.getServer(), new Connection(PacketFlow.SERVERBOUND), player, WorksOfflinePlayer.cookie(player)) { @Override public void send(Packet packet) {} }; player.setPos(POS.getX() + 0.5, POS.getY() + 1, POS.getZ() + 0.5); @@ -218,7 +218,7 @@ private static void run(ServerLevel world, String label) throws Exception { harvestDrops(world, new AABB(POS).inflate(2)).forEach(ItemEntity::discard); JsonObject report = new JsonObject(); report.addProperty("label", label); - report.addProperty("minecraft", "1.20.1"); + report.addProperty("minecraft", net.minecraft.SharedConstants.getCurrentVersion().getName()); report.addProperty("worldTime", world.getGameTime()); report.addProperty("fixtureIsolation", "native117-live-drops-v1"); report.addProperty("status", java.util.stream.StreamSupport.stream(rows.spliterator(), false) diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeInteropProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeInteropProbe.java index c4069727..30318362 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 @@ -179,13 +179,13 @@ private static void tank(ServerLevel world, JsonArray checks) { List drops = world.getEntitiesOfClass(ItemEntity.class, new AABB(TANK).inflate(2)); require(drops.size() == 1, "Expected exactly one portable tank drop; got " + drops.size()); ItemStack item = drops.get(0).getItem().copy(); - require(item.getItem() == block.asItem() && item.getCount() == 1 && item.hasTag() - && item.getTag().getString("FluidID").equals("minecraft:water") - && item.getTag().getInt("Volume") == 500, "Native loot lost portable fluid NBT: " + item); + require(item.getItem() == block.asItem() && item.getCount() == 1 + && WorksItemData.read(item).getString("FluidID").equals("minecraft:water") + && WorksItemData.read(item).getInt("Volume") == 500, "Native loot lost portable fluid NBT: " + item); drops.get(0).discard(); world.setBlock(TANK, block.defaultBlockState(), 3); block.setPlacedBy(world, TANK, block.defaultBlockState(), - net.minecraftforge.common.util.FakePlayerFactory.getMinecraft(world), item); + WorksOfflinePlayer.sentinel(world), item); require(fluid(world.getBlockEntity(TANK)).getFluidInTank(0).getAmount() == 500, "Replacing native drop did not restore exactly 500 mB"); }); @@ -200,9 +200,9 @@ private static void locks(ServerLevel world, JsonArray checks) { input.setItem(0, new ItemStack(net.minecraft.world.item.Items.DIRT, 16)); TileEntityConveyor belt = (TileEntityConveyor) world.getBlockEntity(BELT); BlockEntity chest = world.getBlockEntity(BELT.west()); - CompoundTag state = chest.saveWithFullMetadata(); + CompoundTag state = WorksBlockData.save(chest); state.putString("Lock", "AdvantageInteropKey"); - chest.load(state); + chest.loadWithComponents(state, world.registryAccess()); check(checks, "locked-chest/conveyor-refuses-extraction", () -> { require(InventoryLocks.isLocked(input), "Native nonempty chest lock was not detected"); for (int i = 0; i < 160; i++) belt.tick(); @@ -210,7 +210,7 @@ private static void locks(ServerLevel world, JsonArray checks) { "Conveyor bypassed locked source"); }); state.remove("Lock"); - chest.load(state); + chest.loadWithComponents(state, world.registryAccess()); check(checks, "unlocked-chest/conveyor-transfers-exactly", () -> { require(!InventoryLocks.isLocked(input), "Cleared lock still blocks extraction"); for (int i = 0; i < 160; i++) belt.tick(); @@ -329,9 +329,34 @@ private static void run(ServerLevel world, String phase) throws Exception { report.addProperty("status", java.util.stream.StreamSupport.stream(checks.spliterator(), false) .allMatch(row -> row.getAsJsonObject().get("status").getAsString().equals("PASS")) ? "PASS" : "FAIL"); report.add("checks", checks); + if (net.minecraftforge.fml.ModList.get().isLoaded("basemetals")) { + JsonArray shared = new JsonArray(); + check(shared, "basemetals/native-power-crushing", () -> sharedCrushing(world)); + report.add("sharedChecks", shared); + } File destination = new File("advantage-works-results", "native-interop-" + phase + ".json"); require(!destination.exists(), "Refusing to overwrite interop evidence"); destination.getParentFile().mkdirs(); Files.write(destination.toPath(), new GsonBuilder().setPrettyPrinting().create().toJson(report).getBytes(StandardCharsets.UTF_8)); } + + @SuppressWarnings("unchecked") + private static void sharedCrushing(ServerLevel world) { + ResourceLocation recipeId = new ResourceLocation("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 type = net.minecraft.core.registries.BuiltInRegistries.RECIPE_TYPE.getKey(recipe.getType()); + require(new ResourceLocation("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)); + 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( + 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), + "Shared crushing accepted unrelated input"); + } } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeOilBucketProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeOilBucketProbe.java new file mode 100644 index 00000000..54e6545c --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeOilBucketProbe.java @@ -0,0 +1,219 @@ +package zone.moddev.mc.advantageworks; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.mojang.authlib.GameProfile; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import java.util.function.Consumer; +import net.minecraft.commands.Commands; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.registries.ForgeRegistries; + +/** Real survival interactions on live fluid blocks and native Power tanks. */ +final class NativeOilBucketProbe { + private static final Path MARKER = Path.of("advantage-native-oil-bucket.allow"); + private static final Path RECEIPT = Path.of("advantage-works-results", "native-oil-buckets.json"); + private static final InteractionHand HAND = InteractionHand.MAIN_HAND; + + private NativeOilBucketProbe() {} + + private static final class BucketActor extends WorksOfflinePlayer { + private BlockPos menuPosition; + private int menuRequests; + + BucketActor(ServerLevel world) { + super(world, new GameProfile(UUID.fromString("e95991d9-1ed7-4a81-91f6-0f42e21667a0"), "OilBucketProbe")); + } + + @Override public void openMenu(MenuProvider provider, BlockPos pos) { + // The offline actor records the real block's menu fallback at the packet boundary. + menuPosition = pos.immutable(); + menuRequests++; + } + } + + static void register() { + if (Files.isRegularFile(MARKER)) MinecraftForge.EVENT_BUS.addListener(NativeOilBucketProbe::commands); + } + + private static void commands(RegisterCommandsEvent event) { + event.getDispatcher().register(Commands.literal("advoilbuckets").requires(source -> source.hasPermission(4)) + .executes(context -> { + try { + run(context.getSource().getLevel()); + } catch (Exception error) { + throw new IllegalStateException("Native oil bucket probe failed", error); + } + System.out.println("ADVOILBUCKETS COMPLETE"); + return 1; + })); + } + + private static void require(boolean value, String message) { + if (!value) throw new AssertionError(message); + } + + private static void check(JsonArray results, String id, Consumer action) { + JsonObject row = new JsonObject(); + row.addProperty("id", id); + try { + action.accept(row); + row.addProperty("status", "PASS"); + } catch (AssertionError | RuntimeException error) { + row.addProperty("status", "FAIL"); + row.addProperty("failure", error.toString()); + var trace = new java.io.StringWriter(); + error.printStackTrace(new java.io.PrintWriter(trace)); + row.addProperty("traceback", trace.toString()); + } + results.add(row); + } + + private static void hand(WorksOfflinePlayer player, Item item) { + player.setItemInHand(HAND, new ItemStack(item)); + } + + private static void expectHand(WorksOfflinePlayer player, Item item) { + ItemStack stack = player.getItemInHand(HAND); + require(stack.is(item) && stack.getCount() == 1, "Wrong held item or count: " + stack); + int inventoryItems = 0; + for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++) { + inventoryItems += player.getInventory().getItem(slot).getCount(); + } + require(inventoryItems == 1, "Interaction duplicated or lost inventory: " + inventoryItems); + } + + private static void worldRoundTrip(ServerLevel world, WorksOfflinePlayer player, BlockPos basin, + Fluid oil, Item filled, JsonObject row) { + world.setBlockAndUpdate(basin.below(), Blocks.STONE.defaultBlockState()); + world.setBlockAndUpdate(basin, Blocks.AIR.defaultBlockState()); + for (Direction direction : Direction.Plane.HORIZONTAL) { + world.setBlockAndUpdate(basin.relative(direction), Blocks.STONE.defaultBlockState()); + } + player.moveTo(basin.getX() + 0.5, basin.getY() + 1.5, basin.getZ() + 0.5, 0, 90); + hand(player, filled); + var placed = player.gameMode.useItem(player, world, player.getItemInHand(HAND), HAND); + require(placed.consumesAction(), "Filled bucket world use returned " + placed); + var source = world.getFluidState(basin); + require(source.isSource() && source.getType() == oil, "Bucket did not place the exact oil source: " + source); + expectHand(player, Items.BUCKET); + row.addProperty("placedSource", ForgeRegistries.FLUIDS.getKey(source.getType()).toString()); + row.addProperty("placementBucket", "minecraft:bucket"); + var pickedUp = player.gameMode.useItem(player, world, player.getItemInHand(HAND), HAND); + require(pickedUp.consumesAction(), "Empty bucket world use returned " + pickedUp); + require(world.getFluidState(basin).isEmpty() && world.getBlockState(basin).isAir(), "Oil source remains after pickup"); + expectHand(player, filled); + row.addProperty("pickupBucket", ForgeRegistries.ITEMS.getKey(filled).toString()); + } + + private static IFluidHandler machine(ServerLevel world, BlockPos pos, Fluid oil, int amount) { + world.setBlockAndUpdate(pos, Blocks.AIR.defaultBlockState()); + var block = ForgeRegistries.BLOCKS.getValue(ResourceLocation.fromNamespaceAndPath("basepower", "fluid_storage_tank")); + require(block != null, "Storage tank block unavailable"); + world.setBlockAndUpdate(pos, block.defaultBlockState()); + var tile = world.getBlockEntity(pos); + require(tile != null, "Storage tank tile unavailable"); + var tank = tile.getCapability(ForgeCapabilities.FLUID_HANDLER, Direction.UP) + .orElseThrow(() -> new AssertionError("Storage tank capability unavailable")); + require(tank.getTankCapacity(0) == 4000, "Storage tank capacity changed"); + if (amount > 0) require(tank.fill(new FluidStack(oil, amount), IFluidHandler.FluidAction.EXECUTE) == amount, + "Failed to seed exact tank quantity"); + expectFluid(tank, oil, amount); + return tank; + } + + private static void expectFluid(IFluidHandler tank, Fluid oil, int amount) { + var state = tank.getFluidInTank(0); + require(state.getAmount() == amount && (amount == 0 ? state.isEmpty() : state.getFluid() == oil), + "Wrong fluid identity or amount: " + state + ", expected " + amount + " mB"); + } + + private static boolean clickTank(ServerLevel world, WorksOfflinePlayer player, BlockPos pos) { + player.moveTo(pos.getX() + 0.5, pos.getY() + 1.5, pos.getZ() + 1.5, 180, 35); + var hit = new BlockHitResult(Vec3.atCenterOf(pos).add(0, 0.5, 0), Direction.UP, pos, false); + return player.gameMode.useItemOn(player, world, player.getItemInHand(HAND), HAND, hit).consumesAction(); + } + + private static void machineRoundTrip(ServerLevel world, BucketActor player, BlockPos pos, + Fluid oil, Item filled, JsonObject row) { + IFluidHandler tank = machine(world, pos, oil, 0); + hand(player, filled); + int beforeMenus = player.menuRequests; + require(clickTank(world, player, pos), "Tank did not consume filled bucket interaction"); + expectFluid(tank, oil, 1000); + expectHand(player, Items.BUCKET); + row.addProperty("depositedMb", tank.getFluidInTank(0).getAmount()); + require(clickTank(world, player, pos), "Tank did not consume empty bucket interaction"); + expectFluid(tank, oil, 0); + expectHand(player, filled); + require(player.menuRequests == beforeMenus, "Successful bucket transfer opened a fallback menu"); + row.addProperty("returnedMb", 1000 - tank.getFluidInTank(0).getAmount()); + } + + private static void noLoss(ServerLevel world, BucketActor player, BlockPos pos, Fluid oil, + Item held, int amount, JsonObject row) { + IFluidHandler tank = machine(world, pos, oil, amount); + hand(player, held); + int beforeMenus = player.menuRequests; + require(clickTank(world, player, pos) && player.menuRequests == beforeMenus + 1 + && pos.equals(player.menuPosition), "Blocked transfer did not reach the original tank menu"); + expectFluid(tank, oil, amount); + expectHand(player, held); + require(world.getFluidState(pos.above()).isEmpty(), "Blocked transfer spilled into the world"); + row.addProperty("beforeMb", amount); + row.addProperty("afterMb", tank.getFluidInTank(0).getAmount()); + row.addProperty("heldBucket", ForgeRegistries.ITEMS.getKey(held).toString()); + row.addProperty("menuFallbackRequests", player.menuRequests - beforeMenus); + } + + private static void run(ServerLevel world) throws Exception { + require(Files.isRegularFile(MARKER) && !Files.exists(RECEIPT) + && world.getServer().getWorldData().getLevelName().equals("AdvantageWorksMigration"), "Unmarked or reused world"); + var player = new BucketActor(world); + require(player.gameMode.getGameModeForPlayer() == GameType.SURVIVAL && !player.hasInfiniteMaterials() + && !world.players().contains(player), "Expected a non-joined survival actor"); + JsonArray checks = new JsonArray(); + for (int index = 0; index < 2; index++) { + String name = index == 0 ? "crude_oil" : "refined_oil"; + var oil = ForgeRegistries.FLUIDS.getValue(ResourceLocation.fromNamespaceAndPath("basepower", name)); + var filled = ForgeRegistries.ITEMS.getValue(ResourceLocation.fromNamespaceAndPath("basepower", name + "_bucket")); + require(oil != null && filled instanceof net.minecraft.world.item.BucketItem, "Oil source or bucket unavailable"); + int x = 1072 + 16 * index; + BlockPos basin = new BlockPos(x, 8, 1025), tank = new BlockPos(x, 8, 1029); + require(world.isLoaded(basin) && world.isLoaded(tank), "Bucket witness chunks not ready"); + check(checks, name + "/world-roundtrip", row -> worldRoundTrip(world, player, basin, oil, filled, row)); + check(checks, name + "/machine-roundtrip", row -> machineRoundTrip(world, player, tank, oil, filled, row)); + check(checks, name + "/machine-below-bucket", row -> noLoss(world, player, tank, oil, Items.BUCKET, 375, row)); + check(checks, name + "/machine-insufficient-room", row -> noLoss(world, player, tank, oil, filled, 3500, row)); + check(checks, name + "/machine-full", row -> noLoss(world, player, tank, oil, filled, 4000, row)); + } + JsonObject receipt = new JsonObject(); + receipt.addProperty("worldTime", world.getGameTime()); + receipt.addProperty("gameMode", "survival"); + receipt.add("checks", checks); + Files.createDirectories(RECEIPT.getParent()); + Files.writeString(RECEIPT, new GsonBuilder().setPrettyPrinting().create().toJson(receipt), + java.nio.file.StandardOpenOption.CREATE_NEW); + } +} 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 58bb7d5b..29e058ff 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeTicketGeometry.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeTicketGeometry.java @@ -6,7 +6,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; -import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.server.level.ChunkLevel; import net.minecraft.server.level.FullChunkStatus; import net.minecraft.server.level.Ticket; 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 425fbb14..c563a649 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 @@ -56,22 +56,22 @@ static void register(List cases) { c.world.addFreshEntity(dropped); }, c -> { c.require(c.count(6, 2, 5, "minecraft:dirt") == 16, "dropped input delivered"); - c.require(c.world.getEntitiesOfClass(ItemEntity.class, new AABB(c.at(3, 0, 4), c.at(8, 5, 7))).isEmpty(), "no duplicate/lost item entity remains"); + c.require(c.world.getEntitiesOfClass(ItemEntity.class, WorksBlockData.bounds(c.at(3, 0, 4), c.at(8, 5, 7))).isEmpty(), "no duplicate/lost item entity remains"); })); cases.add(new MachineScenario("power/conveyor/nbt-preserved", "Distinct custom item data must not merge into or overwrite another stack", 200, c -> { conveyor(c, "basepower:item_conveyor"); c.put(4, 2, 5, 0, "minecraft:dirt", 3); c.put(4, 2, 5, 1, "minecraft:dirt", 3); - c.inventory(4, 2, 5).getItem(0).getOrCreateTag().putString("Witness", "A"); - c.inventory(4, 2, 5).getItem(1).getOrCreateTag().putString("Witness", "B"); + WorksItemData.update(c.inventory(4, 2, 5).getItem(0), tag -> tag.putString("Witness", "A")); + WorksItemData.update(c.inventory(4, 2, 5).getItem(1), tag -> tag.putString("Witness", "B")); c.put(6, 2, 5, 0, "minecraft:dirt", 1); - c.inventory(6, 2, 5).getItem(0).getOrCreateTag().putString("Witness", "A"); + WorksItemData.update(c.inventory(6, 2, 5).getItem(0), tag -> tag.putString("Witness", "A")); }, c -> { int a = 0, b = 0, unknown = 0; for (int slot = 0; slot < c.inventory(6, 2, 5).getContainerSize(); slot++) { ItemStack stack = c.inventory(6, 2, 5).getItem(slot); if (stack.isEmpty()) continue; - String witness = stack.hasTag() ? stack.getTag().getString("Witness") : ""; + String witness = WorksItemData.read(stack).getString("Witness"); if (witness.equals("A")) a += stack.getCount(); else if (witness.equals("B")) b += stack.getCount(); else unknown += stack.getCount(); 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 5592c224..517b9c9c 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 @@ -53,7 +53,7 @@ static void smelt(ServerLevel world, JsonObject fixture, String recipe, ItemStac && !output.isEmpty() && id(output).equals(id(expected)) && output.getCount() == expected.getCount(), "Live furnace failed " + recipe + ": input=" + furnace.getItem(0) + ", fuel=" + furnace.getItem(1) + ", output=" + output - + ", expected=" + expected + ", saved=" + furnace.saveWithFullMetadata()); + + ", expected=" + expected + ", saved=" + WorksBlockData.save(furnace)); record("live_furnace:" + recipe, input.getCount(), output.getCount()); } @@ -78,8 +78,8 @@ static void hammer(ServerLevel world, ServerPlayer player, Map type = net.minecraft.core.registries.BuiltInRegistries.RECIPE_TYPE .getOptional(new ResourceLocation("basemetals", "crushing")).orElseThrow(); - Recipe selected = world.getRecipeManager().getRecipeFor( + net.minecraft.world.item.crafting.RecipeHolder selected = world.getRecipeManager().getRecipeFor( (RecipeType>) type, new SimpleContainer(input.copy()), world).orElse(null); selection.addProperty("status", selected == null ? "NO_MATCH" : "MATCH"); selection.addProperty("matched", selected != null); if (selected != null) { - selection.addProperty("id", selected.getId().toString()); - selection.addProperty("class", selected.getClass().getName()); - selection.addProperty("resultNbt", selected.getResultItem(world.registryAccess()) - .save(new net.minecraft.nbt.CompoundTag()).toString()); + selection.addProperty("id", selected.id().toString()); + selection.addProperty("class", selected.value().getClass().getName()); + selection.addProperty("resultNbt", selected.value().getResultItem(world.registryAccess()) + .saveOptional(world.registryAccess()).toString()); } } catch (RuntimeException | LinkageError error) { selection.addProperty("status", "OBSERVER_ERROR"); @@ -159,7 +159,7 @@ private static JsonObject hammerSnapshot(ServerLevel world, ServerPlayer player, sample.addProperty("sourceRemoved", source.isRemoved()); sample.addProperty("sourceBounds", source.getBoundingBox().toString()); sample.addProperty("sourceIntersectsArea", source.getBoundingBox().intersects(area)); - sample.addProperty("sourceNbt", source.getItem().save(new net.minecraft.nbt.CompoundTag()).toString()); + sample.addProperty("sourceNbt", source.getItem().saveOptional(world.registryAccess()).toString()); sample.addProperty("uuidLookupIsSource", world.getEntity(source.getUUID()) == source); sample.addProperty("entitiesLoaded", world.areEntitiesLoaded(net.minecraft.world.level.ChunkPos.asLong(pos))); sample.addProperty("blockAndEntitiesTicking", Native118ChunkTicking.withEntitiesLoaded(world, pos)); @@ -168,7 +168,7 @@ private static JsonObject hammerSnapshot(ServerLevel world, ServerPlayer player, sample.addProperty("creative", player.isCreative()); sample.addProperty("shift", player.isShiftKeyDown()); sample.addProperty("hammerClass", player.getMainHandItem().getItem().getClass().getName()); - sample.addProperty("hammerNbt", player.getMainHandItem().save(new net.minecraft.nbt.CompoundTag()).toString()); + sample.addProperty("hammerNbt", player.getMainHandItem().saveOptional(world.registryAccess()).toString()); sample.addProperty("hammerDamage", player.getMainHandItem().getDamageValue()); sample.addProperty("hammerMaxDamage", player.getMainHandItem().getMaxDamage()); JsonArray items = new JsonArray(); @@ -180,7 +180,7 @@ private static JsonObject hammerSnapshot(ServerLevel world, ServerPlayer player, item.addProperty("isSource", entity == source); item.addProperty("removed", entity.isRemoved()); item.addProperty("bounds", entity.getBoundingBox().toString()); - item.addProperty("stackNbt", entity.getItem().save(new net.minecraft.nbt.CompoundTag()).toString()); + item.addProperty("stackNbt", entity.getItem().saveOptional(world.registryAccess()).toString()); items.add(item); } sample.addProperty("sourceInAabb", sourceInAabb); @@ -192,7 +192,7 @@ static boolean canCrush(Map known, ItemStack input) { if (input.getItem() instanceof BlockItem) { for (String id : known.keySet()) { if (id.startsWith("basemetals:") && id.endsWith("_crackhammer") - && stack(id).getItem().isCorrectToolForDrops(((BlockItem) input.getItem()).getBlock().defaultBlockState())) return true; + && stack(id).isCorrectToolForDrops(((BlockItem) input.getItem()).getBlock().defaultBlockState())) return true; } return (has(known, "basesteam:steam_crusher", "basesteam:steam_boiler_coal", "minecraft:water_bucket")) || has(known, "baseelectric:electric_crusher", "baseelectric:hydroelectric_generator", "minecraft:water_bucket"); 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 db463bd9..ab98d3d8 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 @@ -20,7 +20,7 @@ import java.util.Random; import java.util.Set; import java.util.UUID; -import net.minecraft.advancements.Advancement; +import net.minecraft.advancements.AdvancementHolder; import net.minecraft.advancements.CriteriaTriggers; import net.minecraft.commands.Commands; import net.minecraft.server.level.ServerPlayer; @@ -31,6 +31,7 @@ import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeHolder; import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.network.protocol.PacketFlow; import net.minecraft.server.network.ServerGamePacketListenerImpl; @@ -81,16 +82,16 @@ private void audit(MinecraftServer server, ServerLevel world, String label) thro JsonObject fixture = JSON.fromJson(new String(Files.readAllBytes(new File("progression-fixture.json").toPath()), StandardCharsets.UTF_8), JsonObject.class); ServerPlayer player = new ServerPlayer(server, world, new GameProfile( - UUID.fromString("3ea512a5-4c41-42ac-901f-7fabd2b1cbde"), "ProgressionProbe")); - player.connection = new ServerGamePacketListenerImpl(server, new Connection(PacketFlow.SERVERBOUND), player) { + UUID.fromString("3ea512a5-4c41-42ac-901f-7fabd2b1cbde"), "ProgressionProbe"), net.minecraft.server.level.ClientInformation.createDefault()); + player.connection = new ServerGamePacketListenerImpl(server, new Connection(PacketFlow.SERVERBOUND), player, WorksOfflinePlayer.cookie(player)) { @Override public void send(Packet packet) {} }; - for (Advancement advancement : server.getAdvancements().getAllAdvancements()) { - if (owned(advancement.getId())) for (String criterion : advancement.getCriteria().keySet()) + for (AdvancementHolder advancement : server.getAdvancements().getAllAdvancements()) { + if (owned(advancement.id())) for (String criterion : advancement.value().criteria().keySet()) player.getAdvancements().revoke(advancement, criterion); } - List> oldBook = new ArrayList<>(); - for (Recipe recipe : server.getRecipeManager().getRecipes()) if (owned(recipe.getId())) oldBook.add(recipe); + List> oldBook = new ArrayList<>(); + for (RecipeHolder recipe : server.getRecipeManager().getRecipes()) if (owned(recipe.id())) oldBook.add(recipe); player.resetRecipes(oldBook); ProcessProgression.reset(); Map known = new LinkedHashMap<>(); @@ -101,7 +102,7 @@ private void audit(MinecraftServer server, ServerLevel world, String label) thro if (item != null) acquire(known, player, new ItemStack(item), "natural_resource", Collections.emptyList()); } JsonArray loot = new JsonArray(); - var overriddenTable = server.getLootData().getLootTable(new ResourceLocation("minecraft:chests/simple_dungeon")); + var overriddenTable = lootTable(server, "minecraft:chests/simple_dungeon"); LootParams chest = chestParams(world); for (int seed = 0; seed < 8; seed++) { List raw = new ArrayList<>(); @@ -116,23 +117,23 @@ private void audit(MinecraftServer server, ServerLevel world, String label) thro } if (PowerAdvantage.recipeMode == RecipeMode.APOCALYPTIC) { for (int i = 0; i < 4096; i++) { - for (ItemStack stack : server.getLootData().getLootTable( - new ResourceLocation("minecraft:chests/abandoned_mineshaft")).getRandomItems(chest, i + 1L)) { + for (ItemStack stack : lootTable(server, "minecraft:chests/abandoned_mineshaft").getRandomItems(chest, i + 1L)) { if (advantage(stack)) { if (acquire(known, player, stack, "vanilla_chest_loot", Collections.emptyList())) loot.add(id(stack)); } } } } - List> recipes = new ArrayList<>(server.getRecipeManager().getRecipes()); - recipes.sort(Comparator.comparing(recipe -> recipe.getId().toString())); + List> recipes = new ArrayList<>(server.getRecipeManager().getRecipes()); + recipes.sort(Comparator.comparing(recipe -> recipe.id().toString())); boolean changed = true; int rounds = 0; while (changed) { require(++rounds <= 100, "Progression closure did not converge"); changed = false; - for (Recipe recipe : recipes) { - if (executed.contains(recipe.getId().toString()) || recipe.getIngredients().isEmpty()) continue; + for (RecipeHolder holder : recipes) { + Recipe recipe = holder.value(); + if (executed.contains(holder.id().toString()) || recipe.getIngredients().isEmpty()) continue; String type = net.minecraft.core.registries.BuiltInRegistries.RECIPE_TYPE.getKey(recipe.getType()).toString(); boolean crafting = recipe.getType() == RecipeType.CRAFTING; boolean smelting = recipe.getType() == RecipeType.SMELTING; @@ -173,20 +174,20 @@ private void audit(MinecraftServer server, ServerLevel world, String label) thro result = matchedResult(recipe, grid, world); } if (result.isEmpty()) continue; - if (owned(recipe.getId()) && !crushing) { - Advancement unlock = server.getAdvancements().getAdvancement(new ResourceLocation( - recipe.getId().getNamespace(), "recipes/" + recipe.getId().getPath())); + if (owned(holder.id()) && !crushing) { + AdvancementHolder unlock = server.getAdvancements().get(new ResourceLocation( + holder.id().getNamespace(), "recipes/" + holder.id().getPath())); require(unlock != null && player.getAdvancements().getOrStartProgress(unlock).isDone() - && player.getRecipeBook().contains(recipe), "Manufacture lacks ingredient discovery: " + recipe.getId()); + && player.getRecipeBook().contains(holder), "Manufacture lacks ingredient discovery: " + holder.id()); } - if (owned(recipe.getId()) && smelting) - ProcessProgression.smelt(world, fixture, recipe.getId().toString(), ingredients.get(0), result); - if (owned(recipe.getId()) && crushing && !(ingredients.get(0).getItem() instanceof net.minecraft.world.item.BlockItem)) - ProcessProgression.hammer(world, player, known, fixture, recipe.getId().toString(), ingredients.get(0), result); - executed.add(recipe.getId().toString()); + if (owned(holder.id()) && smelting) + ProcessProgression.smelt(world, fixture, holder.id().toString(), ingredients.get(0), result); + if (owned(holder.id()) && crushing && !(ingredients.get(0).getItem() instanceof net.minecraft.world.item.BlockItem)) + ProcessProgression.hammer(world, player, known, fixture, holder.id().toString(), ingredients.get(0), result); + executed.add(holder.id().toString()); List inputs = new ArrayList<>(); for (ItemStack ingredient : ingredients) if (!ingredient.isEmpty()) inputs.add(id(ingredient)); - changed |= acquire(known, player, result, recipe.getId().toString(), inputs); + changed |= acquire(known, player, result, holder.id().toString(), inputs); } changed |= ProcessProgression.advance(world, player, known, fixture); } @@ -212,10 +213,11 @@ private void audit(MinecraftServer server, ServerLevel world, String label) thro } JsonObject report = new JsonObject(); JsonArray missingRecipes = new JsonArray(); - for (Recipe recipe : recipes) { - if (!owned(recipe.getId()) || (recipe.getType() != RecipeType.CRAFTING + for (RecipeHolder holder : recipes) { + Recipe recipe = holder.value(); + if (!owned(holder.id()) || (recipe.getType() != RecipeType.CRAFTING && recipe.getType() != RecipeType.SMELTING)) continue; - if (!executed.contains(recipe.getId().toString())) missingRecipes.add(recipe.getId().toString()); + if (!executed.contains(holder.id().toString())) missingRecipes.add(holder.id().toString()); } if (missingRecipes.size() > 0) blocked += missingRecipes.size(); report.addProperty("status", blocked == 0 ? "PASS" : "FAIL"); @@ -267,6 +269,11 @@ static ItemStack stack(String 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))); + } + private static LootParams chestParams(ServerLevel world) { return new LootParams.Builder(world) .withParameter(net.minecraft.world.level.storage.loot.parameters.LootContextParams.ORIGIN, new net.minecraft.world.phys.Vec3(0, 80, 0)) 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 fd6572ec..aa1f864f 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 @@ -11,7 +11,8 @@ import net.minecraft.world.inventory.CraftingContainer; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Recipe; -import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.protocol.game.ClientboundUpdateRecipesPacket; import net.minecraft.server.MinecraftServer; import net.minecraft.resources.ResourceLocation; @@ -23,31 +24,33 @@ final class RecipeBookProbe { static void verify(MinecraftServer server, ServerLevel world, String label, JsonObject report) throws Exception { JsonArray entries = new JsonArray(); - List> recipes = new ArrayList<>(); - for (Recipe recipe : server.getRecipeManager().getRecipes()) { - String namespace = recipe.getId().getNamespace(); + List> recipes = new ArrayList<>(); + for (RecipeHolder holder : server.getRecipeManager().getRecipes()) { + Recipe recipe = holder.value(); + String namespace = holder.id().getNamespace(); if (!(namespace.equals("basepower") || namespace.equals("basesteam") || namespace.equals("baseelectric"))) continue; if (recipe.getType() != RecipeType.CRAFTING && recipe.getType() != RecipeType.SMELTING) continue; String output = id(recipe.getResultItem(world.registryAccess())); - require(output.equals(recipe.getGroup()), "Mixed-output recipe group: " + recipe.getId() + " -> " + recipe.getGroup()); + require(output.equals(recipe.getGroup()), "Mixed-output recipe group: " + holder.id() + " -> " + recipe.getGroup()); JsonObject entry = new JsonObject(); - entry.addProperty("id", recipe.getId().toString()); + entry.addProperty("id", holder.id().toString()); entry.addProperty("group", recipe.getGroup()); entry.addProperty("type", net.minecraft.core.registries.BuiltInRegistries.RECIPE_TYPE.getKey(recipe.getType()).toString()); entry.addProperty("output", output); entry.addProperty("count", recipe.getResultItem(world.registryAccess()).getCount()); entries.add(entry); - recipes.add(recipe); + recipes.add(holder); } report.add("bookRecipes", entries); - FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), world.registryAccess()); try { - new ClientboundUpdateRecipesPacket(recipes).write(buffer); + ClientboundUpdateRecipesPacket.STREAM_CODEC.encode(buffer, new ClientboundUpdateRecipesPacket(recipes)); byte[] bytes = new byte[buffer.readableBytes()]; buffer.readBytes(bytes); Files.write(Paths.get("recipes-" + label + ".bin"), bytes); } finally { buffer.release(); } - Recipe starch = server.getRecipeManager().byKey(new ResourceLocation("basepower:starch_from_potato")).orElse(null); + ResourceLocation starchId = new ResourceLocation("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; for (int width : new int[] {2, 3}) for (String potato : new String[] {"minecraft:potato", "minecraft:poisonous_potato"}) { @@ -57,8 +60,8 @@ static void verify(MinecraftServer server, ServerLevel world, String label, Json input.setCount(count); grid.setItem(slot, input); require(starch.matches(grid, world), "Potato recipe rejected slot/stack " + slot + "/" + count); - Recipe selected = world.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, grid, world).orElse(null); - require(selected != null && selected.getId().equals(starch.getId()), "Conflicting native potato crafting recipe"); + RecipeHolder selected = world.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, grid, world).orElse(null); + require(selected != null && selected.id().equals(starchId), "Conflicting native potato crafting recipe"); ItemStack result = starch.assemble(grid, world.registryAccess()); require(id(result).equals("basepower:starch") && result.getCount() == 1, "Starch output changed"); require(grid.getItem(slot).getCount() == count, "Result preview mutated input"); 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 76481d73..a2318bfd 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 @@ -16,14 +16,14 @@ import java.util.List; import java.util.Map; import java.util.UUID; -import net.minecraft.advancements.Advancement; +import net.minecraft.advancements.AdvancementHolder; import net.minecraft.advancements.CriteriaTriggers; import net.minecraft.advancements.critereon.InventoryChangeTrigger; import net.minecraft.commands.Commands; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeHolder; import net.minecraft.network.protocol.PacketFlow; import net.minecraft.server.network.ServerGamePacketListenerImpl; import net.minecraft.network.Connection; @@ -90,10 +90,10 @@ private void run(MinecraftServer server, ServerLevel world, String operation, St StandardCharsets.UTF_8), JsonObject.class); if (player == null) { player = new ServerPlayer(server, world, new GameProfile( - UUID.fromString("b9bcb1ba-e8e3-44ec-a729-ef70587dc453"), "RecipeProbe")); + UUID.fromString("b9bcb1ba-e8e3-44ec-a729-ef70587dc453"), "RecipeProbe"), net.minecraft.server.level.ClientInformation.createDefault()); // Real server player and recipe book, with a sink instead of a remote client. transport = nativeTransport(); - player.connection = new ServerGamePacketListenerImpl(server, transport, player) { + player.connection = new ServerGamePacketListenerImpl(server, transport, player, WorksOfflinePlayer.cookie(player)) { @Override public void send(Packet packet) {} }; } @@ -111,8 +111,8 @@ private void run(MinecraftServer server, ServerLevel world, String operation, St for (JsonElement element : cases) { JsonObject definition = element.getAsJsonObject(); String id = definition.get("id").getAsString(); - Advancement advancement = advancement(server, id); - Recipe recipe = server.getRecipeManager().byKey(new ResourceLocation(id)).orElse(null); + AdvancementHolder advancement = advancement(server, id); + RecipeHolder recipe = server.getRecipeManager().byKey(new ResourceLocation(id)).orElse(null); JsonObject row = new JsonObject(); row.addProperty("id", id); row.addProperty("recipeActive", recipe != null); @@ -123,7 +123,7 @@ private void run(MinecraftServer server, ServerLevel world, String operation, St require(!player.getAdvancements().getOrStartProgress(advancement).isDone(), "Unrelated item discovered " + id); for (Map.Entry criterion : definition.getAsJsonObject("ingredients").entrySet()) { InventoryChangeTrigger.TriggerInstance condition = (InventoryChangeTrigger.TriggerInstance) - advancement.getCriteria().get(criterion.getKey()).getTrigger(); + advancement.value().criteria().get(criterion.getKey()).triggerInstance(); player.getInventory().clearContent(); require(!matchesInventory(condition, player.getInventory()), "Empty inventory matches " + id); player.getInventory().setItem(0, new ItemStack(item("minecraft:barrier"))); @@ -169,7 +169,7 @@ private void run(MinecraftServer server, ServerLevel world, String operation, St JsonArray additions = new JsonArray(); for (JsonElement element : cases) { JsonObject definition = element.getAsJsonObject(); - Advancement advancement = advancement(server, definition.get("id").getAsString()); + AdvancementHolder advancement = advancement(server, definition.get("id").getAsString()); if (!player.getAdvancements().getOrStartProgress(advancement).isDone()) { for (Map.Entry criterion : definition.getAsJsonObject("ingredients").entrySet()) { List choices = choices(criterion.getValue().getAsJsonObject()); @@ -189,9 +189,10 @@ private void run(MinecraftServer server, ServerLevel world, String operation, St result.add("additionalMaterials", additions); verifyDiscovered(server, cases, rows); } else if (operation.equals("restore")) { - require(server.getPlayerList().load(player) != null, "No saved test player"); + require(server.getPlayerList().load(player).isPresent(), "No saved test player"); require(transport.channel().isActive(), "Native recipe-login channel is not active"); - require(net.minecraftforge.network.NetworkHooks.getConnectionData(transport) == null, + require(net.minecraftforge.network.NetworkContext.get(transport).getType() + == net.minecraftforge.network.ConnectionType.VANILLA, "Server-only probe must not impersonate a remote modded-client handshake"); // Dispatch the real Forge event to exercise all three namespaces' restoration. MinecraftForge.EVENT_BUS.post(new PlayerEvent.PlayerLoggedInEvent(player)); @@ -212,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); - Recipe recipe = server.getRecipeManager().byKey(new ResourceLocation(id)).orElse(null); + RecipeHolder recipe = server.getRecipeManager().byKey(new ResourceLocation(id)).orElse(null); require(recipe == null || !available || player.getRecipeBook().contains(recipe), "Lost recipe reward: " + id); JsonObject row = new JsonObject(); row.addProperty("id", id); @@ -229,16 +230,16 @@ private void discover(Item item) { CriteriaTriggers.INVENTORY_CHANGED.trigger(player, player.getInventory(), player.getInventory().getItem(0)); } - private void reset(Advancement advancement, Recipe recipe) { + private void reset(AdvancementHolder advancement, RecipeHolder recipe) { player.getInventory().clearContent(); if (recipe != null) player.resetRecipes(Collections.singletonList(recipe)); - for (String criterion : advancement.getCriteria().keySet()) + for (String criterion : advancement.value().criteria().keySet()) player.getAdvancements().revoke(advancement, criterion); } - private static Advancement advancement(MinecraftServer server, String id) { + private static AdvancementHolder advancement(MinecraftServer server, String id) { ResourceLocation name = new ResourceLocation(id); - return server.getAdvancements().getAdvancement(new ResourceLocation(name.getNamespace(), "recipes/" + name.getPath())); + return server.getAdvancements().get(new ResourceLocation(name.getNamespace(), "recipes/" + name.getPath())); } private static boolean matchesInventory(InventoryChangeTrigger.TriggerInstance condition, 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 d77a698b..cde5295c 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/RedstoneReloadProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/RedstoneReloadProbe.java @@ -137,7 +137,7 @@ private void tick(TickEvent.LevelTickEvent event) { seedTrace.add(observation); boolean ready = true; for (Witness witness : witnesses) { - CompoundTag tag = tile(witness).saveWithFullMetadata(); + CompoundTag tag = WorksBlockData.save(tile(witness)); ready &= tag.getShort("progress") > 0 && tag.contains("targetX", 99) && ((PoweredEntity) tile(witness)).getEnergy(new ConduitType("electricity")) > 24; } @@ -226,7 +226,7 @@ private void construct(Witness witness) { BlockEntity tile = tile(witness); ((PoweredEntity) tile).setEnergy(500, new ConduitType("electricity")); ItemStack dirt = new ItemStack(Items.DIRT, 7); - dirt.getOrCreateTag().putString("AdvWorksWitness", witness.id); + WorksItemData.update(dirt, tag -> tag.putString("AdvWorksWitness", witness.id)); ((Container) tile).setItem(0, dirt); ((Container) tile).setItem(1, new ItemStack(Items.APPLE, 13)); tile.setChanged(); @@ -273,7 +273,7 @@ private List> snapshots() { List> result = new ArrayList<>(); for (Witness witness : witnesses) { BlockEntity tile = tile(witness); - CompoundTag nbt = tile.saveWithFullMetadata(); + CompoundTag nbt = WorksBlockData.save(tile); BlockState state = world.getBlockState(witness.pos); Map row = new LinkedHashMap<>(); row.put("id", witness.id); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamOperationalActors.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/SteamOperationalActors.java index 9fd7dd72..37d5641b 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 @@ -23,7 +23,6 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.core.BlockPos; import net.minecraft.world.level.GameType; -import net.minecraftforge.common.util.FakePlayer; import net.minecraftforge.registries.ForgeRegistries; /** Per-case entities: no shared fake-player factory identity or mutable counters. */ @@ -102,7 +101,7 @@ private static void platform(MachineContext c, int y, int height) { } private static final class Shot { - FakePlayer player; + WorksOfflinePlayer player; Cow target; ItemStack weapon; float expectedDamage; @@ -121,9 +120,9 @@ private static void musket(List cases) { BlockPos muzzle = c.at(8, 0, 8); UUID id = UUID.nameUUIDFromBytes(("SteamOperational/" + mode + "/" + muzzle) .getBytes(StandardCharsets.UTF_8)); - shot.player = new FakePlayer(c.world, new GameProfile(id, "SteamOperational")); + shot.player = new WorksOfflinePlayer(c.world, new GameProfile(id, "SteamOperational")); shot.player.connection = new ServerGamePacketListenerImpl(c.world.getServer(), - new Connection(PacketFlow.SERVERBOUND), shot.player) { + new Connection(PacketFlow.SERVERBOUND), shot.player, WorksOfflinePlayer.cookie(shot.player)) { @Override public void send(Packet packet) {} }; shot.player.gameMode.changeGameModeForPlayer(GameType.SURVIVAL); @@ -157,7 +156,7 @@ private static void musket(List cases) { shot.expectedDamage = 0; } else { c.require(reload == InteractionResult.SUCCESS, "Rapid-reload interaction succeeds"); - c.require(shot.weapon.getOrCreateTag().getByte("loaded") == 1, + c.require(WorksItemData.read(shot.weapon).getByte("loaded") == 1, "Reload interaction loads the chamber before the shot"); c.record("musket/ammo-after-reload", shot.player.getInventory().getItem(1).getCount()); c.require(shot.player.getInventory().getItem(1).getCount() == shot.expectedAmmo, @@ -182,7 +181,7 @@ private static void musket(List cases) { "Only a real shot debits one musket durability"); c.require(shot.player.getInventory().getItem(1).getCount() == shot.expectedAmmo, "Firing does not charge ammunition a second time"); - c.require(shot.weapon.getOrCreateTag().getByte("loaded") == 0, + c.require(WorksItemData.read(shot.weapon).getByte("loaded") == 0, "The chamber is empty after firing or failed unladen use"); c.require(!shot.player.isCreative(), "All ammunition and durability measurements use survival mode"); })); 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 166f6158..69c29d24 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 @@ -23,7 +23,6 @@ import net.minecraft.core.BlockPos; import net.minecraft.world.phys.Vec3; import net.minecraft.world.level.GameType; -import net.minecraftforge.common.util.FakePlayer; import net.minecraftforge.registries.ForgeRegistries; /** Eight supplemental cases included by the Steam operating catalog. */ @@ -79,7 +78,7 @@ private static void priority(List cases) { } private static final class Shot { - FakePlayer player; + WorksOfflinePlayer player; Cow impact, collateral; ItemStack weapon; float configuredDamage; @@ -160,8 +159,8 @@ private static void occlusion(List cases) { private static void prepareShot(MachineContext c, Shot shot, String id, boolean collateral) { BlockPos muzzle = c.at(16, 8, 10); UUID uuid = UUID.nameUUIDFromBytes(("SteamOperationalExtra/" + id + "/" + muzzle).getBytes(StandardCharsets.UTF_8)); - shot.player = new FakePlayer(c.world, new GameProfile(uuid, "SteamOpExtras")); - shot.player.connection = new ServerGamePacketListenerImpl(c.world.getServer(), new Connection(PacketFlow.SERVERBOUND), shot.player) { + shot.player = new WorksOfflinePlayer(c.world, new GameProfile(uuid, "SteamOpExtras")); + shot.player.connection = new ServerGamePacketListenerImpl(c.world.getServer(), new Connection(PacketFlow.SERVERBOUND), shot.player, WorksOfflinePlayer.cookie(shot.player)) { @Override public void send(Packet packet) { if (packet instanceof ClientboundSetEntityMotionPacket) shot.velocityPackets++; } @@ -208,7 +207,7 @@ private static void fire(MachineContext c, Shot shot) { c.require(weapon.use(c.world, shot.player, InteractionHand.MAIN_HAND).getResult() == InteractionResult.SUCCESS, "Native rapid reload interaction succeeds"); c.require(shot.player.getInventory().getItem(1).getCount() == 1 - && shot.weapon.getOrCreateTag().getByte("loaded") == 1, "Exactly one finite cartridge loads the chamber"); + && WorksItemData.read(shot.weapon).getByte("loaded") == 1, "Exactly one finite cartridge loads the chamber"); c.require(weapon.use(c.world, shot.player, InteractionHand.MAIN_HAND).getResult() == InteractionResult.SUCCESS && shot.player.isUsingItem(), "Native use interaction enters the aiming lifecycle"); shot.player.releaseUsingItem(); @@ -227,7 +226,7 @@ else if (explosive) c.require(shot.impact.getHealth() <= 1000 - shot.configuredD "Unenchanted damage is exactly the configured direct impact"); c.require(shot.weapon.getDamageValue() == 1 && shot.player.getInventory().getItem(1).getCount() == 1, "Each shot consumes one durability and exactly one cartridge, without duplicating debit"); - c.require(shot.weapon.getOrCreateTag().getByte("loaded") == 0, "Shot leaves an empty chamber"); + c.require(WorksItemData.read(shot.weapon).getByte("loaded") == 0, "Shot leaves an empty chamber"); inside(c, shot.impact); } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksBlockData.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksBlockData.java new file mode 100644 index 00000000..c080f900 --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksBlockData.java @@ -0,0 +1,19 @@ +package zone.moddev.mc.advantageworks; + +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +final class WorksBlockData { + private WorksBlockData() {} + + static CompoundTag save(BlockEntity tile) { + return tile.saveWithFullMetadata(tile.getLevel().registryAccess()); + } + + static AABB bounds(BlockPos min, BlockPos maxExclusive) { + return new AABB(Vec3.atLowerCornerOf(min), Vec3.atLowerCornerOf(maxExclusive)); + } +} 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 b8dfa63b..9b52a428 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 @@ -9,7 +9,6 @@ import net.minecraft.world.level.block.LiquidBlock; import net.minecraft.world.level.block.StandingSignBlock; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.commands.CommandRuntimeException; import net.minecraft.world.item.enchantment.Enchantment; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; @@ -82,21 +81,21 @@ static WorksController getInstance() { synchronized void loadDefinition() { if (definition != null) return; - definition = WorksDefinitionLoader.loadNative1201(); + definition = WorksDefinitionLoader.loadNative1206(); for (WorksDefinition.Station station : definition.stations) { stations.put(station.id.toUpperCase(Locale.ROOT), station); statuses.put(station.id, "NOT_BUILT"); } } - void requireAuthorizedWorld(MinecraftServer server) throws CommandRuntimeException { + void requireAuthorizedWorld(MinecraftServer server) throws IllegalStateException { File marker = new File("advantage-works.allow"); if (!WorksWorldGuard.authorized(server.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT), marker.isFile())) { throw commandError("Refusing to alter an unmarked world. Expected AdvantageWorks level and advantage-works.allow."); } } - synchronized String build(MinecraftServer server, String target) throws CommandRuntimeException { + synchronized String build(MinecraftServer server, String target) throws IllegalStateException { loadDefinition(); ServerLevel world = overworld(server); if ("all".equalsIgnoreCase(target)) buildConcourse(world); @@ -118,7 +117,7 @@ synchronized String build(MinecraftServer server, String target) throws CommandR return "Advantage Works built " + built + " station(s); skipped " + skipped + " for missing mods."; } - synchronized String reset(MinecraftServer server, String target) throws CommandRuntimeException { + synchronized String reset(MinecraftServer server, String target) throws IllegalStateException { List selected = select(target, true); ServerLevel world = overworld(server); int reset = 0; @@ -133,7 +132,7 @@ synchronized String reset(MinecraftServer server, String target) throws CommandR return "Reset " + reset + " station(s)."; } - synchronized String start(MinecraftServer server, String target) throws CommandRuntimeException { + synchronized String start(MinecraftServer server, String target) throws IllegalStateException { WorksDefinition.Station station = oneStation(target); if (!requirementsPresent(station)) { statuses.put(station.id, "SKIP"); @@ -152,7 +151,7 @@ synchronized String start(MinecraftServer server, String target) throws CommandR return "Started " + station.id + " - " + station.name; } - synchronized String stop(MinecraftServer server, String target) throws CommandRuntimeException { + synchronized String stop(MinecraftServer server, String target) throws IllegalStateException { WorksDefinition.Station station = oneStation(target); ServerLevel world = overworld(server); if (requirementsPresent(station)) { @@ -179,7 +178,7 @@ private static void setStationChunksForced(ServerLevel world, } } - synchronized String check(MinecraftServer server, String target) throws CommandRuntimeException { + synchronized String check(MinecraftServer server, String target) throws IllegalStateException { List selected = select(target, true); ServerLevel world = overworld(server); Map summary = new LinkedHashMap<>(); @@ -195,13 +194,13 @@ synchronized String check(MinecraftServer server, String target) throws CommandR return "Advantage Works check: " + summary; } - synchronized String status(String target) throws CommandRuntimeException { + synchronized String status(String target) throws IllegalStateException { if ("all".equalsIgnoreCase(target)) return statuses.toString(); WorksDefinition.Station station = oneStation(target); return station.id + " " + station.name + ": " + statuses.get(station.id); } - synchronized String checkpoint(MinecraftServer server, String target, String name) throws CommandRuntimeException { + synchronized String checkpoint(MinecraftServer server, String target, String name) throws IllegalStateException { WorksDefinition.Station station = oneStation(target); Map payload = new LinkedHashMap<>(); payload.put("checkpoint", name); @@ -211,7 +210,7 @@ synchronized String checkpoint(MinecraftServer server, String target, String nam return "Checkpoint written for " + station.id + " to " + result.getPath(); } - synchronized String sampleWorldgen(MinecraftServer server, int radius) throws CommandRuntimeException { + synchronized String sampleWorldgen(MinecraftServer server, int radius) throws IllegalStateException { if (radius < 1 || radius > 16) { throw commandError("Worldgen sample radius must be between 1 and 16 chunks"); } @@ -355,7 +354,7 @@ private List> snapshot(ServerLevel world, WorksDefinition.St block.put("block", name == null ? "unknown" : name.toString()); block.put("state", state.toString()); BlockEntity tile = world.getBlockEntity(pos); - if (tile != null) block.put("nbt", tile.saveWithFullMetadata().toString()); + if (tile != null) block.put("nbt", WorksBlockData.save(tile).toString()); snapshot.add(block); } return snapshot; @@ -394,7 +393,7 @@ private boolean evaluateAssertion(ServerLevel world, WorksDefinition.Station sta for (int[] position : assertion.positions) { BlockEntity source = world.getBlockEntity(absolute(station, position)); if (source == null) return false; - total += AssertionChecks.fluidAmount(source.saveWithFullMetadata(), assertion.nbtPath, assertion.fluid); + total += AssertionChecks.fluidAmount(WorksBlockData.save(source), assertion.nbtPath, assertion.fluid); } return compare(total, assertion.operator, assertion.value); } @@ -416,9 +415,8 @@ private boolean evaluateAssertion(ServerLevel world, WorksDefinition.Station sta } if ("comparatorOutput".equals(assertion.type)) { BlockState state = world.getBlockState(pos); - Block source = state.getBlock(); - return source.hasAnalogOutputSignal(state) - && compare(source.getAnalogOutputSignal(state, world, pos), + return state.hasAnalogOutputSignal() + && compare(state.getAnalogOutputSignal(world, pos), assertion.operator, assertion.value); } BlockEntity tile = world.getBlockEntity(pos); @@ -436,24 +434,24 @@ && compare(source.getAnalogOutputSignal(state, world, pos), Container inventory = (Container) tile; if (assertion.slot < 0 || assertion.slot >= inventory.getContainerSize()) return false; ItemStack stack = inventory.getItem(assertion.slot); - if (stack.isEmpty() || !stack.hasTag()) return false; - Number number = numberAt(stack.getTag(), assertion.nbtPath); + if (stack.isEmpty()) return false; + Number number = numberAt(WorksItemData.read(stack), assertion.nbtPath); return number != null && compare(number.doubleValue(), assertion.operator, assertion.value); } if ("nbtNumber".equals(assertion.type)) { if (tile == null) return false; - CompoundTag nbt = tile.saveWithFullMetadata(); + CompoundTag nbt = WorksBlockData.save(tile); Number number = numberAt(nbt, assertion.nbtPath); return number != null && compare(number.doubleValue(), assertion.operator, assertion.value); } if ("nbtString".equals(assertion.type)) { if (tile == null) return false; - String text = stringAt(tile.saveWithFullMetadata(), assertion.nbtPath); + String text = stringAt(WorksBlockData.save(tile), assertion.nbtPath); return assertion.expectedText != null && assertion.expectedText.equals(text); } if ("energy".equals(assertion.type)) { if (tile == null) return false; - int[] energy = tile.saveWithFullMetadata().getIntArray("Energy"); + int[] energy = WorksBlockData.save(tile).getIntArray("Energy"); int index = assertion.energyIndex == null ? 0 : assertion.energyIndex; return index >= 0 && index < energy.length && compare(Float.intBitsToFloat(energy[index]), assertion.operator, assertion.value); @@ -522,7 +520,7 @@ private void removeTaggedEntities(ServerLevel world, WorksDefinition.Station sta BlockPos min = absolute(station, station.boundsMin); BlockPos max = absolute(station, station.boundsMax); for (Entity entity : world.getEntitiesOfClass(Entity.class, - new AABB(min, max.offset(1, 1, 1)))) { + WorksBlockData.bounds(min, max.offset(1, 1, 1)))) { if (StationOwnership.matches(entity, station.id)) { entity.discard(); } @@ -629,7 +627,7 @@ private void applyActions(ServerLevel world, WorksDefinition.Station station, ItemStack stack = itemStack(stackItem, action.count, action.damage); if (action.nbt != null) { try { - stack.setTag(JsonNbtConverter.toCompound(action.nbt)); + WorksItemData.set(stack, JsonNbtConverter.toCompound(action.nbt)); } catch (Exception ex) { throw new IllegalStateException("Cannot parse item NBT for " + action.item, ex); } @@ -749,20 +747,20 @@ private void mergeTileNbt(ServerLevel world, BlockPos pos, JsonObject supplied) BlockEntity tile = world.getBlockEntity(pos); if (tile == null) throw new IllegalStateException("No tile entity at " + pos); try { - CompoundTag base = tile.saveWithFullMetadata(); + CompoundTag base = WorksBlockData.save(tile); CompoundTag additions = JsonNbtConverter.toCompound(supplied); base.merge(additions); base.putInt("x", pos.getX()); base.putInt("y", pos.getY()); base.putInt("z", pos.getZ()); - tile.load(base); + tile.loadWithComponents(base, world.registryAccess()); tile.setChanged(); } catch (Exception ex) { throw new IllegalStateException("Cannot merge tile NBT at " + pos, ex); } } - private List select(String target, boolean allowGroups) throws CommandRuntimeException { + private List select(String target, boolean allowGroups) throws IllegalStateException { loadDefinition(); if ("all".equalsIgnoreCase(target)) return new ArrayList<>(stations.values()); if (allowGroups) { @@ -778,7 +776,7 @@ private List select(String target, boolean allowGroups) return Collections.singletonList(oneStation(target)); } - private WorksDefinition.Station oneStation(String id) throws CommandRuntimeException { + private WorksDefinition.Station oneStation(String id) throws IllegalStateException { loadDefinition(); WorksDefinition.Station station = stations.get(id.toUpperCase(Locale.ROOT)); if (station == null) throw commandError("Unknown Advantage Works station " + id); @@ -856,7 +854,7 @@ private static void tagEntity(Entity entity, String stationId) { entity.getPersistentData().putString("AdvantageWorksStation", stationId); } - private static ServerLevel overworld(MinecraftServer server) throws CommandRuntimeException { + private static ServerLevel overworld(MinecraftServer server) throws IllegalStateException { ServerLevel world = server.getLevel(Level.OVERWORLD); if (world == null) throw commandError("Overworld is not loaded"); return world; @@ -878,8 +876,8 @@ private static Item item(String name) { return ForgeRegistries.ITEMS.containsKey(key) ? ForgeRegistries.ITEMS.getValue(key) : null; } - private static CommandRuntimeException commandError(String message) { - return new CommandRuntimeException(Component.literal(message)); + private static IllegalStateException commandError(String message) { + return new IllegalStateException(message); } private static BlockState legacyState(String name, Block block, int metadata) { 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 9125ad24..85a340cc 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 @@ -42,6 +42,10 @@ static WorksDefinition loadNative1201() { return load("/assets/advantageworkstest/works/works-1.20.1.json", "1.20.1"); } + static WorksDefinition loadNative1206() { + return load("/assets/advantageworkstest/works/works-1.20.6.json", "1.20.6"); + } + static WorksDefinition load(String resource, String minecraft) { JsonObject target = readResource(resource); if (target.get("format").getAsInt() != 1 || !minecraft.equals(target.get("minecraft").getAsString()) diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksItemData.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksItemData.java new file mode 100644 index 00000000..53591402 --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksItemData.java @@ -0,0 +1,23 @@ +package zone.moddev.mc.advantageworks; + +import java.util.function.Consumer; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; + +final class WorksItemData { + private WorksItemData() {} + + static CompoundTag read(ItemStack stack) { + return stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); + } + + static void update(ItemStack stack, Consumer change) { + CustomData.update(DataComponents.CUSTOM_DATA, stack, change); + } + + static void set(ItemStack stack, CompoundTag data) { + CustomData.set(DataComponents.CUSTOM_DATA, stack, data); + } +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksOfflinePlayer.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksOfflinePlayer.java new file mode 100644 index 00000000..db7ef761 --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksOfflinePlayer.java @@ -0,0 +1,40 @@ +package zone.moddev.mc.advantageworks; + +import com.mojang.authlib.GameProfile; +import java.util.UUID; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.server.level.ClientInformation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.network.ServerGamePacketListenerImpl; + +/** Forge 50 no longer ships FakePlayer. These actors never join the player list. */ +class WorksOfflinePlayer extends ServerPlayer { + private static final GameProfile SENTINEL = new GameProfile( + UUID.fromString("ec003652-141d-4c8d-b575-25381335af83"), "AdvWorksSentinel"); + + WorksOfflinePlayer(ServerLevel level, GameProfile profile) { + super(level.getServer(), level, profile, ClientInformation.createDefault()); + connection = new ServerGamePacketListenerImpl(level.getServer(), + new Connection(PacketFlow.SERVERBOUND), this, cookie(this)) { + @Override public void send(Packet packet) {} + }; + } + + static CommonListenerCookie cookie(ServerPlayer player) { + return CommonListenerCookie.createInitial(player.getGameProfile(), false); + } + + static WorksOfflinePlayer sentinel(ServerLevel level) { + for (ServerPlayer player : level.players()) { + if (player instanceof WorksOfflinePlayer actor && actor.getUUID().equals(SENTINEL.getId())) return actor; + } + return new WorksOfflinePlayer(level, SENTINEL); + } + + @Override public void tick() {} + @Override public void doTick() {} +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksResultWriter.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksResultWriter.java index 6b4530b0..ccc9f844 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksResultWriter.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksResultWriter.java @@ -31,7 +31,7 @@ File write(String stationId, String kind, Map payload) { Map document = new LinkedHashMap<>(); document.put("format", 1); document.put("recordedAtUtc", utcNow()); - document.put("minecraft", "1.20.1"); + document.put("minecraft", net.minecraft.SharedConstants.getCurrentVersion().getName()); document.put("forge", net.minecraftforge.versions.forge.ForgeVersion.getVersion()); document.put("mods", loadedMods()); document.put("station", stationId); diff --git a/test-rig/src/main/resources/META-INF/mods.toml b/test-rig/src/main/resources/META-INF/mods.toml index 60dcefb7..239f6b86 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="[47,48)" +loaderVersion="[50,51)" 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.12.120011,3.0.0)" +versionRange="[2.4.13.120061,3.0.0)" ordering="AFTER" side="SERVER" [[dependencies.advantageworkstest]] modId="forge" mandatory=true -versionRange="[47.4.10,48)" +versionRange="[50.2.0,51)" ordering="NONE" side="SERVER" [[dependencies.advantageworkstest]] modId="minecraft" mandatory=true -versionRange="[1.20.1]" +versionRange="[1.20.6]" ordering="NONE" side="SERVER" diff --git a/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.20.6.json b/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.20.6.json new file mode 100644 index 00000000..21fbbc1e --- /dev/null +++ b/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.20.6.json @@ -0,0 +1,6 @@ +{ + "format": 1, + "minecraft": "1.20.6", + "title": "Advantage Works 1.20.6 Commissioning Ground", + "stationSource": "/assets/advantageworkstest/works/works-1.13.2.json" +} diff --git a/test-rig/src/main/resources/pack.mcmeta b/test-rig/src/main/resources/pack.mcmeta index d0d62496..a84f888e 100644 --- a/test-rig/src/main/resources/pack.mcmeta +++ b/test-rig/src/main/resources/pack.mcmeta @@ -1,8 +1,8 @@ { "pack": { - "description": "Advantage Works 1.19.4 test resources", - "pack_format": 15, - "forge:resource_pack_format": 15, - "forge:data_pack_format": 15 + "description": "Advantage Works 1.20.6 test resources", + "pack_format": 32, + "forge:resource_pack_format": 32, + "forge:data_pack_format": 41 } } 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 f1e20335..de8f8f02 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,13 +67,14 @@ private WorksRunner() { public static void main(String[] args) throws Exception { Arguments arguments = Arguments.parse(args); - Profile profile = readNative1201Profile(arguments.profile); + Profile profile = readNative1206Profile(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); if (arguments.validateOnly) { List> inputs = stageMods(testRig, null, profile.mods); if ("packaged".equals(profile.mode)) { @@ -86,7 +87,10 @@ public static void main(String[] args) throws Exception { String stamp = timestamp(); File runtimeBase = resolve(testRig, profile.runtimeBase); File runtime = new File(runtimeBase, profile.name + "-" + stamp).getCanonicalFile(); - requireChild(powerRoot, runtime); + 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"); + } if (profile.preparedSeed != null) profile.preparedSeed.requireDisjoint(runtime); if (!runtime.mkdirs()) throw new IllegalStateException("Cannot create runtime " + runtime); @@ -315,6 +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"; + 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"); + } + return; + } boolean native118 = launcher.startsWith("libraries/net/minecraftforge/forge/1.18.2-40.3.0/"); boolean native1201 = launcher.startsWith("libraries/net/minecraftforge/forge/1.20.1-47.4.10/"); Set expected = new LinkedHashSet<>(Arrays.asList( @@ -730,6 +745,9 @@ private static List> stageMods(File testRig, File mods, List if (input.verifyIdentity) verifyModIdentity(source, input.id, input.version); File destination = mods == null ? source : new File(mods, source.getName()); if (mods != null) Files.copy(source.toPath(), destination.toPath(), StandardCopyOption.COPY_ATTRIBUTES); + if (mods != null && !hash.equals(sha256(destination))) { + throw new IllegalStateException("Mod input changed while staging: " + input.id); + } Map record = new LinkedHashMap<>(); record.put("id", input.id); record.put("version", input.version); @@ -786,7 +804,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.1".equals(profile.minecraftVersion) + serverLauncherPath(profile), "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" : "1.18.2".equals(profile.minecraftVersion) ? "libraries/net/minecraft/server/1.18.2-20220404.173914/server-1.18.2-20220404.173914-extra.jar" @@ -833,9 +853,11 @@ static final class PreparedServerSeed { final Map files = new LinkedHashMap<>(); final Set directories = new LinkedHashSet<>(); final List processes = new ArrayList<>(); + final boolean forge50; 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); this.root = root.toAbsolutePath().normalize(); this.inventory = inventory.toAbsolutePath().normalize(); if (expectedHash == null || !expectedHash.matches("[0-9A-F]{64}")) { @@ -903,6 +925,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 (reader.hasNext()) throw new IllegalArgumentException("Unexpected inventory root field"); reader.endObject(); if (reader.peek() != JsonToken.END_DOCUMENT) throw new IllegalArgumentException("Trailing inventory data"); @@ -955,6 +980,11 @@ 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)) { + requirePlain(path); + return FileVisitResult.CONTINUE; + } if (!codeOnly && !relative.startsWith("libraries/") && !relative.toLowerCase(Locale.ROOT).endsWith(".jar")) { return FileVisitResult.CONTINUE; } @@ -1104,11 +1134,24 @@ 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.1".equals(profile.minecraftVersion) || "1.20.6".equals(profile.minecraftVersion); } private static File runtimeJavaHome(Profile profile) throws Exception { if (!modularForge(profile)) return java8Home(profile); + if ("1.20.6".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() + ? System.getProperty("java.home") : expand(configured)).getCanonicalFile(); + java.util.Properties release = new java.util.Properties(); + try (InputStream input = Files.newInputStream(new File(home, "release").toPath())) { + release.load(input); + } + if (!javaExecutable(home).isFile() || !release.getProperty("JAVA_VERSION", "").matches("\"21(?:\\.[0-9]+)*\"")) + throw new IllegalStateException("Set ADVANTAGE_WORKS_JAVA21_HOME to a Java 21 runtime: " + home); + return home; + } boolean native1201 = "1.20.1".equals(profile.minecraftVersion); boolean java17 = native1201 || "1.18.2".equals(profile.minecraftVersion); String variable = java17 ? "ADVANTAGE_WORKS_JAVA17_HOME" : "ADVANTAGE_WORKS_JAVA16_HOME"; @@ -1208,7 +1251,7 @@ static void writeRuntimeFiles(File runtime, Profile profile) throws Exception { + "level-seed=" + profile.levelSeed + "\n" + "level-type=" + profile.levelType + "\n" + "max-players=8\n" - + "motd=Advantage Works 1.16.5 Commissioning Ground\n" + + "motd=Advantage Works " + profile.minecraftVersion + " Commissioning Ground\n" + "online-mode=false\n" + "server-ip=127.0.0.1\n" + "pvp=false\n" @@ -1281,6 +1324,42 @@ static Profile readNative1201Profile(File file) throws Exception { throw new IllegalArgumentException("Non-1.20.1 destination mod version: " + input.id); } } + return withFlatSettings(profile); + } + + static Profile readNative1206Profile(File file) throws Exception { + Profile profile = readProfile(file, "1.20.6", "1.20.6-50.2.0"); + if (!"EXPLORATORY_UNQUALIFIED".equals(profile.qualificationStatus) || !"packaged".equals(profile.mode)) { + throw new IllegalArgumentException("Forge 50 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") + || 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); + } + } + if (!ids.containsAll(Set.of("basepower", "advantageworkstest", "orespawn"))) { + throw new IllegalArgumentException("Power, the harness and OreSpawn must be explicitly pinned"); + } + return withFlatSettings(profile); + } + + private static void requireNative1206CompilePin(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))) { + var manifest = jar.getManifest().getMainAttributes(); + if (!profile.forge.version.equals(manifest.getValue("Built-On")) + || !profile.qualificationStatus.equals(manifest.getValue("Power-Input-Status")) + || !power.sha256.equalsIgnoreCase(manifest.getValue("Power-Input-SHA256"))) { + throw new IllegalArgumentException("Staged Power must be the same ordinary jar used to compile the harness"); + } + } + } + + private static Profile withFlatSettings(Profile profile) throws Exception { if ("minecraft:flat".equals(profile.levelType)) { if (!profile.generatorSettings.isEmpty()) { throw new IllegalArgumentException("Works flat profiles must use the shared generator settings"); @@ -1826,6 +1905,7 @@ static final class Profile { String java8Home; String java16Home; String java17Home; + String java21Home; String gradleJavaHome; String recipeMode = "NORMAL"; String sourceCommit; diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/ConnectedProbeObservationTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/ConnectedProbeObservationTest.java index 0d754742..3d3c943b 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/ConnectedProbeObservationTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/ConnectedProbeObservationTest.java @@ -10,8 +10,11 @@ import java.util.List; import net.minecraft.SharedConstants; import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.RegistryAccess; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtAccounter; import net.minecraft.server.Bootstrap; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.block.Blocks; @@ -44,12 +47,11 @@ public class ConnectedProbeObservationTest { private static void observe(CompoundTag data) throws Exception { ObservedChest tile = new ObservedChest(); - Field persistent = BlockEntity.class.getDeclaredField("customPersistentData"); - persistent.setAccessible(true); - persistent.set(tile, data); - CompoundTag before = tile.saveWithFullMetadata().copy(); + tile.persistentData = data; ObservationWorld world = allocate(ObservationWorld.class); world.tile = tile; + tile.setLevel(world); + CompoundTag before = WorksBlockData.save(tile).copy(); ConnectedProbe probe = new ConnectedProbe(); Field context = ConnectedProbe.class.getDeclaredField("world"); context.setAccessible(true); @@ -63,12 +65,12 @@ private static void observe(CompoundTag data) throws Exception { definition.add("blocks", blocks); for (int observation = 0; observation < 2; observation++) { JsonObject row = probe.snapshot(definition).get(0).getAsJsonObject(); - assertSame("Observation must not allocate or replace live persistent data", data, persistent.get(tile)); + assertSame("Observation must not allocate or replace live persistent data", data, tile.persistentData); assertEquals("Observation must not call the allocating getter", 0, tile.persistentReads); - assertEquals(before, tile.saveWithFullMetadata()); + assertEquals(before, WorksBlockData.save(tile)); assertEquals(before.toString(), row.get("nbt").getAsString()); assertEquals(before, NbtIo.readCompressed(new ByteArrayInputStream( - Base64.getDecoder().decode(row.get("nbtBase64").getAsString())))); + Base64.getDecoder().decode(row.get("nbtBase64").getAsString())), NbtAccounter.unlimitedHeap())); assertEquals(data == null ? "" : data.getString("AdvConnectedWitness"), row.get("witness").getAsString()); assertEquals(data != null, before.contains("ForgeData")); } @@ -82,9 +84,20 @@ private static T allocate(Class type) throws Exception { } private static final class ObservedChest extends ChestBlockEntity { + CompoundTag persistentData; int persistentReads; ObservedChest() { super(POS, Blocks.CHEST.defaultBlockState()); } - @Override public CompoundTag getPersistentData() { persistentReads++; return super.getPersistentData(); } + public CompoundTag getPersistentData() { + persistentReads++; + if (persistentData == null) persistentData = new CompoundTag(); + return persistentData; + } + @Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + // Forge 50 no longer stores opaque ForgeData for vanilla tiles. + // This extension fixture proves the probe observes, but never allocates, the legacy witness. + if (persistentData != null) tag.put("ForgeData", persistentData.copy()); + } } private static final class ObservationWorld extends ServerLevel { @@ -92,5 +105,8 @@ private static final class ObservationWorld extends ServerLevel { private ObservationWorld() { super(null, null, null, null, null, null, null, false, 0L, List.of(), false, null); } @Override public BlockState getBlockState(BlockPos pos) { return Blocks.CHEST.defaultBlockState(); } @Override public BlockEntity getBlockEntity(BlockPos pos) { return tile; } + @Override public RegistryAccess registryAccess() { + return RegistryAccess.fromRegistryOfRegistries(net.minecraft.core.registries.BuiltInRegistries.REGISTRY); + } } } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117EntityContextTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117EntityContextTest.java index 7b701af4..5fa6e96a 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117EntityContextTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117EntityContextTest.java @@ -110,7 +110,12 @@ private static final class EntityFixtureWorld extends ServerLevel { return net.minecraft.world.flag.FeatureFlags.DEFAULT_FLAGS; } - @Override public void gameEvent(net.minecraft.world.level.gameevent.GameEvent event, + @Override public net.minecraft.core.RegistryAccess registryAccess() { + return net.minecraft.core.RegistryAccess.fromRegistryOfRegistries( + net.minecraft.core.registries.BuiltInRegistries.REGISTRY); + } + + @Override public void gameEvent(net.minecraft.core.Holder event, net.minecraft.world.phys.Vec3 pos, net.minecraft.world.level.gameevent.GameEvent.Context context) { // This isolated entity fixture has no chunks or game-event listeners. } 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 4aed260a..661d0324 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 @@ -8,6 +8,14 @@ import java.lang.reflect.Method; import java.nio.file.Files; import java.util.UUID; +import java.util.List; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.NbtAccounter; +import net.minecraft.nbt.NbtOps; +import net.minecraft.world.item.enchantment.Enchantment; +import com.mojang.serialization.Dynamic; +import net.minecraft.util.datafix.fixes.References; import net.minecraft.world.entity.player.Inventory; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.item.ItemStack; @@ -34,21 +42,44 @@ public class Native117PlayerIoTest { } }; - @BeforeClass public static void bootstrap() { net.minecraft.SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); } + private static RegistryAccess registries; + + @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(); + } + registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + } @Test public void realForgePlayerListSaveWithoutConnectionLeavesLegacyBytesUnchanged() throws Exception { for (int version : new int[] {512, 1343}) { File directory = temporary.newFolder("guard-" + version); File file = new File(directory, ID + ".dat"); CompoundTag legacy = legacy(version); - NbtIo.writeCompressed(legacy, file); + NbtIo.writeCompressed(legacy, file.toPath()); byte[] before = Files.readAllBytes(file.toPath()); PlayerList players = players(data(directory), false); Method save = PlayerList.class.getDeclaredMethod("save", ServerPlayer.class); save.setAccessible(true); save.invoke(players, player(1, 0)); assertArrayEquals(before, Files.readAllBytes(file.toPath())); - assertEquals(legacy, NbtIo.readCompressed(file)); + assertEquals(legacy, readCompressed(file)); } } @@ -57,13 +88,13 @@ public class Native117PlayerIoTest { File directory = temporary.newFolder("save-" + version); File file = new File(directory, ID + ".dat"); CompoundTag legacy = legacy(version); - NbtIo.writeCompressed(legacy, file); + NbtIo.writeCompressed(legacy, file.toPath()); byte[] before = Files.readAllBytes(file.toPath()); HeadlessPlayer player = player(1, 0); Native117PlayerIo.save(players(data(directory), false), player); assertFalse(java.util.Arrays.equals(before, Files.readAllBytes(file.toPath()))); - assertSaved(NbtIo.readCompressed(file), player, 1, 0); - assertEquals(legacy, NbtIo.readCompressed(new File(directory, ID + ".dat_old"))); + assertSaved(readCompressed(file), player, 1, 0); + assertEquals(legacy, readCompressed(new File(directory, ID + ".dat_old"))); } } @@ -73,18 +104,18 @@ public class Native117PlayerIoTest { Native117PlayerIo.save(players(data(directory), false), first); HeadlessPlayer second = player(9, 8); PlayerDataStorage reopened = data(directory); - CompoundTag saved = reopened.load(second); + CompoundTag saved = reopened.load(second).orElseThrow(); assertNotNull(saved); assertSaved(saved, second, 1, 0); Native117PlayerIo.save(players(reopened, false), second); - assertEquals(saved, NbtIo.readCompressed(new File(directory, ID + ".dat"))); + assertEquals(saved, readCompressed(new File(directory, ID + ".dat"))); } @Test public void nativeSavePreservesExactCountSlotAndAllNamedStoredEnchantments() throws Exception { File directory = temporary.newFolder("count-slot"); HeadlessPlayer player = player(3, 8); Native117PlayerIo.save(players(data(directory), false), player); - assertSaved(NbtIo.readCompressed(new File(directory, ID + ".dat")), player, 3, 8); + assertSaved(readCompressed(new File(directory, ID + ".dat")), player, 3, 8); } @Test public void connectedPlayersKeepThePlayerListSavePath() throws Exception { @@ -115,12 +146,21 @@ private static void assertSaved(CompoundTag saved, HeadlessPlayer player, int co assertEquals(1, inventory.size()); CompoundTag stack = inventory.getCompound(0); assertEquals("minecraft:enchanted_book", stack.getString("id")); - assertEquals(count, stack.getByte("Count")); + assertEquals(count, stack.getInt("count")); assertEquals(slot, stack.getByte("Slot")); assertFalse(stack.contains("Damage")); - assertEquals(book(count).getCompound("tag"), stack.getCompound("tag")); - assertEquals(5, stack.getCompound("tag").getList("StoredEnchantments", 10).size()); - assertTrue(ItemStack.matches(ItemStack.of(stack), player.items.getItem(slot))); + ItemStack decoded = ItemStack.parse(registries, stack).orElseThrow(); + var stored = decoded.get(net.minecraft.core.component.DataComponents.STORED_ENCHANTMENTS); + assertNotNull(stack.toString(), stored); + var levels = new java.util.HashMap(); + stored.entrySet().forEach(entry -> levels.put( + entry.getKey().unwrapKey().orElseThrow().location().toString(), entry.getIntValue())); + assertEquals(stack.toString(), 5, levels.size()); + assertEquals(Integer.valueOf(3), levels.get("minecraft:efficiency")); + for (String id : List.of("high_explosive", "powderless", "rapid_reload", "recoil")) { + assertEquals(Integer.valueOf(1), levels.get("basesteam:" + id)); + } + assertTrue(ItemStack.matches(decoded, player.items.getItem(slot))); assertEquals(inventory, player.items.save(new ListTag())); } @@ -151,10 +191,17 @@ private static HeadlessPlayer player(int count, int slot) throws Exception { HeadlessPlayer player = allocate(HeadlessPlayer.class); player.setUUID(ID); player.items = new Inventory(player); - player.items.setItem(slot, ItemStack.of(book(count))); + CompoundTag nativeBook = (CompoundTag) DataFixers.getDataFixer().update(References.ITEM_STACK, + new Dynamic<>(NbtOps.INSTANCE, book(count)), 3465, + SharedConstants.getCurrentVersion().getDataVersion().getVersion()).getValue(); + player.items.setItem(slot, ItemStack.parse(registries, nativeBook).orElseThrow()); return player; } + private static CompoundTag readCompressed(File file) throws Exception { + return NbtIo.readCompressed(file.toPath(), NbtAccounter.unlimitedHeap()); + } + private static PlayerDataStorage data(File directory) throws Exception { PlayerDataStorage data = allocate(PlayerDataStorage.class); set(PlayerDataStorage.class, data, "playerDir", directory); @@ -185,7 +232,8 @@ private static T allocate(Class type) throws Exception { // PlayerData save/load, compressed files, DFU and ItemStacks are native. private static final class HeadlessPlayer extends ServerPlayer { Inventory items; - private HeadlessPlayer() { super(null, null, new GameProfile(ID, "Headless")); } + private HeadlessPlayer() { super(null, null, new GameProfile(ID, "Headless"), net.minecraft.server.level.ClientInformation.createDefault()); } + @Override public RegistryAccess registryAccess() { return registries; } @Override public CompoundTag saveWithoutId(CompoundTag root) { root.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion()); root.putUUID("UUID", getUUID()); diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotTest.java index 58133b4d..6abc8d24 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotTest.java @@ -60,7 +60,7 @@ public class Native117StoppedEntitiesSnapshotTest { JsonObject row = Native117StoppedEntitiesSnapshot.entities(world, List.of(entity), new HashSet<>()).get(0).getAsJsonObject(); CompoundTag captured = decode(row); assertEquals(before, captured); - assertEquals(7, captured.getCompound("Item").getByte("Count")); + assertEquals(7, captured.getCompound("Item").getInt("count")); assertEquals("keep exactly", captured.getCompound("ForgeData").getString("Private")); assertEquals("minecraft:the_nether", row.get("worldDimensionKey").getAsString()); assertEquals(entity.getUUID().toString(), row.get("uuid").getAsString()); @@ -151,6 +151,7 @@ public class Native117StoppedEntitiesSnapshotTest { } private static CompoundTag decode(JsonObject row) throws Exception { - return NbtIo.readCompressed(new ByteArrayInputStream(Base64.getDecoder().decode(row.get("nbtBase64").getAsString()))); + return NbtIo.readCompressed(new ByteArrayInputStream(Base64.getDecoder().decode(row.get("nbtBase64").getAsString())), + net.minecraft.nbt.NbtAccounter.unlimitedHeap()); } } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118EngineSignaturesTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118EngineSignaturesTest.java index 90c588eb..4b9f094e 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118EngineSignaturesTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native118EngineSignaturesTest.java @@ -19,9 +19,10 @@ public class Native118EngineSignaturesTest { @Test public void nativeChunkRegistryAndTileSignaturesAreExact() throws Exception { assertEquals(LevelChunk.class, ServerChunkCache.class.getMethod("getChunkNow", int.class, int.class).getReturnType()); assertEquals(ServerLevel.class, MinecraftServer.class.getMethod("getLevel", ResourceKey.class).getReturnType()); - assertEquals(void.class, BlockEntity.class.getMethod("load", CompoundTag.class).getReturnType()); - assertEquals(CompoundTag.class, BlockEntity.class.getMethod("saveWithFullMetadata").getReturnType()); - assertTrue(Modifier.isProtected(BlockEntity.class.getDeclaredMethod("saveAdditional", CompoundTag.class).getModifiers())); + Class registryProvider = net.minecraft.core.HolderLookup.Provider.class; + assertEquals(void.class, BlockEntity.class.getMethod("loadWithComponents", CompoundTag.class, registryProvider).getReturnType()); + assertEquals(CompoundTag.class, BlockEntity.class.getMethod("saveWithFullMetadata", registryProvider).getReturnType()); + assertTrue(Modifier.isProtected(BlockEntity.class.getDeclaredMethod("saveAdditional", CompoundTag.class, registryProvider).getModifiers())); } @Test public void nativePlayerSaveRemainsProtectedAndRespawnUsesRegistryKeys() throws Exception { diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native1201PackMetadataTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native1201PackMetadataTest.java index b06873da..1e2570a0 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native1201PackMetadataTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native1201PackMetadataTest.java @@ -15,9 +15,9 @@ public class Native1201PackMetadataTest { JsonObject pack = new JsonParser().parse(new InputStreamReader(input, StandardCharsets.UTF_8)) .getAsJsonObject().getAsJsonObject("pack"); assertTrue(pack.getAsJsonPrimitive("pack_format").isNumber()); - assertEquals(15, pack.get("pack_format").getAsInt()); - assertEquals(15, pack.get("forge:resource_pack_format").getAsInt()); - assertEquals(15, pack.get("forge:data_pack_format").getAsInt()); + assertEquals(32, pack.get("pack_format").getAsInt()); + assertEquals(32, pack.get("forge:resource_pack_format").getAsInt()); + assertEquals(41, pack.get("forge:data_pack_format").getAsInt()); } } } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native1201TestFlatTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native1201TestFlatTest.java index e10d01fe..c55228a2 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native1201TestFlatTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native1201TestFlatTest.java @@ -59,7 +59,7 @@ public class Native1201TestFlatTest { JsonObject saved = WorldGenSettings.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, registries), new WorldGenSettings(new WorldOptions(42L, false, false), dimensions)) - .getOrThrow(false, message -> { throw new AssertionError(message); }).getAsJsonObject(); + .getOrThrow(AssertionError::new).getAsJsonObject(); assertEquals(42L, saved.get("seed").getAsLong()); assertFalse(saved.get("generate_features").getAsBoolean()); assertFalse(saved.get("bonus_chest").getAsBoolean()); From 5289e591c7fe39b022ae0e61c46cd6f8873a39df Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Tue, 10 Nov 2026 13:14:49 +0000 Subject: [PATCH 5/9] Record the 1.20.6 upgrade and endurance checks --- CHANGELOG.txt | 3 +- docs/1.20.6/PORT-STATUS.md | 65 ++++++++++++++++++++++++++++++++------ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d1c51242..4532562f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -3,9 +3,10 @@ basePower 2.4.13.120061 (Minecraft 1.20.6 local candidate) - Bring basePower to Forge 50 and Java 21 without changing the names of existing machines or items. - Read old machine inventories into Minecraft's new item components. Portable fluid tanks still keep their contents when picked up. - Move recipes, recipe unlocks and loot to the new Forge and Minecraft formats. +- Keep player datapack chest loot overrides intact. - Keep the shared upgrade hook so Base Metals and basePower can preserve old machine data before Minecraft converts the chunks. -basePower 2.4.12.120011 (Minecraft 1.20.1 local candidate) +basePower 2.4.12.120011 (Minecraft 1.20.1) - Bring basePower to Forge 47 with the same blocks, items, and machine data. - Use Minecraft's current screen rendering, creative tabs, and block properties. diff --git a/docs/1.20.6/PORT-STATUS.md b/docs/1.20.6/PORT-STATUS.md index c41c9d56..71d89964 100644 --- a/docs/1.20.6/PORT-STATUS.md +++ b/docs/1.20.6/PORT-STATUS.md @@ -1,18 +1,65 @@ # Minecraft 1.20.6 Port -This branch is a local port of the accepted 1.20.1 sources. It is not a release. -Results from older versions do not qualify this candidate. +This branch is a local port of the accepted 1.20.1 sources. It is ready for +manual checks, not a release. Results from older versions do not qualify +this candidate. The 1.20.6 build uses Forge 50.2.0 and Java 21. The old item data is read into native item components; block, item and block-entity IDs stay the same. Base Metals' shared pre-flattening hook is on a separate local branch and must be present when testing upgrades with Base Metals installed. +The pinned fix runs the legacy player-inventory migration before Minecraft +converts item tags to components, so custom names and enchantments can still +be read in their original form. The published older Base Metals build is not +the dependency used for these upgrade checks. -Before handing back for manual testing, verify the three exact jars together: +## Automated checks -- Startup on the packaged dedicated server and the combined client. -- Recipe modes, survival progression, harvest and loot, including both tank drops. -- Connected machines, chunk reload, full restarts and the timed endurance runs. -- Original Advantage and released base* worlds, using copies, with inventory, - machine state, item components and nearby entity state checked after migration. -- The Eclipse production classpath and release artifact contents. +These checks used basePower `2.4.13.120061`, baseSteam and baseElectric +`2.2.14.120061`, Forge `50.2.0` and Java 21. The shared profile used +Base Metals `3.1.2.120061` with the local migration fix and OreSpawn +`4.1.0.120061`. + +- The 21-station rig passed with and without Base Metals, skipping only + integration checks for unavailable mods. The connected + checks covered full restarts, partial chunk unloading, network bridges + and moving drills, including saved drill stock, energy and orientation. +- Original 1.10.2 Advantage, released 1.10.2 and 1.12.2 base*, and + 1.20.1 base* machine fixtures passed conversion, a full restart and + operation without rebuilding the machines. All source copies stayed + unchanged. This does not certify the other mods in a world. +- Old player inventories passed on fixtures with legal stack sizes. The + original batteries were already limited to one per stack; the deliberately + overstacked test remains a separate negative case. In the Nether and End + fixtures Minecraft changes a wolf's health from 8/20 to 16/40. A native + control without basePower reproduced that exact change. +- Recipe unlock and survival progression passed all three recipe modes + with and without rubber. The combined Eclipse client checked recipe book + grouping, assets and captions on all 39 machine screens. +- Tank drops, chest loot and player datapack overrides passed. The oil tests + covered desert and ocean deposits, legacy OreSpawn profiles, buckets, + fluid transfer and distillation. Forge Energy converter tests passed. +- The release jars contain no bundled dependencies or test code. The Power + release, sources and Javadoc jars matched on a clean build from a path + with spaces and a fresh Gradle cache. +- A fresh world and a migrated 1.20.1 world each ran for 30 minutes, + then passed a full restart and resumed operation. The separate replay + matched 33 checkpoints and 528 station snapshots per world, including + item outputs and oil conservation. + +## Manual checks + +Refresh the Gradle project in Eclipse, clean the project and start +`runClientAllAdvantage.launch` with Java 21. Start with a new world or a copy +of an existing world. Keep the original closed and backed up. + +- Check the shared creative tab, then collect raw ingredients and check that + recipes unlock in the right mode. +- Open a metal tank with water in it, check the caption and gauge, and compare + the reading after closing Minecraft completely and reopening the world. +- Check the installed hydroturbine's orientation and spin in a running world. + The client resource probe checks its registered renderer and texture; + server tests prove that the turbine operates, not what the animation looks + like on screen. +- A metal tank still drops empty. A portable tank keeps its fluid when broken + and placed again. Both keep stored fluid across a normal save and restart. From 41427a5260658fd452477c88498e7ff82552e48a Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Tue, 10 Nov 2026 14:09:35 +0000 Subject: [PATCH 6/9] Fix fluid gauge textures on Minecraft 1.20.6 --- CHANGELOG.txt | 1 + docs/1.20.6/PORT-STATUS.md | 6 + .../moddev/mc/basepower/gui/FluidTankGUI.java | 14 +- test-rig/client-resources/README.md | 8 +- .../client/TankVisualProbe.java | 187 ++++++++++++------ test-rig/client-resources/run.ps1 | 4 + test-rig/gradle.properties | 2 +- 7 files changed, 157 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4532562f..215b6c0f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -3,6 +3,7 @@ basePower 2.4.13.120061 (Minecraft 1.20.6 local candidate) - Bring basePower to Forge 50 and Java 21 without changing the names of existing machines or items. - Read old machine inventories into Minecraft's new item components. Portable fluid tanks still keep their contents when picked up. - Move recipes, recipe unlocks and loot to the new Forge and Minecraft formats. +- Fix the corrupted fluid texture in tank gauges and other machine screens. - Keep player datapack chest loot overrides intact. - Keep the shared upgrade hook so Base Metals and basePower can preserve old machine data before Minecraft converts the chunks. diff --git a/docs/1.20.6/PORT-STATUS.md b/docs/1.20.6/PORT-STATUS.md index 71d89964..5f77147d 100644 --- a/docs/1.20.6/PORT-STATUS.md +++ b/docs/1.20.6/PORT-STATUS.md @@ -49,6 +49,12 @@ Base Metals `3.1.2.120061` with the local migration fix and OreSpawn ## Manual checks +The follow-up gauge fix is client-side only. Both tanks now pass a +whole-gauge pixel comparison using water, lava, crude oil and refined oil. +The earlier layout check could pass a corrupted fill because it did not +compare those pixels. The full migration and endurance receipts describe +the earlier jar; the gauge fix has its own native client and build checks. + Refresh the Gradle project in Eclipse, clean the project and start `runClientAllAdvantage.launch` with Java 21. Start with a new world or a copy of an existing world. Keep the original closed and backed up. 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 8f173799..a963b39f 100644 --- a/src/main/java/zone/moddev/mc/basepower/gui/FluidTankGUI.java +++ b/src/main/java/zone/moddev/mc/basepower/gui/FluidTankGUI.java @@ -86,12 +86,14 @@ public static void drawFluidFilledRectangle( 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); - for (int dy = 0; dy < height; dy += 16) { - int tileHeight = Math.min(16, height - dy); - for (int dx = 0; dx < width; dx += 16) { - int tileWidth = Math.min(16, width - dx); - float u0 = sprite.getU0(), u1 = sprite.getU(tileWidth); - float v0 = sprite.getV0(), v1 = sprite.getV(tileHeight); + final int tileSize = 16; + for (int dy = 0; dy < height; dy += tileSize) { + int tileHeight = Math.min(tileSize, height - dy); + for (int dx = 0; dx < width; dx += tileSize) { + int tileWidth = Math.min(tileSize, width - dx); + // 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(); diff --git a/test-rig/client-resources/README.md b/test-rig/client-resources/README.md index 8919f8ea..b476b629 100644 --- a/test-rig/client-resources/README.md +++ b/test-rig/client-resources/README.md @@ -67,9 +67,11 @@ this is explicitly `NOT_RUN_NO_NATIVE_FIXTURES`. - Every registered Advantage item and valid block state: geometry, missing models/sprites, English names, creative-tab contents and GUI translation keys. Native fluid-block models are intentionally particle-only. -- Actual tank screen rendering, full/empty 10,000 mB update packets, fluid - capability simulation/execution and neighbour-face culling. The water-gauge - pixel at `(88,39)` must satisfy `blue > red + 20` and `blue > green + 10`. +- Portable and metal tank gauges: compare the whole gauge to an independent + native sprite render at empty, quarter, half and full. Water, lava and both + oils must show the right texture, tint and fill boundary. The probe also + checks real update packets, fluid capability simulation/execution and + neighbour-face culling for both tanks. - Native bucket fluids and fluid-renderer vertices for crude/refined oil, with water/lava controls, actual atlas sprites and tint. - With `-MachineGuiLayouts`, real screen rendering at nine window/GUI-scale diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java index 7227a662..e9e5abe0 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java @@ -1,8 +1,13 @@ package zone.moddev.mc.advantageworks.client; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import cyano.poweradvantage.api.GUIBlock; import cyano.poweradvantage.api.simple.SimpleMachineGUI; +import cyano.poweradvantage.api.simple.TileEntitySimpleFluidMachine; import io.netty.buffer.Unpooled; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.texture.TextureAtlas; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.client.Minecraft; @@ -12,6 +17,7 @@ import com.mojang.blaze3d.pipeline.RenderTarget; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluid; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.core.Direction; import net.minecraft.resources.ResourceLocation; @@ -22,14 +28,12 @@ import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; -import zone.moddev.mc.basepower.gui.FilteredFluidTankGUI; import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.common.capabilities.ForgeCapabilities; import net.minecraftforge.fluids.capability.IFluidHandler; import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction; import zone.moddev.mc.basepower.registry.MachineGUIRegistry; import zone.moddev.mc.basepower.init.Blocks; -import zone.moddev.mc.basepower.machines.fluidmachines.MetalTankTileEntity; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; @@ -37,10 +41,13 @@ import java.io.IOException; import java.lang.reflect.Field; import java.nio.ByteBuffer; +import java.util.function.Consumer; /** Checks native neighbour culling and the actual fluid-bar drawing function. */ final class TankVisualProbe { private static final int GL_COLOR_BUFFER_BIT = 16384, GL_RGBA = 6408, GL_UNSIGNED_BYTE = 5121; + private static final int WIDTH = 176, HEIGHT = 240; + private static final int PANEL_TOP = 12, GAUGE_X = 80, GAUGE_Y = PANEL_TOP + 9, GAUGE_HEIGHT = 60, TILE = 16; private TankVisualProbe() {} static void verify(Minecraft minecraft, JsonObject result) { @@ -49,35 +56,76 @@ static void verify(Minecraft minecraft, JsonObject result) { result.addProperty("metal_tank_floor_visible", metalFloor); result.addProperty("portable_tank_floor_visible", portableFloor); - MetalTankTileEntity serverTank = new MetalTankTileEntity(BlockPos.ZERO, - zone.moddev.mc.basepower.init.Blocks.metal_storage_tank.defaultBlockState()); - IFluidHandler handler = serverTank.getCapability(ForgeCapabilities.FLUID_HANDLER, Direction.UP) - .orElseThrow(() -> new IllegalStateException("Metal tank has no native Forge fluid capability")); - require(handler.fill(new FluidStack(net.minecraft.world.level.material.Fluids.WATER, 10000), FluidAction.SIMULATE) == 10000 - && serverTank.getTank().getFluidAmount() == 0, "Native simulated fill changed the tank"); - require(handler.fill(new FluidStack(net.minecraft.world.level.material.Fluids.WATER, 10000), FluidAction.EXECUTE) == 10000, - "Native capability did not fill the tank"); - MetalTankTileEntity clientTank = new MetalTankTileEntity(BlockPos.ZERO, - zone.moddev.mc.basepower.init.Blocks.metal_storage_tank.defaultBlockState()); + require(metalFloor && portableFloor, "Inset tank culled the floor beneath its exposed corners"); + JsonArray cases = new JsonArray(); + result.add("tank_gauge_pixels", cases); + checkTank(minecraft, Blocks.storage_tank, "portable", "fluid_storage_tank_gui.png", cases); + checkTank(minecraft, Blocks.metal_storage_tank, "metal", "fluid_metal_tank_gui.png", cases); + require(cases.asList().stream().allMatch(row -> row.getAsJsonObject().get("different_pixels").getAsInt() == 0), + "Tank gauge pixels differ from the correctly tinted fluid sprite; see tank-gauge-*.png and tank_gauge_pixels"); + result.addProperty("tank_gauge_pixel_status", "PASS"); + } + + private record Sample(String name, Fluid fluid, String sprite, int tint) {} + + private static void checkTank(Minecraft minecraft, GUIBlock block, String name, String art, JsonArray cases) { + var state = block.defaultBlockState(); + var factory = (net.minecraft.world.level.block.EntityBlock) block; + var server = (TileEntitySimpleFluidMachine) factory.newBlockEntity(BlockPos.ZERO, state); + var client = (TileEntitySimpleFluidMachine) factory.newBlockEntity(BlockPos.ZERO, state); + IFluidHandler handler = server.getCapability(ForgeCapabilities.FLUID_HANDLER, Direction.UP) + .orElseThrow(() -> new IllegalStateException(name + " tank has no native Forge fluid capability")); + int capacity = server.getTank().getCapacity(); RegistryAccess registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); PacketLevel packetLevel = PacketLevel.withRegistries(registries); - syncTank(serverTank, clientTank, packetLevel, registries); - result.addProperty("synced_tank_amount", clientTank.getTank().getFluidAmount()); - require(clientTank.getTank().getFluidAmount() == 10000, "Filled tank packet lost water"); - int rgb = renderWater(minecraft, clientTank); - require(handler.drain(10000, FluidAction.EXECUTE).getAmount() == 10000, "Native capability did not drain the tank"); - syncTank(serverTank, clientTank, packetLevel, registries); - require(clientTank.getTank().getFluidAmount() == 0, "Empty tank packet retained old water"); - - result.addProperty("water_bar_rgb", String.format("%06X", rgb)); - int red = (rgb >>> 16) & 255; - int green = (rgb >>> 8) & 255; - int blue = rgb & 255; - require(metalFloor && portableFloor, "Inset tank culled the floor beneath its exposed corners"); - require(blue > red + 20 && blue > green + 10, "Water gauge is not blue: " + String.format("%06X", rgb)); + var definition = registeredDefinition(block); + var inventory = new Inventory(null); + var container = definition.createMenu(1, inventory, client); + require(container.getType() == MachineGUIRegistry.TYPE && MachineGUIRegistry.TYPE != null, + "Tank GUI is not backed by the registered native container type"); + var screen = definition.new GUIContainer(container, inventory, net.minecraft.network.chat.Component.literal(name)) { + void renderTank(GuiGraphics graphics) { renderBg(graphics, 0F, -100, -100); } + }; + screen.init(minecraft, WIDTH, HEIGHT); + ResourceLocation texture = ResourceLocation.fromNamespaceAndPath("basepower", "textures/gui/container/" + art); + BufferedImage empty = render(minecraft, screen::renderTank); + BufferedImage emptyReference = reference(minecraft, texture, null, 0); + compare(name, "empty", 0, 0, empty, emptyReference, emptyReference, cases); + for (Sample sample : new Sample[] { + new Sample("water", net.minecraft.world.level.material.Fluids.WATER, "minecraft:block/water_still", 0x3F76E4), + new Sample("lava", net.minecraft.world.level.material.Fluids.LAVA, "minecraft:block/lava_still", 0xFFFFFF), + new Sample("crude-oil", zone.moddev.mc.basepower.init.Fluids.crude_oil, "basepower:blocks/crude_oil_still", 0xFFFFFF), + new Sample("refined-oil", zone.moddev.mc.basepower.init.Fluids.refined_oil, "basepower:blocks/refined_oil_still", 0xFFFFFF) }) { + for (int quarters : new int[] { 1, 2, 4 }) { + int amount = capacity * quarters / 4; + int rows = GAUGE_HEIGHT * quarters / 4; + FluidStack fill = new FluidStack(sample.fluid(), amount); + require(handler.fill(fill, FluidAction.SIMULATE) == amount && server.getTank().getFluidAmount() == 0, + name + " tank changed during simulated fill"); + require(handler.fill(fill, FluidAction.EXECUTE) == amount, name + " tank did not fill"); + syncTank(server, client, packetLevel, registries); + require(client.getTank().getFluidAmount() == amount && client.getTank().getFluid().getFluid() == sample.fluid(), + name + " tank update lost fluid identity or amount"); + compare(name, sample.name(), amount, rows, render(minecraft, screen::renderTank), + reference(minecraft, texture, sample, rows), emptyReference, cases); + require(handler.drain(amount, FluidAction.EXECUTE).getAmount() == amount, name + " tank did not drain"); + syncTank(server, client, packetLevel, registries); + require(client.getTank().getFluidAmount() == 0, name + " tank update retained old fluid"); + } + } } - private static void syncTank(MetalTankTileEntity server, MetalTankTileEntity client, PacketLevel packetLevel, + private static SimpleMachineGUI registeredDefinition(GUIBlock block) { + try { + var method = MachineGUIRegistry.class.getDeclaredMethod("definition", int.class); + method.setAccessible(true); + return (SimpleMachineGUI) method.invoke(null, block.getGuiID()); + } catch (ReflectiveOperationException failure) { + throw new IllegalStateException("Could not resolve the registered tank GUI", failure); + } + } + + private static void syncTank(TileEntitySimpleFluidMachine server, TileEntitySimpleFluidMachine client, PacketLevel packetLevel, RegistryAccess registries) { RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), registries); // Forge 50 obtains the packet's registry access from the sending block entity's level. @@ -136,24 +184,56 @@ private static boolean floorVisible(final BlockState tank) { return Block.shouldRenderFace(net.minecraft.world.level.block.Blocks.GRASS_BLOCK.defaultBlockState(), world, BlockPos.ZERO, Direction.UP, BlockPos.ZERO.above()); } - private static int renderWater(Minecraft minecraft, MetalTankTileEntity tile) { - FilteredFluidTankGUI definition = new FilteredFluidTankGUI(ResourceLocation.fromNamespaceAndPath( - "basepower", "textures/gui/container/fluid_metal_tank_gui.png")); - final int width = 176, height = 240; - Inventory inventory = new Inventory(null); - SimpleMachineGUI.Container container = definition.createMenu(1, inventory, tile); - require(container.getType() == MachineGUIRegistry.TYPE && MachineGUIRegistry.TYPE != null, - "Tank GUI is not backed by the registered native container type"); - var screen = definition.new GUIContainer(container, inventory, net.minecraft.network.chat.Component.literal("Tank probe")) { - void renderTank(net.minecraft.client.gui.GuiGraphics graphics) { - renderBg(graphics, 0F, -100, -100); + private static BufferedImage reference(Minecraft minecraft, ResourceLocation texture, Sample sample, int rows) { + return render(minecraft, graphics -> { + graphics.blit(texture, 0, PANEL_TOP, 0, 0, WIDTH, 128); + if (sample != null) { + var sprite = minecraft.getTextureAtlas(TextureAtlas.LOCATION_BLOCKS).apply(ResourceLocation.parse(sample.sprite())); + require(sprite.contents().name().equals(ResourceLocation.parse(sample.sprite())), "Missing reference sprite: " + sample.sprite()); + int tint = sample.tint(); + // Use Minecraft's full-sprite blit, not the production UV or clipped-tile calculations. + for (int y = GAUGE_Y + GAUGE_HEIGHT - rows; y < GAUGE_Y + GAUGE_HEIGHT; y += TILE) { + graphics.blit(GAUGE_X, y, 0, TILE, TILE, sprite, + (tint >>> 16 & 255) / 255F, (tint >>> 8 & 255) / 255F, (tint & 255) / 255F, 1F); + } } - }; - screen.init(minecraft, width, height); - require(screen.getMinecraft() == minecraft, "Native tank screen was not initialized"); - RenderTarget framebuffer = new com.mojang.blaze3d.pipeline.TextureTarget(width, height, true, Minecraft.ON_OSX); + RenderSystem.enableBlend(); + graphics.blit(texture, GAUGE_X - 3, GAUGE_Y - 3, 176, 0, 22, 66); + }); + } + + private static void compare(String tank, String fluid, int amount, int rows, BufferedImage actual, + BufferedImage expected, BufferedImage empty, JsonArray cases) { + int different = 0, visible = 0; + int bottom = GAUGE_Y + GAUGE_HEIGHT; + for (int y = GAUGE_Y; y < bottom + 3; y++) { + for (int x = GAUGE_X; x < GAUGE_X + TILE; x++) { + int expectedPixel = y < bottom ? expected.getRGB(x, y) : empty.getRGB(x, y); + if (actual.getRGB(x, y) != expectedPixel) different++; + if (expectedPixel != empty.getRGB(x, y)) visible++; + } + } + require(visible >= rows * TILE / 2, "Fluid reference is blank or obscured: " + tank + " " + fluid); + var result = new JsonObject(); + result.addProperty("tank", tank); + result.addProperty("fluid", fluid); + result.addProperty("amount_mb", amount); + result.addProperty("fill_rows", rows); + result.addProperty("different_pixels", different); + cases.add(result); + try { + String file = "tank-gauge-" + tank + "-" + fluid + "-" + amount; + ImageIO.write(actual, "png", new File(file + ".png")); + if (different > 0) ImageIO.write(expected, "png", new File(file + "-expected.png")); + } catch (IOException failure) { + throw new IllegalStateException("Could not retain the native tank gauge image", failure); + } + } + + private static BufferedImage render(Minecraft minecraft, Consumer draw) { + RenderTarget framebuffer = new com.mojang.blaze3d.pipeline.TextureTarget(WIDTH, HEIGHT, true, Minecraft.ON_OSX); framebuffer.bindWrite(true); - NativeGuiProjection projection = new NativeGuiProjection(width, height, 1000, 3000, -2000); + NativeGuiProjection projection = new NativeGuiProjection(WIDTH, HEIGHT, 1000, 3000, -2000); RenderSystem.disableDepthTest(); RenderSystem.enableBlend(); RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); @@ -161,23 +241,20 @@ void renderTank(net.minecraft.client.gui.GuiGraphics graphics) { try { RenderSystem.clearColor(0F, 0F, 0F, 1F); RenderSystem.clear(GL_COLOR_BUFFER_BIT, Minecraft.ON_OSX); - var graphics = new net.minecraft.client.gui.GuiGraphics(minecraft, minecraft.renderBuffers().bufferSource()); - screen.renderTank(graphics); + var graphics = new GuiGraphics(minecraft, minecraft.renderBuffers().bufferSource()); + draw.accept(graphics); graphics.flush(); - ByteBuffer pixels = ByteBuffer.allocateDirect(width * height * 4); - RenderSystem.readPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels); - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int index = ((height - y - 1) * width + x) * 4; + ByteBuffer pixels = ByteBuffer.allocateDirect(WIDTH * HEIGHT * 4); + RenderSystem.readPixels(0, 0, WIDTH, HEIGHT, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); + for (int y = 0; y < HEIGHT; y++) { + for (int x = 0; x < WIDTH; x++) { + int index = ((HEIGHT - y - 1) * WIDTH + x) * 4; image.setRGB(x, y, (pixels.get(index) & 255) << 16 | (pixels.get(index + 1) & 255) << 8 | pixels.get(index + 2) & 255); } } - ImageIO.write(image, "png", new File("tank-water-gauge.png")); - return image.getRGB(88, 51) & 0xFFFFFF; - } catch (IOException failure) { - throw new IllegalStateException("Could not retain the native tank gauge image", failure); + return image; } finally { projection.close(); framebuffer.destroyBuffers(); diff --git a/test-rig/client-resources/run.ps1 b/test-rig/client-resources/run.ps1 index 6c7080be..011d1ca4 100644 --- a/test-rig/client-resources/run.ps1 +++ b/test-rig/client-resources/run.ps1 @@ -215,6 +215,9 @@ if (-not (Test-Path -LiteralPath $resultPath)) { throw "Client did not produce a $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.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.' +} if ($MachineGuiLayouts -and $result.machine_gui_layout_status -ne 'PASS') { throw 'Machine GUI layouts did not pass; inspect the retained images and JSON.' } $expectedGuiTextChecks = if ($RequireExtensions) { 351 } else { 126 } if ($MachineGuiLayouts -and ($result.machine_gui_text_spec -ne 2 -or $result.machine_gui_text_status -ne 'PASS' -or @@ -234,6 +237,7 @@ if ($log -match "Unable to load model:.*${namespaces}:|Exception loading blockst throw "Client logged a model or texture error; inspect $runtime/logs/latest.log" } Write-Output "PASS: $($result.items) items, $($result.block_states) block states, $($result.creative_entries) creative entries. Evidence: $runtime" +Write-Output "Fluid gauges: all 26 rendered tank cases matched the reference pixels." if ($MachineGuiLayouts) { Write-Output "Required-caption GUI contract v2: $($result.machine_gui_text_checks) text cases passed; $($result.machine_gui_negative_controls_rejected) deliberately broken controls rejected. Images retained." } if ($ChildRendererGeometry) { Write-Output 'Additional renderer geometry: worldless scope passed; hydroturbine orientation/spin still requires a disposable-world test. This is not pixel/manual acceptance.' } $inputReport.qualification = 'NATIVE_CLIENT_RESOURCE_PASS' diff --git a/test-rig/gradle.properties b/test-rig/gradle.properties index 1215860f..3be35f94 100644 --- a/test-rig/gradle.properties +++ b/test-rig/gradle.properties @@ -7,4 +7,4 @@ mapping_channel=official mapping_version=1.20.6 mcp_version=20240429.135109 harness_version=1.1.4.120061 -works_power_sha256=29C1BD5EF384F8BB2441029061615E52A4B7302B9FC5480F2CE9C84C357A3181 +works_power_sha256=E53BDD64776741B2EBAE36A646049DC98487E1BAD1CF69F1E3FCD95188F907F1 From 361b0859b4da6a2595b1f6f6d09de6811b36e0cb Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Tue, 10 Nov 2026 14:14:02 +0000 Subject: [PATCH 7/9] Use the corrected fluid gauge build in the combined launch --- test-rig/profiles/native1206/dev-core.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-rig/profiles/native1206/dev-core.json b/test-rig/profiles/native1206/dev-core.json index ddc8ca4a..21afac59 100644 --- a/test-rig/profiles/native1206/dev-core.json +++ b/test-rig/profiles/native1206/dev-core.json @@ -3,7 +3,7 @@ "mode": "development", "minecraftVersion": "1.20.6", "forgeVersion": "50.2.0", - "powerDeobfSha256": "29C1BD5EF384F8BB2441029061615E52A4B7302B9FC5480F2CE9C84C357A3181", + "powerDeobfSha256": "E53BDD64776741B2EBAE36A646049DC98487E1BAD1CF69F1E3FCD95188F907F1", "mods": [ { "id": "basesteam", @@ -13,7 +13,7 @@ { "id": "baseelectric", "version": "2.2.14.120061", - "sha256": "E9002244F220D7DDBCDBD7B512AB0F34710D8B49276DB4CD72BA131536FE0330" + "sha256": "079401D155CF548FA7CB095E155B52D3F10A5CAABAF9ED08F86829D87242AB0E" } ] } From 02c84cbb1f6411ab1dab41d0d1d35f79c4ff933e Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Tue, 10 Nov 2026 14:30:54 +0000 Subject: [PATCH 8/9] Prepare basePower for the Minecraft 1.20.6 release --- .github/verify-base-metals-pin.sh | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- CHANGELOG.txt | 2 +- README.md | 12 ++++++------ gradle.properties | 1 + 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/verify-base-metals-pin.sh b/.github/verify-base-metals-pin.sh index fac90713..255270db 100644 --- a/.github/verify-base-metals-pin.sh +++ b/.github/verify-base-metals-pin.sh @@ -7,7 +7,7 @@ if [[ ! "$commit" =~ ^[0-9a-f]{40}$ ]]; then exit 1 fi -branch=master-1.20.1 +branch=master-1.20.6 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 0c70834a..41536d5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Check out PowerAdvantage uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Require the Base Metals pin on upstream master-1.20.1 + - name: Require the Base Metals pin on upstream master-1.20.6 id: base-metals-pin env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index bdaee4be..8bf5c61c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Check out PowerAdvantage uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Require the Base Metals pin on upstream master-1.20.1 + - name: Require the Base Metals pin on upstream master-1.20.6 id: base-metals-pin env: GH_TOKEN: ${{ github.token }} diff --git a/CHANGELOG.txt b/CHANGELOG.txt index fbd1dbec..4c214e6e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,4 @@ -basePower 2.4.13.120061 (Minecraft 1.20.6 local candidate) +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. - Read old machine inventories into Minecraft's new item components. Portable fluid tanks still keep their contents when picked up. diff --git a/README.md b/README.md index 5f5bd4c7..a193c8bf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # basePower -**Minecraft 1.20.6, version 2.4.13.120061 (local candidate).** +**Minecraft 1.20.6 (Forge), version 2.4.13.120061.** 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. @@ -8,14 +8,14 @@ Results from earlier versions do not qualify this build. basePower continues DrCyano's Power Advantage. It provides shared power, fluids and item transport for baseSteam and baseElectric. -## Playing On 1.20.1 +## Playing On 1.20.6 -- Install [MMD OreSpawn 4.1.0.120011](https://www.curseforge.com/minecraft/mc-mods/mmd-orespawn/files/9175609) on both client and server. +- Install [MMD OreSpawn 4.1.0.120061](https://www.curseforge.com/minecraft/mc-mods/mmd-orespawn/files/9183384) 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.1 build - `3.1.2.120011`. Older Base Metals builds do not have the shared upgrade hook. + 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. - baseSteam and baseElectric are separate mods. Use only their matching - 1.20.1 builds. Additional Loot Tables is not needed on 1.20.1. + 1.20.6 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 diff --git a/gradle.properties b/gradle.properties index 85cd8561..6f3528b2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,6 +27,7 @@ mod_id=basepower mod_name=basePower mod_license=LGPL-3.0-only mod_version=2.4.13.120061 +release_player_sha256=E53BDD64776741B2EBAE36A646049DC98487E1BAD1CF69F1E3FCD95188F907F1 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. From 68f36e826b5d7673874d5e313e40e8c10e1e660b Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Tue, 10 Nov 2026 15:41:16 +0000 Subject: [PATCH 9/9] Stage OreSpawn from the Base Metals checkout --- .github/workflows/ci.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .../mc/basepower/resources/BuildWorkflowContractTest.java | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41536d5e..760c6786 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: basemetals="$GITHUB_WORKSPACE/.ci-dependencies/BaseMetals" mavenizer="$orespawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar" mirror="$RUNNER_TEMP/orespawn-mirror" - bash "$basemetals/gradle/stage-orespawn-release.sh" "$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) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8bf5c61c..c0b91bf7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -73,7 +73,7 @@ jobs: basemetals="$GITHUB_WORKSPACE/.ci-dependencies/BaseMetals" mavenizer="$orespawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar" mirror="$RUNNER_TEMP/orespawn-mirror" - bash "$basemetals/gradle/stage-orespawn-release.sh" "$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) diff --git a/src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java index 57da6e07..11307d16 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/BuildWorkflowContractTest.java @@ -42,6 +42,7 @@ public class BuildWorkflowContractTest { assertTrue(name, workflow.contains("build/libs/BaseMetals-" + pins.getProperty("basemetals_version") + ".jar")); assertTrue(name, workflow.contains("build/libs/OreSpawn-" + pins.getProperty("orespawn_version") + ".jar")); assertTrue(name, workflow.contains("ref: " + pins.getProperty("orespawn_source_commit"))); + assertTrue(name, workflow.contains("(cd \"$basemetals\" && bash gradle/stage-orespawn-release.sh \"$mirror\")")); assertTrue(name, workflow.contains("master-" + pins.getProperty("minecraft_version"))); assertTrue(name, workflow.contains("java-version: '25.0.3+9.0.LTS'")); assertTrue(name, workflow.contains("java-version: '" + pins.getProperty("java_setup_version") + ".0.LTS'"));