From e7f1945b5e7de47bf584b1ade878c3e532c8d95b Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Thu, 12 Nov 2026 16:01:07 +0000 Subject: [PATCH 1/3] Port the core and native test fixtures to Forge 65 --- .github/test-base-metals-pin.sh | 4 +- .github/verify-base-metals-pin.sh | 2 +- .github/workflows/ci.yml | 22 +++++----- .github/workflows/codeql-analysis.yml | 14 +++---- .github/workflows/validate-gradle-build.yml | 4 +- CHANGELOG.txt | 5 +++ README.md | 10 ++--- build.gradle | 39 +++++++++--------- docs/26.2/PORT-STATUS.md | 18 +++++++++ gradle.properties | 40 +++++++++---------- gradle/forge64-mavenizer.gradle | 12 +++--- .../moddev/mc/basepower/PowerAdvantage.java | 2 +- .../migration/VanillaRegistryRemaps.java | 2 +- src/main/resources/META-INF/mods.toml | 4 +- src/main/resources/pack.mcmeta | 4 +- .../simple/SimpleMachineGuiNativeTest.java | 24 +++++------ .../DataDrivenRegistryMigrationTest.java | 2 +- .../LegacyEntityAttributeAliasesTest.java | 4 +- .../LegacyForgeAttributeRegistryTest.java | 6 +-- .../migration/LegacyMachineItemsTest.java | 2 +- .../LegacyPlayerTransformerTest.java | 2 +- .../Native12111BiomeRegistryMappingTest.java | 2 +- .../migration/NativeWolfTestWorld.java | 3 +- .../registry/MachineGuiTitleTest.java | 2 +- .../resources/ClientResourceContractTest.java | 10 ++--- ...Test.java => Native262PackFormatTest.java} | 26 ++++++------ ...> Native262VillagerTradeResourceTest.java} | 6 +-- .../NativeBlockEntityFactoryTest.java | 2 +- .../resources/NativeGuiExtractionFixture.java | 9 +++-- .../NativePackFormatContractTest.java | 10 ++--- .../NativePackRangeContractTest.java | 10 ++--- .../resources/RecipeUnlockContractTest.java | 2 +- .../NativePackProcessContractTest.java | 4 +- .../testing/NativePackResourceProof.java | 10 ++--- .../mc/basepower/util/InventoryLocksTest.java | 2 +- .../NativeLeashKnotPlatformControl.java | 6 +-- .../NativePlatformSupport.java | 3 ++ .../NativeWolfPlatformControl.java | 2 +- test-rig/profiles/native262/dev-core.json | 20 ++++++++++ .../unit/NativeEmptyEntityTracker.java | 37 +++++++++++++++++ 40 files changed, 238 insertions(+), 150 deletions(-) create mode 100644 docs/26.2/PORT-STATUS.md rename src/test/java/zone/moddev/mc/basepower/resources/{Native2612PackFormatTest.java => Native262PackFormatTest.java} (75%) rename src/test/java/zone/moddev/mc/basepower/resources/{Native2612VillagerTradeResourceTest.java => Native262VillagerTradeResourceTest.java} (97%) create mode 100644 test-rig/profiles/native262/dev-core.json create mode 100644 test-rig/unit-support/zone/moddev/mc/advantageworks/unit/NativeEmptyEntityTracker.java diff --git a/.github/test-base-metals-pin.sh b/.github/test-base-metals-pin.sh index 240854aa..43a6565e 100644 --- a/.github/test-base-metals-pin.sh +++ b/.github/test-base-metals-pin.sh @@ -24,7 +24,7 @@ chmod +x "$scratch/bin/gh" export PATH="$scratch/bin:$PATH" commit=1111111111111111111111111111111111111111 cd "$scratch/project" -for minecraft in 1.20.6 1.21.1 1.21.11 26.1.2; do +for minecraft in 1.20.6 1.21.1 1.21.11 26.1.2 26.2; do printf 'minecraft_version=%s\nbasemetals_source_commit=%s\n' "$minecraft" "$commit" > gradle.properties export EXPECTED_COMPARE="repos/MinecraftModDevelopmentMods/BaseMetals/compare/$commit...master-$minecraft" export GITHUB_OUTPUT="$scratch/output" @@ -44,7 +44,7 @@ for minecraft in 1.20.6 1.21.1 1.21.11 26.1.2; do test ! -s "$GITHUB_OUTPUT" done done -for minecraft in invalid 26.1 26.1.2/evil master-26.1.2 0.1.2; do +for minecraft in invalid 1.21 26.2/evil master-26.2 0.1.2; do printf 'minecraft_version=%s\nbasemetals_source_commit=%s\n' "$minecraft" "$commit" > gradle.properties if bash "$verify" > "$scratch/invalid.log" 2>&1; then echo "Incorrectly accepted an invalid target: $minecraft" >&2 diff --git a/.github/verify-base-metals-pin.sh b/.github/verify-base-metals-pin.sh index f8bc2bb2..93848636 100644 --- a/.github/verify-base-metals-pin.sh +++ b/.github/verify-base-metals-pin.sh @@ -8,7 +8,7 @@ if [[ ! "$commit" =~ ^[0-9a-f]{40}$ ]]; then fi minecraft="$(sed -n 's/^minecraft_version=//p' gradle.properties)" -if [[ ! "$minecraft" =~ ^(1|[2-9][0-9])\.[0-9]+\.[0-9]+$ ]]; then +if [[ ! "$minecraft" =~ ^(1\.[0-9]+\.[0-9]+|[2-9][0-9]\.[0-9]+(\.[0-9]+)?)$ ]]; then echo "::error::Expected a complete Minecraft target version in gradle.properties" exit 1 fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f6a6a1b..c22ba426 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,16 @@ -name: basePower 26.1.2 CI +name: basePower 26.2 CI on: push: - branches: [master-26.1.2, 'feature/**'] + branches: [master-26.2, 'feature/**'] pull_request: - branches: [master-26.1.2] + branches: [master-26.2] permissions: contents: read concurrency: - group: basepower-26.1.2-${{ github.workflow }}-${{ github.ref }} + group: basepower-26.2-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Test target-derived dependency guards run: bash .github/test-base-metals-pin.sh - - name: Require the Base Metals pin on upstream master-26.1.2 + - name: Require the Base Metals pin on upstream master-26.2 id: base-metals-pin env: GH_TOKEN: ${{ github.token }} @@ -34,13 +34,13 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/OreSpawn - ref: 101fd12b598934c353e417c57edc5c2bbefd5f53 + ref: c64d189c899ca5f277078c4c1efe1b1fd1af1742 path: .ci-dependencies/OreSpawn - name: Check out pinned Base Metals uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/BaseMetals - ref: 331fa46914f854db71a8c33500cc559a8878b7d3 + ref: 523335a59ecff399e40a7c66eb121a3aabdc45ba path: .ci-dependencies/BaseMetals - name: Install Java 25 for Gradle, tools and Minecraft uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 @@ -68,8 +68,8 @@ jobs: (cd "$basemetals" && bash gradle/stage-orespawn-release.sh "$mirror") (cd "$orespawn" && bash gradlew $MMD_GRADLE_TOOLCHAINS jar --no-daemon --stacktrace) (cd "$basemetals" && bash gradlew $MMD_GRADLE_TOOLCHAINS jar -PorespawnVerificationRepository="$mirror" --no-daemon --stacktrace) - osjar="$orespawn/build/libs/OreSpawn-4.1.1.2601021.jar" - bmjar="$basemetals/build/libs/BaseMetals-3.1.2.2601021.jar" + osjar="$orespawn/build/libs/OreSpawn-4.1.0.2602001.jar" + bmjar="$basemetals/build/libs/BaseMetals-3.1.2.2602001.jar" test -f "$osjar" && test -f "$bmjar" echo "orespawn=$osjar" >> "$GITHUB_OUTPUT" echo "basemetals=$bmjar" >> "$GITHUB_OUTPUT" @@ -85,7 +85,7 @@ jobs: - name: Upload audited release candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: basePower-26.1.2-${{ github.sha }} + name: basePower-26.2-${{ github.sha }} if-no-files-found: error retention-days: 30 path: | @@ -96,7 +96,7 @@ jobs: if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: basePower-26.1.2-diagnostics-${{ github.sha }} + name: basePower-26.2-diagnostics-${{ github.sha }} if-no-files-found: ignore retention-days: 14 path: | diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 218a77b0..8d8a1669 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,9 +2,9 @@ name: CodeQL on: push: - branches: [master-26.1.2, 'feature/**'] + branches: [master-26.2, 'feature/**'] pull_request: - branches: [master-26.1.2] + branches: [master-26.2] schedule: - cron: '47 7 * * 4' @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Test target-derived dependency guards run: bash .github/test-base-metals-pin.sh - - name: Require the Base Metals pin on upstream master-26.1.2 + - name: Require the Base Metals pin on upstream master-26.2 id: base-metals-pin env: GH_TOKEN: ${{ github.token }} @@ -34,13 +34,13 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/OreSpawn - ref: 101fd12b598934c353e417c57edc5c2bbefd5f53 + ref: c64d189c899ca5f277078c4c1efe1b1fd1af1742 path: .ci-dependencies/OreSpawn - name: Check out pinned Base Metals uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/BaseMetals - ref: 331fa46914f854db71a8c33500cc559a8878b7d3 + ref: 523335a59ecff399e40a7c66eb121a3aabdc45ba path: .ci-dependencies/BaseMetals - name: Install Java 25 for Gradle, tools and Minecraft uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 @@ -69,8 +69,8 @@ jobs: chmod +x "$orespawn/gradlew" "$basemetals/gradlew" (cd "$orespawn" && ./gradlew $MMD_GRADLE_TOOLCHAINS jar --no-daemon --stacktrace) (cd "$basemetals" && ./gradlew $MMD_GRADLE_TOOLCHAINS jar -PorespawnVerificationRepository="$mirror" --no-daemon --stacktrace) - echo "orespawn=$orespawn/build/libs/OreSpawn-4.1.1.2601021.jar" >> "$GITHUB_OUTPUT" - echo "basemetals=$basemetals/build/libs/BaseMetals-3.1.2.2601021.jar" >> "$GITHUB_OUTPUT" + echo "orespawn=$orespawn/build/libs/OreSpawn-4.1.0.2602001.jar" >> "$GITHUB_OUTPUT" + echo "basemetals=$basemetals/build/libs/BaseMetals-3.1.2.2602001.jar" >> "$GITHUB_OUTPUT" echo "mavenizer=$mavenizer" >> "$GITHUB_OUTPUT" - name: Initialize CodeQL uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 diff --git a/.github/workflows/validate-gradle-build.yml b/.github/workflows/validate-gradle-build.yml index 531f53fe..d8bacce9 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-26.1.2 + - master-26.2 - 'feature/**' pull_request: branches: - - master-26.1.2 + - master-26.2 permissions: contents: read diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 300f634a..5d1e12f4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +basePower 2.4.17.2602001 (Minecraft 26.2, in development) + +- Begin the Forge 65 port from the accepted 26.1.2 source, keeping existing machine identities and saved data. +- Update the native data and client resource format checks for Minecraft 26.2. + basePower 2.4.16.2601021 (Minecraft 26.1.2) - Require OreSpawn 4.1.1 so old generation rules keep their saved settings without enabling duplicate modern ore rules. diff --git a/README.md b/README.md index 8b74b4ec..45678430 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # basePower -**Minecraft 26.1.2 (Forge), version 2.4.16.2601021.** +**Minecraft 26.2 (Forge), version 2.4.17.2602001, in development.** -This branch has passed its -[26.1.2 automated qualification](docs/26.1.2/QUALIFICATION.md) and manual -playtesting. Publication is being prepared separately. The -[port record](docs/26.1.2/PORT-STATUS.md) keeps the earlier findings separate. +This branch is a fresh port of the accepted 26.1.2 source. It has not yet passed +the 26.2 qualification suite. See the [26.2 port record](docs/26.2/PORT-STATUS.md). +The [26.1.2 qualification](docs/26.1.2/QUALIFICATION.md) and +[port record](docs/26.1.2/PORT-STATUS.md) keep the earlier findings separate. The [1.21.11 qualification](docs/1.21.11/QUALIFICATION.md) remains historical evidence, not proof of this port. diff --git a/build.gradle b/build.gradle index aadc5d4e..41d66d5e 100644 --- a/build.gradle +++ b/build.gradle @@ -134,7 +134,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 26.1.2 launches.' + description = 'Merges classes and processed resources for Forge 26.2 launches.' dependsOn tasks.named('classes') from sourceSets.main.output into forgeRunModClassesDirectory @@ -170,11 +170,11 @@ repositories { // build to a drive letter. CI and other layouts can override either path. def baseMetalsDeobfJar = file(providers.gradleProperty('baseMetalsJar') .orElse(providers.gradleProperty('baseMetalsDeobfJar')) - .getOrElse('../../../BaseMetals/BaseMetals 2612/BaseMetals/build/libs/' + .getOrElse('../../../BaseMetals/BaseMetals 262/BaseMetals/build/libs/' + "BaseMetals-${project.basemetals_version}.jar")) def oreSpawnDeobfJar = file(providers.gradleProperty('oreSpawnJar') .orElse(providers.gradleProperty('oreSpawnDeobfJar')) - .getOrElse('../../../MinecraftMineralogy/OS2612/OreSpawn/build/libs/' + .getOrElse('../../../MinecraftMineralogy/OS262/OreSpawn/build/libs/' + "OreSpawn-${project.orespawn_version}.jar")) dependencies { @@ -203,6 +203,7 @@ def unitBootstrapJar = tasks.register('unitBootstrapJar', Jar) { manifest.attributes('Premain-Class': 'zone.moddev.mc.advantageworks.unit.ForgeCapabilityBootstrap') } tasks.withType(Test).configureEach { + maxHeapSize = '2G' dependsOn unitBootstrapJar doFirst { jvmArgs "-javaagent:${unitBootstrapJar.get().archiveFile.get().asFile}" @@ -259,7 +260,7 @@ tasks.named('jar', Jar) { } } -// Forge 64 uses the official names at runtime, so the same jar serves both uses. +// Forge 65 uses the official names at runtime, so the same jar serves both uses. def releaseJar = tasks.named('jar', Jar) tasks.register('deobfJar') { @@ -312,18 +313,18 @@ tasks.register('verifyReleaseConfiguration') { description = 'Validates release identity, target metadata, and required dependencies.' doLast { def pack = new JsonSlurper().parse(file('src/main/resources/pack.mcmeta')).pack - if (pack.min_format != [101, 1] || pack.max_format != [101, 1] + if (pack.min_format != [107, 1] || pack.max_format != [107, 1] || !file('src/main/resources/META-INF/mods.toml').text.contains('showAsResourcePack=false')) { - throw new GradleException('Forge 64 requires data format 101.1 and its native hidden-client resource pack') + throw new GradleException('Forge 65 requires data format 107.1 and its native hidden-client resource pack') } - if (project.mod_version != '2.4.16.2601021') { + if (project.mod_version != '2.4.17.2602001') { throw new GradleException("Unexpected PowerAdvantage release version: ${project.mod_version}") } - if (project.minecraft_version != '26.1.2' - || project.forge_version != '64.1.5' + if (project.minecraft_version != '26.2' + || project.forge_version != '65.1.0' || project.mapping_channel != 'official' - || project.mapping_version != '26.1.2' - || project.mcp_version != '20260409.101008') { + || project.mapping_version != '26.2' + || project.mcp_version != '20260616.103818') { throw new GradleException('Unexpected Minecraft, Forge, or mappings target') } if (project.loader_name != 'forge' || project.loader_code != '1' @@ -340,19 +341,19 @@ tasks.register('verifyReleaseConfiguration') { } if (project.cf_requirements != 'base-metals,mmd-orespawn' || project.cf_optionals != 'additional-loot-tables' - || project.cf_versions != '26.1.2') { + || project.cf_versions != '26.2') { throw new GradleException('Unexpected CurseForge dependency or game-version metadata') } - if (project.basemetals_version != '3.1.2.2601021' - || project.orespawn_version != '4.1.1.2601021' + if (project.basemetals_version != '3.1.2.2602001' + || project.orespawn_version != '4.1.0.2602001' || !(project.basemetals_source_commit ==~ /[a-f0-9]{40}/) - || project.orespawn_source_commit != '101fd12b598934c353e417c57edc5c2bbefd5f53') { + || project.orespawn_source_commit != 'c64d189c899ca5f277078c4c1efe1b1fd1af1742') { 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.16.2601021"') + || !mainSource.contains('VERSION = "2.4.17.2602001"') || !mainSource.contains('@Mod(PowerAdvantage.MODID)')) { throw new GradleException('PowerAdvantage source identity changed') } @@ -937,7 +938,7 @@ tasks.register('isolateEclipseProductionRuns') { } String program = programAttribute.@value if (program.contains('--to-srg') || program.contains('--to-obf')) { - throw new GradleException("${name} must not remap the official Forge 64 runtime") + throw new GradleException("${name} must not remap the official Forge 65 runtime") } if (name == 'runClient.launch') { File nativeJar = productionRuntime.find { @@ -1059,7 +1060,7 @@ tasks.register('verifyEclipseProductionClasspath') { if (main != 'net.minecraftforge.launcher.Main' || !program.startsWith('--main net.minecraftforge.bootstrap.ForgeBootstrap ') || launchTargets.count != 1 || launchTargets[0][1] != target) { - throw new GradleException("${name} does not start the Forge 64 bootstrap") + throw new GradleException("${name} does not start the Forge 65 bootstrap") } if (program.contains('--to-srg') || program.contains('--to-obf') || program.contains('{asset_index}') || program.contains('{assets_root}') @@ -1333,7 +1334,7 @@ tasks.register('prepareCombinedEclipseLaunch') { dependsOn tasks.named('deobfJar') doLast { File inputProfile = file(providers.gradleProperty('advantageDevelopmentProfile').getOrElse( - 'test-rig/profiles/native2612/dev-core.json')) + 'test-rig/profiles/native262/dev-core.json')) def profile = new JsonSlurper().parse(inputProfile) if (profile.mode != 'development' || profile.minecraftVersion != project.minecraft_version || profile.forgeVersion != project.forge_version) { diff --git a/docs/26.2/PORT-STATUS.md b/docs/26.2/PORT-STATUS.md new file mode 100644 index 00000000..42d61ad3 --- /dev/null +++ b/docs/26.2/PORT-STATUS.md @@ -0,0 +1,18 @@ +# Minecraft 26.2 Port + +This is a local port of the accepted Minecraft 26.1.2 source, not a tested or +published release. Earlier qualification records apply only to their named +Minecraft version and exact jars. + +The starting target is Forge 65.1.0, official 26.2 names, MCP +20260616.103818 and Temurin 25.0.3+9. Minecraft's own version metadata declares +data version 4903, client resource format 88.0 and server data format 107.1. + +Base Metals needs its shared migration extension ported before the old-world +suite can run. OreSpawn's retained-provider-settings repair is present locally +but is not yet a public 26.2 release. Dependency and family checksums will be +sealed only after the actual candidates have been built and verified. + +Remaining work includes production compilation, the full unit/resource/Eclipse +audits, packaged machine and migration checks, full restarts, client rendering, +and measured endurance tests. Manual testing comes after those checks. diff --git a/gradle.properties b/gradle.properties index 54e4f74b..95fca302 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,15 +5,15 @@ org.gradle.caching=true org.gradle.parallel=false net.minecraftforge.gradle.merge-source-sets=false -minecraft_version=26.1.2 -minecraft_version_range=[26.1.2] -forge_version=64.1.5 -forge_installer_sha256=6E01323837051176762BD466340A601DA122F179FE6A00AC616E05429AE07648 -forge_version_range=[64.1.5,65) -loader_version_range=[64,65) +minecraft_version=26.2 +minecraft_version_range=[26.2] +forge_version=65.1.0 +forge_installer_sha256=813FBBFECC3086A492783B9F5023AE15007737B42E17AAB75F48BA77403314DF +forge_version_range=[65.1.0,66) +loader_version_range=[65,66) mapping_channel=official -mapping_version=26.1.2 -mcp_version=20260409.101008 +mapping_version=26.2 +mcp_version=20260616.103818 loader_name=forge loader_code=1 @@ -26,26 +26,26 @@ curseforge_project_id=1780140 mod_id=basepower mod_name=basePower mod_license=LGPL-3.0-only -mod_version=2.4.16.2601021 +mod_version=2.4.17.2602001 # Frozen build candidate, not a claim of completed gameplay qualification or publication. -release_player_sha256=23ABCC1FEFDAA6988BF709437EB165C8948C757E773ADAB738092EB93CA8D2B0 -basesteam_version=2.2.17.2601021 -baseelectric_version=2.2.17.2601021 +release_player_sha256=06522DB981D3A0ED36D07FD162CE72F63561DD02CFAF9E060EFE7F6A08989633 +basesteam_version=2.2.18.2602001 +baseelectric_version=2.2.18.2602001 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=26.1.2 +cf_versions=26.2 -basemetals_version=3.1.2.2601021 -basemetals_source_commit=331fa46914f854db71a8c33500cc559a8878b7d3 +basemetals_version=3.1.2.2602001 +basemetals_source_commit=523335a59ecff399e40a7c66eb121a3aabdc45ba basemetals_curse_project_id=240967 -basemetals_deobf_sha256=6786238835D4D8A653C769F3B0E61D226959146300E17523799CFDF17540FD19 +basemetals_deobf_sha256=15CBD0C8DD95BBA846969929B8767887BA4B8152DFB10C9C58FB314AD39FB9E1 -orespawn_version=4.1.1.2601021 -orespawn_source_commit=101fd12b598934c353e417c57edc5c2bbefd5f53 +orespawn_version=4.1.0.2602001 +orespawn_source_commit=c64d189c899ca5f277078c4c1efe1b1fd1af1742 orespawn_curse_project_id=245586 -orespawn_public_file_id=9572926 -orespawn_deobf_sha256=810284ECB32D5249381F246498CEFA57E70FFF7CCC40110F799FC276CBC42BF7 +orespawn_public_file_id=9332183 +orespawn_deobf_sha256=F74785025898DF059E0F00F6ADA53364E07ECBBC306807ED7CD55350609E2547 diff --git a/gradle/forge64-mavenizer.gradle b/gradle/forge64-mavenizer.gradle index 375f853c..7b51c91a 100644 --- a/gradle/forge64-mavenizer.gradle +++ b/gradle/forge64-mavenizer.gradle @@ -6,20 +6,20 @@ import org.gradle.jvm.toolchain.JvmVendorSpec def tool = 'ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar' def defaultJar = project.extensions.extraProperties.has('forge64MavenizerDefault') ? project.forge64MavenizerDefault - : "../../../MinecraftMineralogy/OS2612/OreSpawn/${tool}" + : "../../../MinecraftMineralogy/OS262/OreSpawn/${tool}" def fixture = file(providers.gradleProperty('forge64MavenizerJar').getOrElse(defaultJar)) def checksums = [ - (fixture.name): 'BB0DACB3DEB2A711CA67DD402D5FDAB5A9847C37C7B9B596DEC7CF37378FA085', - 'minecraft-source-compatibility.json': '21D1F5984E735BCEA1B1EFB9F965655796B39538FC91AAED1C3474004710423D', - 'minecraft-mavenizer-0.5.21-orespawn-compat.patch': '7C8EC895E79DF31AFD2CA3B505B43573C7052718A24050832189AAA2D3CC6137', + (fixture.name): '5ABAB52A3F1BE115381C3CE80D5FD38DBE6C9F4C0B9ECF70A957700EC90FB88B', + 'minecraft-source-compatibility.json': '9437E03FC5E1CB5D605D72F0538C4DF4417B64DB0A57D0AABDC5184781517AC1', + 'minecraft-mavenizer-0.5.21-orespawn-compat.patch': '2DA8DD0FD5F112F5B7591113AED06D9A58C584A25F8705E26445A6D2E9D5C78E', 'LICENSE-MAVENIZER.txt': '20C17D8B8C48A600800DFD14F95D5CB9FF47066A9641DDEAB48DC54AEC96E331', - 'README.md': '7686880D2B0F6BD6ACAFA4F0AB7585189767813174BC36DB675FEC0142440A77' + 'README.md': 'EAD1477F393897C491A10FB86178213A99C360571A8063D551DA0579DF65BA6F' ] checksums.each { name, expected -> File input = new File(fixture.parentFile, name) if (!input.isFile() || MessageDigest.getInstance('SHA-256').digest(input.bytes) .encodeHex().toString().toUpperCase(Locale.ROOT) != expected) { - throw new GradleException("Missing or altered Forge 64 Mavenizer fixture: ${input}") + throw new GradleException("Missing or altered Forge 65 Mavenizer fixture: ${input}") } } diff --git a/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java b/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java index 8dd1ce38..af2f1df1 100644 --- a/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java +++ b/src/main/java/zone/moddev/mc/basepower/PowerAdvantage.java @@ -43,7 +43,7 @@ public final class PowerAdvantage { public static final String MODID = "basepower"; public static final String NAME = "basePower"; - public static final String VERSION = "2.4.16.2601021"; + public static final String VERSION = "2.4.17.2602001"; public static final Logger LOGGER = LogManager.getLogger(MODID); private static PowerAdvantage instance; 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 d0f1193c..c9a967bb 100644 --- a/src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java +++ b/src/main/java/zone/moddev/mc/basepower/migration/VanillaRegistryRemaps.java @@ -44,7 +44,7 @@ public final class VanillaRegistryRemaps { Biomes.WARM_OCEAN, Biomes.LUKEWARM_OCEAN, Biomes.DEEP_LUKEWARM_OCEAN, Biomes.OCEAN, Biomes.DEEP_OCEAN, Biomes.COLD_OCEAN, Biomes.DEEP_COLD_OCEAN, Biomes.FROZEN_OCEAN, Biomes.DEEP_FROZEN_OCEAN, Biomes.MUSHROOM_FIELDS, Biomes.DRIPSTONE_CAVES, - Biomes.LUSH_CAVES, Biomes.DEEP_DARK, Biomes.NETHER_WASTES, Biomes.WARPED_FOREST, + Biomes.LUSH_CAVES, Biomes.DEEP_DARK, Biomes.SULFUR_CAVES, Biomes.NETHER_WASTES, Biomes.WARPED_FOREST, Biomes.CRIMSON_FOREST, Biomes.SOUL_SAND_VALLEY, Biomes.BASALT_DELTAS, Biomes.THE_END, Biomes.END_HIGHLANDS, Biomes.END_MIDLANDS, Biomes.SMALL_END_ISLANDS, Biomes.END_BARRENS); private static final Set LEGACY_PAINTINGS = Set.of("kebab", "aztec", "alban", "aztec2", "bomb", diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index 1e49bd18..fb152333 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -15,14 +15,14 @@ description='''API and shared machines for pressure, electricity, quantum power, [[dependencies.basepower]] modId="basemetals" mandatory=true -versionRange="[3.1.2.2601021,4.0.0)" +versionRange="[3.1.2.2602001,4.0.0)" ordering="AFTER" side="BOTH" [[dependencies.basepower]] modId="orespawn" mandatory=true -versionRange="[4.1.1.2601021,5.0.0)" +versionRange="[4.1.0.2602001,5.0.0)" ordering="AFTER" side="BOTH" diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index e48b0364..e058bf40 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,7 +1,7 @@ { "pack": { "description": "basePower resources", - "min_format": [101, 1], - "max_format": [101, 1] + "min_format": [107, 1], + "max_format": [107, 1] } } diff --git a/src/test/java/cyano/poweradvantage/api/simple/SimpleMachineGuiNativeTest.java b/src/test/java/cyano/poweradvantage/api/simple/SimpleMachineGuiNativeTest.java index b0c57770..8899645b 100644 --- a/src/test/java/cyano/poweradvantage/api/simple/SimpleMachineGuiNativeTest.java +++ b/src/test/java/cyano/poweradvantage/api/simple/SimpleMachineGuiNativeTest.java @@ -94,14 +94,14 @@ public class SimpleMachineGuiNativeTest { var inventory = fixture.target.texts.get(1); assertEquals("Machine", text(title)); assertEquals("Inventory", text(inventory)); - assertEquals(67, title.x); - assertEquals(1, title.y); - assertEquals(8, inventory.x); - assertEquals(140, inventory.y); + assertEquals(67, get(title, "x")); + assertEquals(1, get(title, "y")); + assertEquals(8, get(inventory, "x")); + assertEquals(140, get(inventory, "y")); for (var caption : fixture.target.texts) { - assertEquals(0xFFFFFFFF, caption.color); - assertTrue(caption.dropShadow); - assertSame(fixture.font, caption.font); + assertEquals(0xFFFFFFFF, get(caption, "color")); + assertTrue((Boolean) get(caption, "dropShadow")); + assertSame(fixture.font, get(caption, "font")); assertEquals(new Matrix3x2f(fixture.pose), new Matrix3x2f(caption.pose)); } fixture.pose.translate(50, 50); @@ -115,8 +115,8 @@ public class SimpleMachineGuiNativeTest { assertEquals(2, fixture.target.texts.size()); assertEquals("M".repeat(29) + "...", text(fixture.target.texts.get(0))); assertEquals("I".repeat(23) + "...", text(fixture.target.texts.get(1))); - assertEquals(-8, fixture.target.texts.get(0).x); - assertEquals(8, fixture.target.texts.get(1).x); + assertEquals(-8, get(fixture.target.texts.get(0), "x")); + assertEquals(8, get(fixture.target.texts.get(1), "x")); } @Test public void nativeScreenWrapperExtractsBackgroundOnceAndReleasesDecorationContext() throws Exception { @@ -190,7 +190,6 @@ private static Fixture screen(String title, String inventory, int width) throws set(screen, "minecraft", extraction.minecraft); var inventoryItems = new Inventory(null, new EntityEquipment()); set(screen, "menu", gui.createMenu(1, inventoryItems, new SimpleContainer(0))); - set(screen, "draggingItem", net.minecraft.world.item.ItemStack.EMPTY); set(screen, "renderables", List.of()); set(screen, "playerInventoryLabel", Component.literal(inventory)); set(screen, "title", Component.literal(title)); @@ -243,9 +242,10 @@ private static final class TextQueue extends GuiRenderState { @Override public void addText(GuiTextRenderState text) { texts.add(text); } } - private static String text(GuiTextRenderState state) { + private static String text(GuiTextRenderState state) throws Exception { var text = new StringBuilder(); - state.text.accept((index, style, point) -> { text.appendCodePoint(point); return true; }); + ((net.minecraft.util.FormattedCharSequence) get(state, "text")) + .accept((index, style, point) -> { text.appendCodePoint(point); return true; }); return text.toString(); } diff --git a/src/test/java/zone/moddev/mc/basepower/migration/DataDrivenRegistryMigrationTest.java b/src/test/java/zone/moddev/mc/basepower/migration/DataDrivenRegistryMigrationTest.java index 94e403a4..5696a248 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/DataDrivenRegistryMigrationTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/DataDrivenRegistryMigrationTest.java @@ -109,7 +109,7 @@ public class DataDrivenRegistryMigrationTest { new com.mojang.serialization.Dynamic<>(net.minecraft.nbt.NbtOps.INSTANCE, root.getListOrEmpty("Entities").getCompoundOrEmpty(0)), 3839, net.minecraft.SharedConstants.getCurrentVersion().dataVersion().version()).getValue(); - var entity = new net.minecraft.world.entity.decoration.painting.Painting(net.minecraft.world.entity.EntityType.PAINTING, world); + var entity = new net.minecraft.world.entity.decoration.painting.Painting(net.minecraft.world.entity.EntityTypes.PAINTING, world); NativeWolfTestWorld.load(entity, converted); var saved = NativeWolfTestWorld.save(entity); assertEquals(Identifier.parse("minecraft:alban"), entity.getVariant().unwrapKey().orElseThrow().identifier()); 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 244eb3a1..818e193f 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntityAttributeAliasesTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyEntityAttributeAliasesTest.java @@ -230,12 +230,12 @@ public class LegacyEntityAttributeAliasesTest { CompoundTag red = (CompoundTag) DataFixers.getDataFixer().update(References.ENTITY, new Dynamic<>(NbtOps.INSTANCE, source.copy()), 1343, SharedConstants.getCurrentVersion().dataVersion().version()).getValue(); - Wolf before = new Wolf(EntityType.WOLF, world); NativeWolfTestWorld.load(before, red); + Wolf before = new Wolf(net.minecraft.world.entity.EntityTypes.WOLF, world); NativeWolfTestWorld.load(before, red); 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); + Wolf after = new Wolf(net.minecraft.world.entity.EntityTypes.WOLF, world); NativeWolfTestWorld.load(after, root.getCompoundOrEmpty("Level").getListOrEmpty("Entities").getCompoundOrEmpty(0)); assertEquals(2D, after.getAttribute(swimSpeed).getBaseValue(), 0D); var modifierId = net.minecraft.resources.Identifier.fromNamespaceAndPath("minecraft", MODIFIER.toString()); 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 cc254966..4b92f814 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyForgeAttributeRegistryTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyForgeAttributeRegistryTest.java @@ -65,13 +65,13 @@ static void registerForgeAttributes() throws ReflectiveOperationException { CompoundTag raw = (CompoundTag) DataFixers.getDataFixer().update(References.ENTITY, new Dynamic<>(NbtOps.INSTANCE, source.copy()), 1343, SharedConstants.getCurrentVersion().dataVersion().version()).getValue(); - Wolf negative = new Wolf(EntityType.WOLF, world); NativeWolfTestWorld.load(negative, raw); + Wolf negative = new Wolf(net.minecraft.world.entity.EntityTypes.WOLF, world); NativeWolfTestWorld.load(negative, raw); AttributeInstance old = negative.getAttribute(ForgeRegistries.ATTRIBUTES.getHolder( Identifier.parse(ALIASES[index][2])).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); NativeWolfTestWorld.load(positive, fixedEntity(root)); + Wolf positive = new Wolf(net.minecraft.world.entity.EntityTypes.WOLF, world); NativeWolfTestWorld.load(positive, fixedEntity(root)); AttributeInstance actual = positive.getAttribute(old.getAttribute()); assertEquals(ALIASES[index][0], values[index], actual.getBaseValue(), 0D); assertEquals(0.125D, actual.getModifier(modifierId()).amount(), 0D); @@ -175,7 +175,7 @@ static CompoundTag nativeAttributeRecord(CompoundTag source) { LegacyEntityDataFixes.prepare(root); LegacyEntityDataFixes.finish(root); CompoundTag converted = fixedEntity(root); - Wolf nativeReader = new Wolf(EntityType.WOLF, NativeWolfTestWorld.create()); + Wolf nativeReader = new Wolf(net.minecraft.world.entity.EntityTypes.WOLF, NativeWolfTestWorld.create()); NativeWolfTestWorld.load(nativeReader, converted); AttributeInstance speed = nativeReader.getAttribute(ForgeMod.SWIM_SPEED.getHolder().orElseThrow()); assertEquals(2D, speed.getBaseValue(), 0D); 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 bfbb89e9..7169a23d 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyMachineItemsTest.java @@ -47,7 +47,7 @@ public class LegacyMachineItemsTest { TileEntityConveyor conveyor = new TileEntityConveyor(net.minecraft.core.BlockPos.ZERO, Blocks.HOPPER.defaultBlockState()); conveyor.loadWithComponents(net.minecraft.world.level.storage.TagValueInput.create( net.minecraft.util.ProblemReporter.DISCARDING, registries(), tile)); - assertSame(Items.RED_WOOL, conveyor.getItem(0).getItem()); + assertSame(Items.WOOL.red(), conveyor.getItem(0).getItem()); assertEquals(7, conveyor.getItem(0).getCount()); } finally { zone.moddev.mc.basepower.init.Entities.CONVEYOR = previous; } } 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 ba45ebed..ead57f85 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/LegacyPlayerTransformerTest.java @@ -147,7 +147,7 @@ private static List targets() throws Exception { assertEquals(List.of("before-components"), calls); assertPlayerContents(migrated); int current = SharedConstants.getCurrentVersion().dataVersion().version(); - assertEquals(4790, current); + assertEquals(4903, current); assertEquals(current, migrated.getIntOr("DataVersion", 0)); assertEquals(original, NbtIo.readCompressed(file, NbtAccounter.unlimitedHeap())); calls.clear(); diff --git a/src/test/java/zone/moddev/mc/basepower/migration/Native12111BiomeRegistryMappingTest.java b/src/test/java/zone/moddev/mc/basepower/migration/Native12111BiomeRegistryMappingTest.java index 6b3ae515..43785dbb 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/Native12111BiomeRegistryMappingTest.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/Native12111BiomeRegistryMappingTest.java @@ -46,7 +46,7 @@ public class Native12111BiomeRegistryMappingTest { @Test public void everyNativeBiomeKeyRetiresOnlyItsObsoleteForgeIndex() throws Exception { Set current = nativeBiomeKeys(); - assertEquals("Review the allowlist when changing the native target", 65, current.size()); + assertEquals("Review the allowlist when changing the native target", 66, current.size()); var before = Set.copyOf(ForgeRegistries.BIOMES.getKeys()); for (Identifier key : current) assertFalse(key.toString(), ForgeRegistries.BIOMES.containsKey(key)); var mappings = mappings(ForgeRegistries.BIOMES, new ArrayList<>(current)); diff --git a/src/test/java/zone/moddev/mc/basepower/migration/NativeWolfTestWorld.java b/src/test/java/zone/moddev/mc/basepower/migration/NativeWolfTestWorld.java index 593c9839..2ae56109 100644 --- a/src/test/java/zone/moddev/mc/basepower/migration/NativeWolfTestWorld.java +++ b/src/test/java/zone/moddev/mc/basepower/migration/NativeWolfTestWorld.java @@ -20,6 +20,7 @@ static ServerLevel create() throws ReflectiveOperationException { unsafe.setAccessible(true); ServerLevel world = (ServerLevel) allocator.getMethod("allocateInstance", Class.class) .invoke(unsafe.get(null), ServerLevel.class); + zone.moddev.mc.advantageworks.unit.NativeEmptyEntityTracker.install(world); set(world, "dimension", Level.NETHER); set(world, "registryAccess", RegistriesHolder.ACCESS); var server = (net.minecraft.server.dedicated.DedicatedServer) allocator.getMethod("allocateInstance", Class.class) @@ -34,7 +35,7 @@ static ServerLevel create() throws ReflectiveOperationException { } private static synchronized void registerNativeAttributes() throws ReflectiveOperationException { - var wolf = net.minecraft.world.entity.EntityType.WOLF; + var wolf = net.minecraft.world.entity.EntityTypes.WOLF; if (!net.minecraftforge.common.ForgeMod.SWIM_SPEED.isPresent() || net.minecraft.world.entity.ai.attributes.DefaultAttributes.getSupplier(wolf) .hasAttribute(net.minecraftforge.common.ForgeMod.SWIM_SPEED.getHolder().orElseThrow())) return; 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 32cfc42b..1d715855 100644 --- a/src/test/java/zone/moddev/mc/basepower/registry/MachineGuiTitleTest.java +++ b/src/test/java/zone/moddev/mc/basepower/registry/MachineGuiTitleTest.java @@ -31,7 +31,7 @@ public class MachineGuiTitleTest { } @Test public void unnamedTilesWithoutInventoryNamesStillHaveATranslatableTitle() { - BlockEntity tile = new BlockEntity(BlockEntityType.CHEST, net.minecraft.core.BlockPos.ZERO, Blocks.CHEST.defaultBlockState()) {}; + BlockEntity tile = new BlockEntity(net.minecraft.world.level.block.entity.BlockEntityTypes.CHEST, net.minecraft.core.BlockPos.ZERO, Blocks.CHEST.defaultBlockState()) {}; assertEquals("block.minecraft.chest", key(MachineGUIRegistry.menuTitle(tile, Blocks.CHEST))); } diff --git a/src/test/java/zone/moddev/mc/basepower/resources/ClientResourceContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/ClientResourceContractTest.java index a2443e31..748f18f8 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/ClientResourceContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/ClientResourceContractTest.java @@ -84,16 +84,16 @@ public class ClientResourceContractTest { @Test public void developmentProfilePinsTheActualNative2612Family() throws IOException { String build = Files.readString(Paths.get("build.gradle"), StandardCharsets.UTF_8); - assertTrue("Combined Eclipse defaults must select the native 26.1.2 profile", - build.contains("'test-rig/profiles/native2612/dev-core.json'")); + assertTrue("Combined Eclipse defaults must select the native 26.2 profile", + build.contains("'test-rig/profiles/native262/dev-core.json'")); assertFalse("A CLI override must not hide a stale 1.21.11 default", build.contains("'test-rig/profiles/native12111/dev-core.json'")); assertFalse("A CLI override must not hide a stale 1.21.1 default", build.contains("'test-rig/profiles/native1211/dev-core.json'")); - JsonObject profile = read(Paths.get("test-rig/profiles/native2612/dev-core.json")); + JsonObject profile = read(Paths.get("test-rig/profiles/native262/dev-core.json")); assertEquals("development", profile.get("mode").getAsString()); - assertEquals("26.1.2", profile.get("minecraftVersion").getAsString()); - assertEquals("64.1.5", profile.get("forgeVersion").getAsString()); + assertEquals("26.2", profile.get("minecraftVersion").getAsString()); + assertEquals("65.1.0", profile.get("forgeVersion").getAsString()); java.util.Properties pins = new java.util.Properties(); try (Reader reader = Files.newBufferedReader(Paths.get("gradle.properties"), StandardCharsets.UTF_8)) { pins.load(reader); diff --git a/src/test/java/zone/moddev/mc/basepower/resources/Native2612PackFormatTest.java b/src/test/java/zone/moddev/mc/basepower/resources/Native262PackFormatTest.java similarity index 75% rename from src/test/java/zone/moddev/mc/basepower/resources/Native2612PackFormatTest.java rename to src/test/java/zone/moddev/mc/basepower/resources/Native262PackFormatTest.java index c0574ef1..0a89d374 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/Native2612PackFormatTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/Native262PackFormatTest.java @@ -12,17 +12,17 @@ import org.junit.Test; import static org.junit.Assert.*; -public class Native2612PackFormatTest { +public class Native262PackFormatTest { @BeforeClass public static void detectActualEngine() { SharedConstants.tryDetectVersion(); } @Test public void actualEngineDeclaresTheExactTargetFormats() { var version = SharedConstants.getCurrentVersion(); - assertEquals("26.1.2", version.name()); - assertEquals(4790, version.dataVersion().version()); - assertEquals(PackFormat.of(84, 0), version.packVersion(PackType.CLIENT_RESOURCES)); - assertEquals(PackFormat.of(101, 1), version.packVersion(PackType.SERVER_DATA)); + assertEquals("26.2", version.name()); + assertEquals(4903, version.dataVersion().version()); + assertEquals(PackFormat.of(88, 0), version.packVersion(PackType.CLIENT_RESOURCES)); + assertEquals(PackFormat.of(107, 1), version.packVersion(PackType.SERVER_DATA)); assertEquals(25, Runtime.version().feature()); } @@ -30,8 +30,8 @@ public class Native2612PackFormatTest { JsonObject metadata = JsonParser.parseString(Files.readString(Path.of("src/main/resources/pack.mcmeta"))) .getAsJsonObject().getAsJsonObject("pack"); var range = PackFormat.packCodec(PackType.SERVER_DATA).codec().parse(JsonOps.INSTANCE, metadata).getOrThrow(); - assertEquals(PackFormat.of(101, 1), range.minInclusive()); - assertEquals(PackFormat.of(101, 1), range.maxInclusive()); + assertEquals(PackFormat.of(107, 1), range.minInclusive()); + assertEquals(PackFormat.of(107, 1), range.maxInclusive()); assertFalse(metadata.has("pack_format")); assertFalse(metadata.has("supported_formats")); assertTrue(range.isValueInRange(SharedConstants.getCurrentVersion().packVersion(PackType.SERVER_DATA))); @@ -39,18 +39,18 @@ public class Native2612PackFormatTest { } @Test public void nativeCodecRejectsAnInvertedRange() { - var malformed = JsonParser.parseString("{\"min_format\":[101,2],\"max_format\":[101,1]}"); + var malformed = JsonParser.parseString("{\"min_format\":[107,2],\"max_format\":[107,1]}"); assertTrue(PackFormat.packCodec(PackType.SERVER_DATA).codec().parse(JsonOps.INSTANCE, malformed).error().isPresent()); } @Test public void serverOnlyRangeCannotQualifyTheClientOrOlderTargets() { - var dataOnly = JsonParser.parseString("{\"min_format\":[101,1],\"max_format\":[101,1]}"); + var dataOnly = JsonParser.parseString("{\"min_format\":[107,1],\"max_format\":[107,1]}"); var range = PackFormat.packCodec(PackType.CLIENT_RESOURCES).codec().parse(JsonOps.INSTANCE, dataOnly).getOrThrow(); - assertFalse(range.isValueInRange(PackFormat.of(84, 0))); + assertFalse(range.isValueInRange(PackFormat.of(88, 0))); assertFalse(range.isValueInRange(PackFormat.of(75, 0))); assertFalse(range.isValueInRange(PackFormat.of(94, 1))); - assertFalse(range.isValueInRange(PackFormat.of(101, 0))); - assertFalse(range.isValueInRange(PackFormat.of(101, 2))); - assertTrue(range.isValueInRange(PackFormat.of(101, 1))); + assertFalse(range.isValueInRange(PackFormat.of(107, 0))); + assertFalse(range.isValueInRange(PackFormat.of(107, 2))); + assertTrue(range.isValueInRange(PackFormat.of(107, 1))); } } diff --git a/src/test/java/zone/moddev/mc/basepower/resources/Native2612VillagerTradeResourceTest.java b/src/test/java/zone/moddev/mc/basepower/resources/Native262VillagerTradeResourceTest.java similarity index 97% rename from src/test/java/zone/moddev/mc/basepower/resources/Native2612VillagerTradeResourceTest.java rename to src/test/java/zone/moddev/mc/basepower/resources/Native262VillagerTradeResourceTest.java index 0c511c1b..28528685 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/Native2612VillagerTradeResourceTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/Native262VillagerTradeResourceTest.java @@ -23,13 +23,13 @@ import static org.junit.Assert.*; /** Native resource/field codecs only. Full Power-item trade decoding and offers need the packaged server. */ -public class Native2612VillagerTradeResourceTest { +public class Native262VillagerTradeResourceTest { private static final Path DATA = Path.of("src/main/resources/data"); @BeforeClass public static void bootstrap() { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); - assertEquals("26.1.2", SharedConstants.getCurrentVersion().name()); + assertEquals("26.2", SharedConstants.getCurrentVersion().name()); } @Test public void nativeTradeFieldsRetainBothHistoricalExchangeRates() throws Exception { @@ -108,7 +108,7 @@ private static void verify(JsonObject trade, String item, int min, int max) { var function = LootItemFunctions.ROOT_CODEC.parse(ops, modifier).getOrThrow(); assertTrue(function instanceof SetItemCountFunction); var encoded = LootItemFunctions.ROOT_CODEC.encodeStart(ops, function).getOrThrow().getAsJsonObject(); - assertFalse(encoded.get("add").getAsBoolean()); + assertFalse(encoded.has("add") && encoded.get("add").getAsBoolean()); var count = NumberProviders.CODEC.parse(ops, encoded.get("count")).getOrThrow(); assertTrue(count instanceof UniformGenerator); var uniform = (UniformGenerator) count; 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 4843f473..5b779ae2 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/NativeBlockEntityFactoryTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativeBlockEntityFactoryTest.java @@ -45,7 +45,7 @@ public class NativeBlockEntityFactoryTest { net.minecraft.core.RegistryAccess.fromRegistryOfRegistries(net.minecraft.core.registries.BuiltInRegistries.REGISTRY), saved)); assertEquals("NBT cannot replace the native factory's final position", position, tile.getBlockPos()); } - assertNull(block.getTicker(null, state, BlockEntityType.CHEST)); + assertNull(block.getTicker(null, state, net.minecraft.world.level.block.entity.BlockEntityTypes.CHEST)); } finally { Entities.CONVEYOR = old; } } diff --git a/src/test/java/zone/moddev/mc/basepower/resources/NativeGuiExtractionFixture.java b/src/test/java/zone/moddev/mc/basepower/resources/NativeGuiExtractionFixture.java index 177aa39c..fd73ec23 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/NativeGuiExtractionFixture.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativeGuiExtractionFixture.java @@ -28,15 +28,18 @@ public NativeGuiExtractionFixture(GuiRenderState target) throws Exception { var manager = allocate(TextureManager.class); set(manager, "byPath", textures); set(minecraft, "textureManager", manager); - set(minecraft, "gui", allocate(Gui.class)); + var gui = allocate(Gui.class); + set(gui, "hud", allocate(net.minecraft.client.gui.Hud.class)); + set(minecraft, "gui", gui); graphics = allocate(GuiGraphicsExtractor.class); set(graphics, "minecraft", minecraft); set(graphics, "pose", pose); set(graphics, "guiRenderState", target); var constructor = Class.forName(GuiGraphicsExtractor.class.getName() + "$ScissorStack") - .getDeclaredConstructor(); + .getDeclaredConstructor(net.minecraft.client.gui.navigation.ScreenRectangle.class); constructor.setAccessible(true); - set(graphics, "scissorStack", constructor.newInstance()); + set(graphics, "scissorStack", constructor.newInstance( + new net.minecraft.client.gui.navigation.ScreenRectangle(0, 0, 854, 480))); } public GpuTextureView texture(Identifier id) throws Exception { diff --git a/src/test/java/zone/moddev/mc/basepower/resources/NativePackFormatContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativePackFormatContractTest.java index c5f6fa8a..fa8d340f 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/NativePackFormatContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativePackFormatContractTest.java @@ -44,13 +44,13 @@ public class NativePackFormatContractTest { SharedConstants.RESOURCE_PACK_FORMAT_MAJOR, SharedConstants.RESOURCE_PACK_FORMAT_MINOR))); assertTrue(server.supportedFormats().isValueInRange(PackFormat.of( SharedConstants.DATA_PACK_FORMAT_MAJOR, SharedConstants.DATA_PACK_FORMAT_MINOR))); - assertEquals(PackFormat.of(84, 0).minorRange(), client.supportedFormats()); - assertEquals(PackFormat.of(101, 1), server.supportedFormats().minInclusive()); - assertEquals(PackFormat.of(101, 1), server.supportedFormats().maxInclusive()); + assertEquals(PackFormat.of(88, 0).minorRange(), client.supportedFormats()); + assertEquals(PackFormat.of(107, 1), server.supportedFormats().minInclusive()); + assertEquals(PackFormat.of(107, 1), server.supportedFormats().maxInclusive()); assertFalse(client.supportedFormats().isValueInRange(PackFormat.of(83))); assertFalse(client.supportedFormats().isValueInRange(PackFormat.of(85))); - assertFalse(server.supportedFormats().isValueInRange(PackFormat.of(101, 0))); - assertFalse(server.supportedFormats().isValueInRange(PackFormat.of(101, 2))); + assertFalse(server.supportedFormats().isValueInRange(PackFormat.of(107, 0))); + assertFalse(server.supportedFormats().isValueInRange(PackFormat.of(107, 2))); assertFalse(server.supportedFormats().isValueInRange(PackFormat.of(102))); assertFalse(client.supportedFormats().isValueInRange(PackFormat.of(94, 1))); assertFalse(server.supportedFormats().isValueInRange(PackFormat.of(75, 0))); diff --git a/src/test/java/zone/moddev/mc/basepower/resources/NativePackRangeContractTest.java b/src/test/java/zone/moddev/mc/basepower/resources/NativePackRangeContractTest.java index a780615d..27ca5713 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/NativePackRangeContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/NativePackRangeContractTest.java @@ -20,9 +20,9 @@ public class NativePackRangeContractTest { JsonObject pack = side.getAsJsonObject("nativeMetadata"); var range = PackFormat.packCodec(type).codec().parse(JsonOps.INSTANCE, pack).getOrThrow(); assertEquals(type.toString(), type == PackType.CLIENT_RESOURCES, - range.isValueInRange(PackFormat.of(84, 0))); + range.isValueInRange(PackFormat.of(88, 0))); assertEquals(type.toString(), type == PackType.SERVER_DATA, - range.isValueInRange(PackFormat.of(101, 1))); + range.isValueInRange(PackFormat.of(107, 1))); assertFalse(range.isValueInRange(PackFormat.of(83))); assertFalse(range.isValueInRange(PackFormat.of(102))); assertFalse(range.isValueInRange(PackFormat.of(75, 0))); @@ -48,12 +48,12 @@ public class NativePackRangeContractTest { } @Test public void dataOnlyRangeDoesNotQualifyClientResources() { - JsonObject dataOnly = JsonParser.parseString("{\"min_format\":[101,1],\"max_format\":[101,1]}") + JsonObject dataOnly = JsonParser.parseString("{\"min_format\":[107,1],\"max_format\":[107,1]}") .getAsJsonObject(); var range = PackFormat.packCodec(PackType.CLIENT_RESOURCES).codec() .parse(JsonOps.INSTANCE, dataOnly).getOrThrow(); - assertFalse(range.isValueInRange(PackFormat.of(84, 0))); + assertFalse(range.isValueInRange(PackFormat.of(88, 0))); assertFalse(range.isValueInRange(PackFormat.of(94, 1))); - assertTrue(range.isValueInRange(PackFormat.of(101, 1))); + assertTrue(range.isValueInRange(PackFormat.of(107, 1))); } } 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 bce5ca7c..6fdc2cdb 100644 --- a/src/test/java/zone/moddev/mc/basepower/resources/RecipeUnlockContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/resources/RecipeUnlockContractTest.java @@ -131,7 +131,7 @@ private static String unlockPredicateKey(JsonObject predicate) { JsonObject predicate = criterion.getAsJsonObject("conditions").getAsJsonArray("items").get(0).getAsJsonObject(); 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.criterion.ItemPredicate.CODEC.parse(ops, predicate).getOrThrow(); + var nativePredicate = net.minecraft.advancements.predicates.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.test(new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.DIRT))); diff --git a/src/test/java/zone/moddev/mc/basepower/testing/NativePackProcessContractTest.java b/src/test/java/zone/moddev/mc/basepower/testing/NativePackProcessContractTest.java index 6574d62f..522212fe 100644 --- a/src/test/java/zone/moddev/mc/basepower/testing/NativePackProcessContractTest.java +++ b/src/test/java/zone/moddev/mc/basepower/testing/NativePackProcessContractTest.java @@ -27,9 +27,9 @@ public class NativePackProcessContractTest { @Test public void nativeProofActuallyRunsWithTerminalDisabled() throws Exception { var proof = NativePackResourceProof.read(); - assertEquals("26.1.2", proof.get("minecraft").getAsString()); + assertEquals("26.2", proof.get("minecraft").getAsString()); assertEquals("RESOURCE_PACK_ONLY", proof.get("discoveryScope").getAsString()); - assertEquals("64.1.5", proof.get("forge").getAsString()); + assertEquals("65.1.0", proof.get("forge").getAsString()); assertEquals(25, proof.get("javaFeature").getAsInt()); assertTrue(proof.get("interactiveTerminalDisabled").getAsBoolean()); assertEquals(0, proof.getAsJsonArray("warningsAndErrors").size()); diff --git a/src/test/java/zone/moddev/mc/basepower/testing/NativePackResourceProof.java b/src/test/java/zone/moddev/mc/basepower/testing/NativePackResourceProof.java index d07fb9ce..1afdfa15 100644 --- a/src/test/java/zone/moddev/mc/basepower/testing/NativePackResourceProof.java +++ b/src/test/java/zone/moddev/mc/basepower/testing/NativePackResourceProof.java @@ -91,13 +91,13 @@ public static void main(String[] args) throws Exception { } SharedConstants.tryDetectVersion(); if (Runtime.version().feature() != 25 - || !"26.1.2".equals(SharedConstants.getCurrentVersion().name()) - || !"64.1.5".equals(net.minecraftforge.versions.forge.ForgeVersion.getVersion()) - || !net.minecraft.server.packs.metadata.pack.PackFormat.of(84, 0).equals( + || !"26.2".equals(SharedConstants.getCurrentVersion().name()) + || !"65.1.0".equals(net.minecraftforge.versions.forge.ForgeVersion.getVersion()) + || !net.minecraft.server.packs.metadata.pack.PackFormat.of(88, 0).equals( SharedConstants.getCurrentVersion().packVersion(PackType.CLIENT_RESOURCES)) - || !net.minecraft.server.packs.metadata.pack.PackFormat.of(101, 1).equals( + || !net.minecraft.server.packs.metadata.pack.PackFormat.of(107, 1).equals( SharedConstants.getCurrentVersion().packVersion(PackType.SERVER_DATA))) { - throw new AssertionError("Resource proof requires actual Forge 64.1.5 / Minecraft 26.1.2 on Java 25"); + throw new AssertionError("Resource proof requires actual Forge 65.1.0 / Minecraft 26.2 on Java 25"); } Bootstrap.bootStrap(); Path resources = Path.of(args[0]); diff --git a/src/test/java/zone/moddev/mc/basepower/util/InventoryLocksTest.java b/src/test/java/zone/moddev/mc/basepower/util/InventoryLocksTest.java index c3f89b8e..e685dcf3 100644 --- a/src/test/java/zone/moddev/mc/basepower/util/InventoryLocksTest.java +++ b/src/test/java/zone/moddev/mc/basepower/util/InventoryLocksTest.java @@ -1,6 +1,6 @@ package zone.moddev.mc.basepower.util; -import net.minecraft.advancements.criterion.ItemPredicate; +import net.minecraft.advancements.predicates.ItemPredicate; import net.minecraft.core.BlockPos; import net.minecraft.core.component.DataComponentMap; import net.minecraft.core.component.DataComponentPatch; diff --git a/test-rig/platform-controls/NativeLeashKnotPlatformControl.java b/test-rig/platform-controls/NativeLeashKnotPlatformControl.java index 6e1f247e..7e1fd8bf 100644 --- a/test-rig/platform-controls/NativeLeashKnotPlatformControl.java +++ b/test-rig/platform-controls/NativeLeashKnotPlatformControl.java @@ -42,7 +42,7 @@ public static void main(String[] args) throws Exception { CompoundTag observed = NativePlatformSupport.observe(knot); boolean selected = knot.shouldBeSaved(); boolean encodable = knot.getEncodeId() != null; - if (knot.getClass() != LeashFenceKnotEntity.class || knot.getType() != EntityType.LEASH_KNOT + if (knot.getClass() != LeashFenceKnotEntity.class || knot.getType() != net.minecraft.world.entity.EntityTypes.LEASH_KNOT || knot.getType().canSerialize() || rootAccepted || passengerAccepted || !selected || encodable || !rootOutput.buildResult().isEmpty() || !passengerOutput.buildResult().isEmpty() || !ID.equals(before.read("UUID", UUIDUtil.CODEC).orElseThrow()) || !ID.equals(knot.getUUID()) @@ -65,7 +65,7 @@ public static void main(String[] args) throws Exception { || !"minecraft:item".equals(persistentOutput.buildResult().getString("id").orElseThrow())) { throw new AssertionError("Persistent native writer control failed"); } - LightningBolt otherNoSave = new LightningBolt(EntityType.LIGHTNING_BOLT, world); + LightningBolt otherNoSave = new LightningBolt(net.minecraft.world.entity.EntityTypes.LIGHTNING_BOLT, world); if (otherNoSave.getType().canSerialize() || otherNoSave.save(NativePlatformSupport.output(otherNoSave))) { throw new AssertionError("Unexpected native lightning writer eligibility"); } @@ -79,7 +79,7 @@ public static void main(String[] args) throws Exception { Map proof = new LinkedHashMap<>(); proof.put("format", 1); - proof.put("policy", "native2612-leash-knot-nosave-v1"); + proof.put("policy", "native262-leash-knot-nosave-v1"); proof.put("minecraftVersion", SharedConstants.getCurrentVersion().name()); proof.put("dataVersion", SharedConstants.getCurrentVersion().dataVersion().version()); proof.put("forgeVersion", net.minecraftforge.versions.forge.ForgeVersion.getVersion()); diff --git a/test-rig/platform-controls/NativePlatformSupport.java b/test-rig/platform-controls/NativePlatformSupport.java index f1545fc5..9a230690 100644 --- a/test-rig/platform-controls/NativePlatformSupport.java +++ b/test-rig/platform-controls/NativePlatformSupport.java @@ -52,6 +52,7 @@ static ServerLevel context(boolean worldgen) throws Exception { instance.setAccessible(true); ServerLevel world = (ServerLevel) unsafe.getMethod("allocateInstance", Class.class) .invoke(instance.get(null), ServerLevel.class); + zone.moddev.mc.advantageworks.unit.NativeEmptyEntityTracker.install(world); field(world, "dimension", Level.NETHER); field(world, "random", RandomSource.create(0)); RegistryAccess registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); @@ -137,6 +138,8 @@ static void sourceEvidence(Map proof, Class... types) throws for (String source : List.of("NativePlatformSupport.java", "NativeLeashKnotPlatformControl.java")) { controls.put(source, sha(Files.readAllBytes(Path.of("platform-controls", source)))); } + controls.put("NativeEmptyEntityTracker.java", sha(Files.readAllBytes(Path.of( + "unit-support/zone/moddev/mc/advantageworks/unit/NativeEmptyEntityTracker.java")))); proof.put("controlSourceSha256", controls); } } diff --git a/test-rig/platform-controls/NativeWolfPlatformControl.java b/test-rig/platform-controls/NativeWolfPlatformControl.java index d7dcb9a1..9d7f626e 100644 --- a/test-rig/platform-controls/NativeWolfPlatformControl.java +++ b/test-rig/platform-controls/NativeWolfPlatformControl.java @@ -38,7 +38,7 @@ private static ServerLevel context() throws Exception { } private static Wolf wolf(ServerLevel world) { - Wolf wolf = new Wolf(EntityType.WOLF, world); wolf.setUUID(ID); return wolf; + Wolf wolf = new Wolf(net.minecraft.world.entity.EntityTypes.WOLF, world); wolf.setUUID(ID); return wolf; } private static void control(Path root, ServerLevel world, boolean tame, float health) throws Exception { diff --git a/test-rig/profiles/native262/dev-core.json b/test-rig/profiles/native262/dev-core.json new file mode 100644 index 00000000..9b86eaf8 --- /dev/null +++ b/test-rig/profiles/native262/dev-core.json @@ -0,0 +1,20 @@ +{ + "name": "dev-core", + "mode": "development", + "qualificationStatus": "DEVELOPMENT_ONLY_NOT_QUALIFIED", + "minecraftVersion": "26.2", + "forgeVersion": "65.1.0", + "powerDeobfSha256": "06522DB981D3A0ED36D07FD162CE72F63561DD02CFAF9E060EFE7F6A08989633", + "mods": [ + { + "id": "basesteam", + "version": "2.2.18.2602001", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "id": "baseelectric", + "version": "2.2.18.2602001", + "sha256": "0000000000000000000000000000000000000000000000000000000000000000" + } + ] +} diff --git a/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/NativeEmptyEntityTracker.java b/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/NativeEmptyEntityTracker.java new file mode 100644 index 00000000..183d3a32 --- /dev/null +++ b/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/NativeEmptyEntityTracker.java @@ -0,0 +1,37 @@ +package zone.moddev.mc.advantageworks.unit; + +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import java.lang.reflect.Field; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; + +/** Empty native entity-ID lookup for an unticked, worldless test context. */ +public final class NativeEmptyEntityTracker { + private NativeEmptyEntityTracker() { } + + public static void install(ServerLevel world) throws ReflectiveOperationException { + Class type = Class.forName("sun.misc.Unsafe"); + Field instance = type.getDeclaredField("theUnsafe"); + instance.setAccessible(true); + Object allocator = instance.get(null); + var allocate = type.getMethod("allocateInstance", Class.class); + var cache = (ServerChunkCache) allocate.invoke(allocator, ServerChunkCache.class); + var chunks = (ChunkMap) allocate.invoke(allocator, ChunkMap.class); + var entities = new Int2ObjectOpenHashMap<>(); + set(chunks, "entityMap", entities); + set(cache, "chunkMap", chunks); + set(world, "chunkSource", cache); + int first = world.getNextEntityId(), second = world.getNextEntityId(); + if (first == 0 || second == 0 || first == second || !entities.isEmpty() + || cache.hasEntityWithId(first) || cache.hasEntityWithId(second)) { + throw new AssertionError("Empty native entity-ID lookup did not stay empty"); + } + } + + private static void set(Object owner, String name, Object value) throws ReflectiveOperationException { + Field field = owner.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(owner, value); + } +} From f84b2362449e39eff5e76696dd64ce4cb144da4f Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Thu, 12 Nov 2026 16:11:57 +0000 Subject: [PATCH 2/3] Pin the local retained-settings repair for 26.2 testing --- .github/workflows/ci.yml | 4 ++-- .github/workflows/codeql-analysis.yml | 4 ++-- build.gradle | 4 ++-- docs/26.2/PORT-STATUS.md | 13 +++++++++---- gradle.properties | 10 +++++----- src/main/resources/META-INF/mods.toml | 2 +- test-rig/profiles/native262/dev-core.json | 2 +- 7 files changed, 22 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c22ba426..e5b8b9bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/OreSpawn - ref: c64d189c899ca5f277078c4c1efe1b1fd1af1742 + ref: dcaaa933299dd8b1ebc73e31c9e5acd36dd7957c path: .ci-dependencies/OreSpawn - name: Check out pinned Base Metals uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -68,7 +68,7 @@ jobs: (cd "$basemetals" && bash gradle/stage-orespawn-release.sh "$mirror") (cd "$orespawn" && bash gradlew $MMD_GRADLE_TOOLCHAINS jar --no-daemon --stacktrace) (cd "$basemetals" && bash gradlew $MMD_GRADLE_TOOLCHAINS jar -PorespawnVerificationRepository="$mirror" --no-daemon --stacktrace) - osjar="$orespawn/build/libs/OreSpawn-4.1.0.2602001.jar" + osjar="$orespawn/build/libs/OreSpawn-4.1.1.2602001.jar" bmjar="$basemetals/build/libs/BaseMetals-3.1.2.2602001.jar" test -f "$osjar" && test -f "$bmjar" echo "orespawn=$osjar" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8d8a1669..e3c6a6cc 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/OreSpawn - ref: c64d189c899ca5f277078c4c1efe1b1fd1af1742 + ref: dcaaa933299dd8b1ebc73e31c9e5acd36dd7957c path: .ci-dependencies/OreSpawn - name: Check out pinned Base Metals uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -69,7 +69,7 @@ jobs: chmod +x "$orespawn/gradlew" "$basemetals/gradlew" (cd "$orespawn" && ./gradlew $MMD_GRADLE_TOOLCHAINS jar --no-daemon --stacktrace) (cd "$basemetals" && ./gradlew $MMD_GRADLE_TOOLCHAINS jar -PorespawnVerificationRepository="$mirror" --no-daemon --stacktrace) - echo "orespawn=$orespawn/build/libs/OreSpawn-4.1.0.2602001.jar" >> "$GITHUB_OUTPUT" + echo "orespawn=$orespawn/build/libs/OreSpawn-4.1.1.2602001.jar" >> "$GITHUB_OUTPUT" echo "basemetals=$basemetals/build/libs/BaseMetals-3.1.2.2602001.jar" >> "$GITHUB_OUTPUT" echo "mavenizer=$mavenizer" >> "$GITHUB_OUTPUT" - name: Initialize CodeQL diff --git a/build.gradle b/build.gradle index 41d66d5e..d236eac0 100644 --- a/build.gradle +++ b/build.gradle @@ -345,9 +345,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected CurseForge dependency or game-version metadata') } if (project.basemetals_version != '3.1.2.2602001' - || project.orespawn_version != '4.1.0.2602001' + || project.orespawn_version != '4.1.1.2602001' || !(project.basemetals_source_commit ==~ /[a-f0-9]{40}/) - || project.orespawn_source_commit != 'c64d189c899ca5f277078c4c1efe1b1fd1af1742') { + || project.orespawn_source_commit != 'dcaaa933299dd8b1ebc73e31c9e5acd36dd7957c') { throw new GradleException('Unexpected Base Metals or OreSpawn dependency identity') } String mainSource = file( diff --git a/docs/26.2/PORT-STATUS.md b/docs/26.2/PORT-STATUS.md index 42d61ad3..e4bd2d69 100644 --- a/docs/26.2/PORT-STATUS.md +++ b/docs/26.2/PORT-STATUS.md @@ -8,10 +8,15 @@ The starting target is Forge 65.1.0, official 26.2 names, MCP 20260616.103818 and Temurin 25.0.3+9. Minecraft's own version metadata declares data version 4903, client resource format 88.0 and server data format 107.1. -Base Metals needs its shared migration extension ported before the old-world -suite can run. OreSpawn's retained-provider-settings repair is present locally -but is not yet a public 26.2 release. Dependency and family checksums will be -sealed only after the actual candidates have been built and verified. +Base Metals' shared migration extension is ported locally as 3.1.2.2602001. +Its 135 unit tests and release checks pass. OreSpawn's retained-provider-settings +repair is qualified locally as 4.1.1.2602001, with 261 unit tests, surface +integration and release/Eclipse checks. Neither dependency patch is published. + +Power passes all 357 unit tests and its initial release/Eclipse audits. The +dependency repin is being requalified; full family runtime testing is still +pending. A separate native control confirms the leash-helper saveability +contract on this actual engine without loading Power or the test mod. Remaining work includes production compilation, the full unit/resource/Eclipse audits, packaged machine and migration checks, full restarts, client rendering, diff --git a/gradle.properties b/gradle.properties index 95fca302..5a482bd3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,7 +28,7 @@ mod_name=basePower mod_license=LGPL-3.0-only mod_version=2.4.17.2602001 # Frozen build candidate, not a claim of completed gameplay qualification or publication. -release_player_sha256=06522DB981D3A0ED36D07FD162CE72F63561DD02CFAF9E060EFE7F6A08989633 +release_player_sha256=7E456DE1A45140E86415A701D186FEDE08C4AB63D53C6B7848935DB70CBC68B1 basesteam_version=2.2.18.2602001 baseelectric_version=2.2.18.2602001 mod_group=zone.moddev.mc.basepower @@ -44,8 +44,8 @@ basemetals_source_commit=523335a59ecff399e40a7c66eb121a3aabdc45ba basemetals_curse_project_id=240967 basemetals_deobf_sha256=15CBD0C8DD95BBA846969929B8767887BA4B8152DFB10C9C58FB314AD39FB9E1 -orespawn_version=4.1.0.2602001 -orespawn_source_commit=c64d189c899ca5f277078c4c1efe1b1fd1af1742 +orespawn_version=4.1.1.2602001 +orespawn_source_commit=dcaaa933299dd8b1ebc73e31c9e5acd36dd7957c orespawn_curse_project_id=245586 -orespawn_public_file_id=9332183 -orespawn_deobf_sha256=F74785025898DF059E0F00F6ADA53364E07ECBBC306807ED7CD55350609E2547 +orespawn_public_file_id= +orespawn_deobf_sha256=BC0658DF2E149CC23E65E5771F3FF62E2BB13FFDA4E316AB632EB92EF7FA5A1F diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index fb152333..93b1c8fd 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -22,7 +22,7 @@ side="BOTH" [[dependencies.basepower]] modId="orespawn" mandatory=true -versionRange="[4.1.0.2602001,5.0.0)" +versionRange="[4.1.1.2602001,5.0.0)" ordering="AFTER" side="BOTH" diff --git a/test-rig/profiles/native262/dev-core.json b/test-rig/profiles/native262/dev-core.json index 9b86eaf8..d2fea194 100644 --- a/test-rig/profiles/native262/dev-core.json +++ b/test-rig/profiles/native262/dev-core.json @@ -4,7 +4,7 @@ "qualificationStatus": "DEVELOPMENT_ONLY_NOT_QUALIFIED", "minecraftVersion": "26.2", "forgeVersion": "65.1.0", - "powerDeobfSha256": "06522DB981D3A0ED36D07FD162CE72F63561DD02CFAF9E060EFE7F6A08989633", + "powerDeobfSha256": "7E456DE1A45140E86415A701D186FEDE08C4AB63D53C6B7848935DB70CBC68B1", "mods": [ { "id": "basesteam", From 446bc6c49a6b0d570715026789022eea89d17f70 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Thu, 12 Nov 2026 17:58:10 +0000 Subject: [PATCH 3/3] Qualify the 26.2 machines, world upgrades and native client --- CHANGELOG.txt | 8 +- README.md | 55 +- docs/26.2/PORT-STATUS.md | 32 +- docs/26.2/QUALIFICATION.md | 97 ++ test-rig/NATIVE262.md | 89 ++ test-rig/README.md | 12 + test-rig/build.gradle | 24 +- .../client/ChildRendererGeometryProbe.java | 28 +- .../client/ClientResourceProbe.java | 38 +- .../client/NativeExperimentalFixtureGate.java | 22 +- .../client/NativeGuiEvidence.java | 22 +- .../client/NativeGuiProjection.java | 34 +- .../client/NativeVanillaResourceProbe.java | 2 +- .../client/NativeWorldOilProbe.java | 31 +- .../advantageworks/client/OilRenderProbe.java | 16 +- .../client/RecipeBookGroupingProbe.java | 10 +- .../resources/META-INF/mods.toml | 8 +- .../client-resources/resources/pack.mcmeta | 4 +- .../NativeExperimentalFixtureGateTest.java | 4 +- .../NativeFamilyAtlasRegistrationTest.java | 2 +- test-rig/gradle.properties | 12 +- .../native262-client-platform-sequences.json | 596 ++++++++++++ .../fixtures/native262-file-host-perflib.json | 169 ++++ .../native262-server-host-perflib.json | 197 ++++ .../native262-server-netty-java25.json | 53 ++ test-rig/migration/native262.py | 451 +++++++++ .../migration/native262_actor_comparison.py | 124 +++ .../migration/native262_actor_fixtures.py | 152 +++ .../native262_actor_transformer_order.py | 330 +++++++ test-rig/migration/native262_actors.py | 738 +++++++++++++++ test-rig/migration/native262_broad.py | 126 +++ test-rig/migration/native262_client.py | 872 ++++++++++++++++++ .../migration/native262_client_diagnostics.py | 116 +++ test-rig/migration/native262_endurance.py | 307 ++++++ .../migration/native262_endurance_replay.py | 342 +++++++ test-rig/migration/native262_engine_proof.py | 80 ++ test-rig/migration/native262_interop.py | 87 ++ test-rig/migration/native262_layout.py | 128 +++ .../native262_legacy_alternatives.py | 119 +++ test-rig/migration/native262_machines.py | 101 ++ test-rig/migration/native262_networks.py | 294 ++++++ test-rig/migration/native262_oil.py | 178 ++++ test-rig/migration/native262_operations.py | 192 ++++ .../native262_platform_diagnostics.py | 113 +++ test-rig/migration/native262_resources.py | 259 ++++++ .../migration/native262_retained_worldgen.py | 279 ++++++ .../native262_stopped_actor_reference.py | 163 ++++ .../migration/native262_strict_migration.py | 151 +++ .../migration/native262_transformer_order.py | 481 ++++++++++ test-rig/migration/native262_upgrade.py | 402 ++++++++ test-rig/migration/native262_worldgen.py | 155 ++++ test-rig/migration/test_native262.py | 223 +++++ .../test_native262_actor_transformer_order.py | 274 ++++++ test-rig/migration/test_native262_actors.py | 146 +++ test-rig/migration/test_native262_broad.py | 172 ++++ test-rig/migration/test_native262_client.py | 588 ++++++++++++ .../test_native262_client_diagnostics.py | 75 ++ .../migration/test_native262_endurance.py | 247 +++++ test-rig/migration/test_native262_interop.py | 85 ++ test-rig/migration/test_native262_layout.py | 165 ++++ .../test_native262_legacy_alternatives.py | 91 ++ test-rig/migration/test_native262_machines.py | 56 ++ test-rig/migration/test_native262_networks.py | 159 ++++ test-rig/migration/test_native262_oil.py | 162 ++++ .../migration/test_native262_operations.py | 86 ++ .../test_native262_platform_diagnostics.py | 90 ++ .../migration/test_native262_resources.py | 144 +++ .../test_native262_retained_worldgen.py | 145 +++ .../test_native262_strict_migration.py | 63 ++ .../test_native262_transformer_order.py | 293 ++++++ test-rig/migration/test_native262_upgrade.py | 467 ++++++++++ test-rig/migration/test_native262_worldgen.py | 86 ++ .../NativePlatformSupport.java | 4 +- .../NativeSystemReportPlatformControl.java | 63 ++ .../NativeWolfPlatformControl.java | 49 + .../NettyJava25PlatformControl.java | 63 ++ test-rig/profiles/native262/dev-core.json | 4 +- .../legacy-visible-alternatives.json | 11 + .../advantageworks/DimensionEntityProbe.java | 4 +- .../ElectricOperationalCases.java | 4 +- .../Native117StoppedEntitiesSnapshotV2.java | 8 +- .../mc/advantageworks/NativeInteropProbe.java | 2 +- .../advantageworks/PowerOperationalCases.java | 2 +- .../mc/advantageworks/ProgressionProbe.java | 2 +- .../mc/advantageworks/RecipeUnlockProbe.java | 4 +- .../SteamOperationalActors.java | 2 +- .../SteamOperationalSupplementalCases.java | 2 +- .../src/main/resources/META-INF/mods.toml | 8 +- .../native262-helper-contract.json | 55 ++ .../mc/advantageworks/runner/WorksRunner.java | 99 +- .../Native117EntityContextTest.java | 3 +- .../Native117StoppedEntitiesSnapshotTest.java | 5 +- ...ative117StoppedEntitiesSnapshotV2Test.java | 2 +- .../Native12111HelperContractTest.java | 10 +- .../mc/advantageworks/NativeActorIoTest.java | 4 +- .../NativeEntityReadSelectorsTest.java | 2 +- .../runner/Native262ProfileTest.java | 86 ++ .../unit/NativeEmptyEntityTracker.java | 12 +- 98 files changed, 12418 insertions(+), 214 deletions(-) create mode 100644 docs/26.2/QUALIFICATION.md create mode 100644 test-rig/NATIVE262.md create mode 100644 test-rig/migration/fixtures/native262-client-platform-sequences.json create mode 100644 test-rig/migration/fixtures/native262-file-host-perflib.json create mode 100644 test-rig/migration/fixtures/native262-server-host-perflib.json create mode 100644 test-rig/migration/fixtures/native262-server-netty-java25.json create mode 100644 test-rig/migration/native262.py create mode 100644 test-rig/migration/native262_actor_comparison.py create mode 100644 test-rig/migration/native262_actor_fixtures.py create mode 100644 test-rig/migration/native262_actor_transformer_order.py create mode 100644 test-rig/migration/native262_actors.py create mode 100644 test-rig/migration/native262_broad.py create mode 100644 test-rig/migration/native262_client.py create mode 100644 test-rig/migration/native262_client_diagnostics.py create mode 100644 test-rig/migration/native262_endurance.py create mode 100644 test-rig/migration/native262_endurance_replay.py create mode 100644 test-rig/migration/native262_engine_proof.py create mode 100644 test-rig/migration/native262_interop.py create mode 100644 test-rig/migration/native262_layout.py create mode 100644 test-rig/migration/native262_legacy_alternatives.py create mode 100644 test-rig/migration/native262_machines.py create mode 100644 test-rig/migration/native262_networks.py create mode 100644 test-rig/migration/native262_oil.py create mode 100644 test-rig/migration/native262_operations.py create mode 100644 test-rig/migration/native262_platform_diagnostics.py create mode 100644 test-rig/migration/native262_resources.py create mode 100644 test-rig/migration/native262_retained_worldgen.py create mode 100644 test-rig/migration/native262_stopped_actor_reference.py create mode 100644 test-rig/migration/native262_strict_migration.py create mode 100644 test-rig/migration/native262_transformer_order.py create mode 100644 test-rig/migration/native262_upgrade.py create mode 100644 test-rig/migration/native262_worldgen.py create mode 100644 test-rig/migration/test_native262.py create mode 100644 test-rig/migration/test_native262_actor_transformer_order.py create mode 100644 test-rig/migration/test_native262_actors.py create mode 100644 test-rig/migration/test_native262_broad.py create mode 100644 test-rig/migration/test_native262_client.py create mode 100644 test-rig/migration/test_native262_client_diagnostics.py create mode 100644 test-rig/migration/test_native262_endurance.py create mode 100644 test-rig/migration/test_native262_interop.py create mode 100644 test-rig/migration/test_native262_layout.py create mode 100644 test-rig/migration/test_native262_legacy_alternatives.py create mode 100644 test-rig/migration/test_native262_machines.py create mode 100644 test-rig/migration/test_native262_networks.py create mode 100644 test-rig/migration/test_native262_oil.py create mode 100644 test-rig/migration/test_native262_operations.py create mode 100644 test-rig/migration/test_native262_platform_diagnostics.py create mode 100644 test-rig/migration/test_native262_resources.py create mode 100644 test-rig/migration/test_native262_retained_worldgen.py create mode 100644 test-rig/migration/test_native262_strict_migration.py create mode 100644 test-rig/migration/test_native262_transformer_order.py create mode 100644 test-rig/migration/test_native262_upgrade.py create mode 100644 test-rig/migration/test_native262_worldgen.py create mode 100644 test-rig/platform-controls/NativeSystemReportPlatformControl.java create mode 100644 test-rig/platform-controls/NettyJava25PlatformControl.java create mode 100644 test-rig/profiles/native262/legacy-visible-alternatives.json create mode 100644 test-rig/src/main/resources/assets/advantageworkstest/native262-helper-contract.json create mode 100644 test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native262ProfileTest.java diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5d1e12f4..9389e4be 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,7 +1,11 @@ -basePower 2.4.17.2602001 (Minecraft 26.2, in development) +basePower 2.4.17.2602001 (Minecraft 26.2, local test candidate) -- Begin the Forge 65 port from the accepted 26.1.2 source, keeping existing machine identities and saved data. +- Bring the accepted 26.1.2 source to Forge 65 and Java 25, keeping existing machine identities and saved data. - Update the native data and client resource format checks for Minecraft 26.2. +- Use the current native GUI and rendering APIs without changing machine balance. +- Recognise Minecraft's new sulfur-caves biome when checking saved registry snapshots. +- Keep the shared Base Metals migration hook and qualify OreSpawn's retained-settings repair locally. +- Complete machine, recipe progression, old-world migration, full-restart, graphical and two-world 30-minute endurance checks. Manual testing is next; nothing from 26.2 is published yet. basePower 2.4.16.2601021 (Minecraft 26.1.2) diff --git a/README.md b/README.md index 45678430..17a9a95d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ # basePower -**Minecraft 26.2 (Forge), version 2.4.17.2602001, in development.** +**Minecraft 26.2 (Forge), version 2.4.17.2602001. Local test candidate.** -This branch is a fresh port of the accepted 26.1.2 source. It has not yet passed -the 26.2 qualification suite. See the [26.2 port record](docs/26.2/PORT-STATUS.md). +This branch is a fresh port of the accepted 26.1.2 source. Its automated +machine, migration, graphical and endurance checks have passed. It is ready +for manual testing, not publication. See the +[26.2 qualification](docs/26.2/QUALIFICATION.md) and +[port record](docs/26.2/PORT-STATUS.md). The [26.1.2 qualification](docs/26.1.2/QUALIFICATION.md) and [port record](docs/26.1.2/PORT-STATUS.md) keep the earlier findings separate. The [1.21.11 qualification](docs/1.21.11/QUALIFICATION.md) remains historical @@ -15,21 +18,22 @@ matching baseSteam and baseElectric candidates with basePower. basePower continues DrCyano's Power Advantage. It provides shared power, fluids and item transport for baseSteam and baseElectric. -## Testing On 26.1.2 +## Testing On 26.2 -- Use MMD OreSpawn `4.1.1.2601021` on both client and server. This includes - the fix that keeps saved ore settings when providers rename their rules. -- This candidate requires the local Base Metals `3.1.2.2601021` migration-hook - build on both client and server. It has not been published; the public - `3.1.1` jar is not a substitute for the upgrade tests. +- Use the local MMD OreSpawn `4.1.1.2602001` on both client and server. This + includes the retained-provider-settings fix used by the upgrade tests. +- Use the local Base Metals `3.1.2.2602001` migration-hook build on both sides. + Neither dependency fix is published. Older public jars are not substitutes + for this test kit; exact source commits and hashes are in `gradle.properties`. - baseSteam and baseElectric are separate mods. Use only their matching - 26.1.2 builds. Additional Loot Tables is not needed for the built-in loot. + 26.2 builds: version `2.2.18.2602001` for each. 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 used disposable copies of original Advantage and released base* saves, including connected machines, and check them after a full restart. Check that -the other mods in your world support 26.1.2 before attempting an upgrade. +the other mods in your world support 26.2 before attempting an upgrade. The accepted [1.14 results](docs/1.14/BROADER-QUALIFICATION.md) are historical evidence, not qualification of this port or a whole modpack. @@ -37,17 +41,17 @@ 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 26.1.2 +## Building Minecraft 26.2 -The build uses Forge 64.1.5, official `26.1.2` names, -MCP `20260409.101008`, ForgeGradle 7.0.34 and Gradle 9.6.1. +The build uses Forge 65.1.0, official `26.2` names, +MCP `20260616.103818`, ForgeGradle 7.0.34 and Gradle 9.6.1. Use Java `25.0.3+9` for Gradle, compilation, Mavenizer and Minecraft. Pass its home using `-Porg.gradle.java.installations.paths=` and disable automatic discovery and downloads for a reproducibility check. -Use the pinned Base Metals `3.1.2.2601021` and OreSpawn `4.1.1.2601021` jars. +Use the pinned Base Metals `3.1.2.2602001` and OreSpawn `4.1.1.2602001` jars. The source pins and checksums are in `gradle.properties`. -Forge 64 also needs the Mavenizer compatibility jar in the pinned OreSpawn +Forge 65 also needs the Mavenizer compatibility jar in the pinned OreSpawn checkout's `ci-fixtures/tools/` directory. This is the same checksum-verified source preparation tool used by OreSpawn; its manifest makes no source changes for this target. It is not part of the mod or game runtime. @@ -65,8 +69,8 @@ gradlew.bat clean check build javadoc genEclipseRuns eclipse isolateEclipseProdu The default dependency jars are: ```text -D:\BaseMetals\BaseMetals 2612\BaseMetals\build\libs\BaseMetals-3.1.2.2601021.jar -D:\MinecraftMineralogy\OS2612\OreSpawn\build\libs\OreSpawn-4.1.1.2601021.jar +D:\BaseMetals\BaseMetals 262\BaseMetals\build\libs\BaseMetals-3.1.2.2602001.jar +D:\MinecraftMineralogy\OS262\OreSpawn\build\libs\OreSpawn-4.1.1.2602001.jar ``` Other layouts can pass `-PbaseMetalsDeobfJar=` and `-PoreSpawnDeobfJar=`. @@ -75,10 +79,11 @@ Dependencies are never embedded in PowerAdvantage artifacts. Dependency checksums and source commits are recorded in `gradle.properties`. Unknown artifacts fail the dependency audit. The release audit also rejects a player jar that differs from the frozen test kit. Hosted CI requires the Base -Metals pin to be on its upstream `master-26.1.2` branch; a locally tested pin -does not qualify a hosted build. +Metals pin to be on its upstream `master-26.2` branch; a locally tested pin +does not qualify a hosted build. The current local dependency fixes still +need their own review and publication before that gate can pass. -Release jars are written to `build/libs`. Forge 64 uses the same mod jar for play +Release jars are written to `build/libs`. Forge 65 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: @@ -91,10 +96,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.17.2601021` jars, +To prepare a combined client, build the baseSteam and baseElectric `2.2.18.2602001` jars, run `gradlew.bat prepareCombinedEclipseLaunch`, refresh Eclipse, then launch `runClientAllAdvantage.launch`. Inputs must match -`test-rig/profiles/native2612/dev-core.json`; alternative layouts can pass +`test-rig/profiles/native262/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. @@ -129,7 +134,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) describes the prior -qualification and the checks carried forward into later ports. +The [Advantage Works 26.2 guide](test-rig/NATIVE262.md) describes the current +drivers, input seals and independent replay. Earlier guides remain historical. 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/docs/26.2/PORT-STATUS.md b/docs/26.2/PORT-STATUS.md index e4bd2d69..a746a0c5 100644 --- a/docs/26.2/PORT-STATUS.md +++ b/docs/26.2/PORT-STATUS.md @@ -1,8 +1,10 @@ # Minecraft 26.2 Port -This is a local port of the accepted Minecraft 26.1.2 source, not a tested or -published release. Earlier qualification records apply only to their named -Minecraft version and exact jars. +This is a local port of the accepted Minecraft 26.1.2 source. The automated +checks have passed; manual testing and publication are separate steps. +See the [26.2 qualification record](QUALIFICATION.md) for the exact jars, +completed checks and remaining manual scope. Earlier records apply only to +their named Minecraft version and exact jars. The starting target is Forge 65.1.0, official 26.2 names, MCP 20260616.103818 and Temurin 25.0.3+9. Minecraft's own version metadata declares @@ -13,11 +15,21 @@ Its 135 unit tests and release checks pass. OreSpawn's retained-provider-setting repair is qualified locally as 4.1.1.2602001, with 261 unit tests, surface integration and release/Eclipse checks. Neither dependency patch is published. -Power passes all 357 unit tests and its initial release/Eclipse audits. The -dependency repin is being requalified; full family runtime testing is still -pending. A separate native control confirms the leash-helper saveability -contract on this actual engine without loading Power or the test mod. +Power passes all 357 unit tests, fresh Eclipse compilation and its release +audits. Independent empty-cache builds reproduce the player, sources and +Javadoc archives. Steam and Electric pass their own equivalent build gates. +The complete family passes operating migrations, restart, network-unloading, +moving-drill, generation, progression, graphical and measured endurance checks. +A separate native control confirms the leash-helper saveability contract on +this actual engine without loading Power or the test mod. -Remaining work includes production compilation, the full unit/resource/Eclipse -audits, packaged machine and migration checks, full restarts, client rendering, -and measured endurance tests. Manual testing comes after those checks. +The port uses the new native GUI, render-target and vertex APIs without +changing machine balance or registry identities. Power recognises the new +native sulfur-caves biome when validating saved biome snapshots. The graphical +test probe submits native GPU work before waiting on its readback fence; +that test-only correction is not a change to player fluid rendering. + +No 26.2 source, dependency fix, tag or release has been pushed or published. +Manual testing should use the prepared combined Eclipse launch and the pinned +local dependencies. Remaining observations include normal play, turbine +orientation/spin, weapon handling and the other mods in a real modpack. diff --git a/docs/26.2/QUALIFICATION.md b/docs/26.2/QUALIFICATION.md new file mode 100644 index 00000000..84b09077 --- /dev/null +++ b/docs/26.2/QUALIFICATION.md @@ -0,0 +1,97 @@ +# Minecraft 26.2 Test Results + +The three local candidates have passed the automated checks below. They are +ready for manual testing, not release. No 26.2 implementation or dependency +fix has been pushed or published, and hosted CI has not qualified these pins. + +## Exact Test Kit + +| Mod | Version | Player Jar SHA-256 | +| --- | --- | --- | +| basePower | 2.4.17.2602001 | `7E456DE1A45140E86415A701D186FEDE08C4AB63D53C6B7848935DB70CBC68B1` | +| baseSteam | 2.2.18.2602001 | `EDD10D6ED4A623F6EAA364D21480FDA3B777FB15C2195D437882DB09C0F415AB` | +| baseElectric | 2.2.18.2602001 | `A7D12A66CA91A6BACCFEFD2CED9D3EC962F5FEB73BF632E1683F99084384F938` | +| Base Metals | 3.1.2.2602001 | `15CBD0C8DD95BBA846969929B8767887BA4B8152DFB10C9C58FB314AD39FB9E1` | +| OreSpawn | 4.1.1.2602001 | `BC0658DF2E149CC23E65E5771F3FF62E2BB13FFDA4E316AB632EB92EF7FA5A1F` | +| Advantage Works test harness | 1.1.7.2602001 | `9914D684980E3980257CB2097F972F7128A670F3B814EBEB019AF73D7A2E563E` | + +Base Metals and OreSpawn are installed throughout. Both include approved +local fixes, not public replacements: Base Metals' shared migration extension +and OreSpawn's retained-provider-settings repair. Their exact source pins are +in `gradle.properties`. The harness is a separate server test mod, never +included in any player jar or normal Eclipse launch. + +The target is Forge 65.1.0, official 26.2 names, MCP 20260616.103818 and +Temurin 25.0.3+9. Native data version is 4903; data/client formats are +107.1/88.0. Frozen profile03 has SHA-256 +`A98D4F9B54A0AE47392D9A3CF81335F5E228F718C7D322DF9838A94AD27D9736`. +Its pre-run exploratory label is unchanged. Completed evidence supplies the +verdict; sealed inputs and earlier failures were not relabelled. + +## Completed Checks + +- Release, dependency, class-version, model, recipe, archive, checksum and production-only Eclipse audits for all three mods. +- Power's 357 unit tests and fresh Eclipse compiler run; Steam's 276 tests and Eclipse run; Electric's 117 unit tests, 28 native checks and Eclipse run. No test failures or skips in these inventories. +- Independent empty Gradle/native SDK caches and paths containing spaces, with matching player, source and Javadoc archives. The independent SDK's complete entry payloads match; ZIP container headers are not treated as game-code differences. +- The 389-test Java harness, 265 current Python adapter tests and 24 worldless client-probe tests, kept separate from actual gameplay and GPU evidence. +- All 21 walkable works bays and 12 drill witnesses, capability/API automation, finite machine jobs, harvesting and stopped-save comparisons after full restarts. +- Physical chunk unloading, partially unloaded networks, bridge reconnection and extended moving-drill scenarios. +- Recipe discovery and survival manufacturing progression in all three modes, with rubber enabled and disabled; drain, pipe, tank and oil-processing lanes across full restarts. +- Fresh generation, legacy imports and retained original-1.10/released-1.10/released-1.12 profiles, including new-chunk sampling and saved user settings. +- Nine operating machine-upgrade routes: original Cyano 1.10, released base* 1.10 and 1.12, retained 1.19.4, 1.20.1, 1.20.6, 1.21.1 and 1.21.11, plus the newly promoted 26.1.2 kit. No rebuilding or replacing the imported machines was needed. +- Nine player/entity routes including Nether, End and chained conversions, with source immutability and stopped-data comparisons. Both actual migration-hook orders were tested separately for machines and entities; instrumented ordering proofs are not substituted for unmodified player-jar campaigns. +- A real graphical client: 39 screens, 351 caption checks, 26 tank-gauge cases, three window/GUI scales and 1,247 fenced GPU frames. Recipe-book groups, world oil rendering, resource reloads and deliberate negative controls were exercised. +- Two genuine 30-minute endurance worlds, one fresh and one upgraded from original 1.10. Both saved, stopped completely, restarted and resumed processing. Independent replay validated all 66 checkpoints, conservation, interval clocks and final raw saved state. Minimum measured interval throughput remained above 19 ticks per second. + +## Findings And Limits + +No additional machine-balance repair was needed during these campaigns. +The port translates the current native GUI/rendering interfaces and adds the +new sulfur-caves biome to saved-registry validation. The test harness uses +the new GPU submission contract before reading its fenced frames. + +Six dimension fixtures retain `PASS_WITH_NATIVE_DIFFERENCE`. Their strict +comparisons still record Minecraft's own tamed-wolf health/max-health scaling. +Independent controls reproduce exactly that difference without Advantage; +ownership, UUIDs and all other compared fields must still agree. + +This Windows host has unavailable Perflib process counters. Independent +worldless native controls reproduce the exact OSHI diagnostics and Netty +Java-25 capability fallbacks. Only reviewed complete log sequences are +classified; changed levels, threads, frames and unrelated exceptions still +fail. No log category is suppressed and the host has not been repaired. + +Three Mineralogy-only oil assertions are explicit skips because that optional +mod is absent. This test kit is not qualification of an entire modpack or of +unrelated mods' migration. Automated child-model geometry does not replace +the remaining in-world turbine orientation/spin check. + +## Manual Checks + +Use Java 25 and the generated `runClientAllAdvantage.launch` in the existing +Power project. Both local dependency patches and the matching child jars must +remain installed. Refresh the Gradle project, let Eclipse finish building, +then check the shared creative tab, normal screens, recipe discovery, world +models, turbines, weapons and the ordinary play experience. + +Back up real worlds and test disposable copies only. Check other installed +mods before attempting an upgrade. Do not reopen a converted world on an +older Minecraft version. + +## Local Evidence + +Original logs, command records, native controls, frozen jars, checkpoints and +client frames are retained under +`D:/PowerAdvantage/_verification/base-262-port-20261112/` and +`test-rig/build/native262-current03/`. Player/entity and ordering results are +under `children/actors-orders-profile03-16/` and its referenced rig outputs. + +The genuine endurance input seal is `endurance-prerun-seal-03.json`. +`endurance-independent-replay-03.json` records the stopped, separately frozen +replay and confirms that the seal predates setup with no custody gap. +Failed setup, SDK-transition and native-platform diagnostic attempts remain +separate from the final passing runs. + +See [the rig instructions](../../test-rig/NATIVE262.md) to repeat the checks. +Manual acceptance, public dependency review, push/merge, tags and publication +remain separate steps. diff --git a/test-rig/NATIVE262.md b/test-rig/NATIVE262.md new file mode 100644 index 00000000..4de5a994 --- /dev/null +++ b/test-rig/NATIVE262.md @@ -0,0 +1,89 @@ +# Advantage Works On Minecraft 26.2 + +This is the local Forge 65 port of the existing rig. Station IDs, expected +machine behaviour, conservation checks and stopped-save comparisons are kept +from the previous version. Base Metals and OreSpawn are required inputs. +There is no supported without-Base-Metals profile. + +## Inputs + +Use Temurin 25.0.3+9, Forge 65.1.0 and the exact six jars in +`native262-local-candidate-03.json`. The profile SHA-256 is +`A98D4F9B54A0AE47392D9A3CF81335F5E228F718C7D322DF9838A94AD27D9736`. +Its original pre-run `EXPLORATORY_UNQUALIFIED` label remains unchanged; final +qualification is a separate receipt, not an edit to a sealed input. + +The local evidence directory is +`D:/PowerAdvantage/_verification/base-262-port-20261112/`. It contains the +frozen jars, code-only server seed, code inventory, command records, negative +controls and original failures. Generated worlds and reports are not tracked. + +The player versions are basePower 2.4.17.2602001 and baseSteam/baseElectric +2.2.18.2602001. Base Metals 3.1.2.2602001 and OreSpawn 4.1.1.2602001 are +local fixes, not public releases. Do not replace them with older downloads. + +## Build And Run + +Build Power, the two expansions, then `test-rig` using Java 25. The rig's +`check build` tasks verify that no harness content enters the player jar. +Clear inherited `JAVA_TOOL_OPTIONS`, `JDK_JAVA_OPTIONS`, `_JAVA_OPTIONS` and +`CLASSPATH` for qualification. Use ordinary frozen copies of the jars, not +mutable files in another build's output directory. + +Drivers live in `test-rig/migration`. Each accepts `--help`; use `native262` +drivers only for this target. Common inputs are the sealed profile, code-only +server seed, code inventory with its SHA-256, Java executable, a new output +directory and an unused port. Existing result directories must not be reused. + +| Driver | Purpose | +| --- | --- | +| `native262_broad.py` | Walkable bays and drill witnesses | +| `native262_interop.py` | Capabilities, automation and API transfer | +| `native262_operations.py --group all` | Finite measured machine jobs | +| `native262_machines.py` | Harvesting and chunk lifecycle | +| `native262_networks.py` | Partial unloading, bridges and extended drills | +| `native262_resources.py` | Recipe modes and manufacturing progression | +| `native262_oil.py` | World fluids, drains, pipes, tanks and restarts | +| `native262_worldgen.py` | Fresh provider generation | +| `native262_retained_worldgen.py` | Imported generation settings and new chunks | +| `native262_strict_migration.py` | Operating old-world conversions | +| `native262_actors.py` | Player, entity and dimension state | +| `native262_transformer_order.py` | Actual machine migration-hook order | +| `native262_actor_transformer_order.py` | Actual entity migration-hook order | +| `native262_client.py` | Native GUI, oil rendering and recipe-book frames | +| `native262_endurance.py` | Fresh and retained worlds under measured load | + +The test-world marker, station bounds and operator checks still apply. Do not +point a driver at a real player's save. Source fixtures must be hash-checked +before and after every disposable migration. + +## Endurance Evidence + +Create the `native262_endurance_replay.py seal` before starting either world. +It binds the jars, Java, specification, server inventory, retained source, +native platform proof and transitive oracle files. Run for 1,800 real seconds, +then save, shut down completely, restart and check resumed processing. + +After both worlds stop, use the replay driver's `freeze` and `replay` commands +against new paths. Qualification requires an intact pre-run seal, all 66 +checkpoints, per-interval throughput and conservation checks, and agreement +with the final raw region data. A retrospective seal is explicitly insufficient. +The current independent result is `endurance-independent-replay-03.json`. + +## Native Controls And Remaining Scope + +Native 26.2 changes tamed wolves' health through vanilla data fixing and loading. +Six dimension fixtures therefore retain `PASS_WITH_NATIVE_DIFFERENCE`, with +their original strict failures and independent no-Advantage control retained. +All other player/entity fields must still match. + +Windows' missing Perflib counters also affect this machine's native OSHI +statistics. Worldless native controls reproduce the exact diagnostics. +The server and client classifiers accept only the reviewed whole sequences; +unrelated errors, changed frames, levels or threads still fail. The host has +not been repaired, and no log category is suppressed. + +Mineralogy-only oil assertions remain explicit skips when that optional mod is +absent. Geometry assertions do not replace manually checking the turbine's +orientation and spin in a real world. See the family qualification record for +the completed checks and the manual-testing handoff. diff --git a/test-rig/README.md b/test-rig/README.md index eb3045c2..d2154a51 100644 --- a/test-rig/README.md +++ b/test-rig/README.md @@ -1,5 +1,17 @@ # Advantage Works Test Rig +## Current Target: Minecraft 26.2 + +Use the [26.2 instructions](NATIVE262.md) and the +[family qualification record](../docs/26.2/QUALIFICATION.md) for this branch. +The packaged machine, migration, graphical and two-world endurance campaigns +have passed against frozen profile03. Final build checks and manual acceptance +are recorded separately; do not substitute an older version's passing receipt. + +The following 26.1.2 section is retained history, including its original +pending labels and failures. It is not the current 26.2 status or a command +template for a 26.2 launch. + ## Native 26.1.2: Local And Unqualified > **Current-instruction boundary:** Only this native26 section describes the diff --git a/test-rig/build.gradle b/test-rig/build.gradle index 664313f9..e8e6b242 100644 --- a/test-rig/build.gradle +++ b/test-rig/build.gradle @@ -5,7 +5,7 @@ plugins { id 'net.minecraftforge.gradle' version '7.0.34' } -ext.forge64MavenizerDefault = '../../../../MinecraftMineralogy/OS2612/OreSpawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar' +ext.forge64MavenizerDefault = '../../../../MinecraftMineralogy/OS262/OreSpawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar' apply from: '../gradle/forge64-mavenizer.gradle' // Saves, pinned inputs and evidence are siblings, never Gradle cleanup targets. @@ -70,7 +70,7 @@ repositories { } def powerCompileJar = file(project.findProperty('powerAdvantageReleaseJar') - ?: '../build/libs/basePower-2.4.16.2601021.jar') + ?: '../build/libs/basePower-2.4.17.2602001.jar') def exploratoryPowerSha = providers.gradleProperty('worksExploratoryPowerSha256') def powerCompileSha = exploratoryPowerSha.orElse(providers.gradleProperty('works_power_sha256')) @@ -97,6 +97,7 @@ def unitBootstrapJar = tasks.register('unitBootstrapJar', Jar) { manifest.attributes('Premain-Class': 'zone.moddev.mc.advantageworks.unit.ForgeCapabilityBootstrap') } tasks.withType(Test).configureEach { + maxHeapSize = '2G' dependsOn unitBootstrapJar doFirst { jvmArgs "-javaagent:${unitBootstrapJar.get().archiveFile.get().asFile}" } } @@ -107,7 +108,7 @@ tasks.register('verifyWorksCompileInputPin') { inputs.file(powerCompileJar) inputs.property('powerSha256', powerCompileSha.orElse('')) doLast { - if (!powerCompileJar.isFile()) throw new GradleException("Missing 26.1.2 Power compile input: ${powerCompileJar}") + if (!powerCompileJar.isFile()) throw new GradleException("Missing 26.2 Power compile input: ${powerCompileJar}") if (!(powerCompileSha.getOrElse('') ==~ /[A-Fa-f0-9]{64}/)) { throw new GradleException('Missing or invalid Power SHA-256 pin') } @@ -259,7 +260,7 @@ tasks.register('verifyWorksNative12111Inputs', JavaExec) { tasks.register('verifyWorksNative2612Inputs', JavaExec) { group = 'verification' - description = 'Checks actual 26.1.2 candidate/Java/seed identities without opening a world.' + description = 'Checks actual 26.2 candidate/Java/seed identities without opening a world.' dependsOn tasks.named('runnerClasses'), tasks.named('jar') classpath = sourceSets.runner.runtimeClasspath mainClass = 'zone.moddev.mc.advantageworks.runner.WorksRunner' @@ -327,10 +328,25 @@ tasks.register('verifyNativeWolfPlatform', JavaExec) { def output = providers.gradleProperty('nativeControlOutput').orNull if (!output || file(output).exists()) throw new GradleException('Supply a new -PnativeControlOutput directory') args file(output).absolutePath + environment 'CLASSPATH', '' jvmArgs "-javaagent:${unitBootstrapJar.get().archiveFile.get().asFile}" } } +tasks.register('verifyNativeSystemReportPlatform', JavaExec) { + group = 'verification' + dependsOn compilePlatformControls + classpath = files(compilePlatformControls.flatMap { it.destinationDirectory }) + nativeControlClasspath + mainClass = 'NativeSystemReportPlatformControl' + javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) } + doFirst { + def output = providers.gradleProperty('nativeControlOutput').orNull + if (!output || file(output).exists()) throw new GradleException('Supply a new -PnativeControlOutput directory') + args file(output).absolutePath + environment 'CLASSPATH', '' + } +} + tasks.register('verifyNativeLeashKnotPlatform', JavaExec) { group = 'verification' dependsOn compilePlatformControls, unitBootstrapJar diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ChildRendererGeometryProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ChildRendererGeometryProbe.java index 91018e81..aa59ab0f 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ChildRendererGeometryProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ChildRendererGeometryProbe.java @@ -2,12 +2,12 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.mojang.blaze3d.PrimitiveTopology; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; import cyano.poweradvantage.api.PoweredEntity; import net.minecraft.client.Minecraft; import com.mojang.blaze3d.vertex.BufferBuilder; -import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.rendertype.RenderType; import net.minecraft.client.renderer.entity.EntityRenderer; import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; @@ -229,7 +229,7 @@ private static List render(Minecraft minecraft, BlockEntity tile, Strin return buffers.vertices; } - private static final class Capture implements MultiBufferSource { + private static final class Capture { private final Minecraft minecraft; private final Identifier expectedTexture; private final JsonArray batches = new JsonArray(); @@ -243,14 +243,14 @@ private static final class Capture implements MultiBufferSource { row.add("native_buffers", batches); } - @Override public VertexConsumer getBuffer(RenderType type) { - require(type.mode() == com.mojang.blaze3d.vertex.VertexFormat.Mode.QUADS && type.format().equals(DefaultVertexFormat.ENTITY), "Unexpected production renderer vertex format/mode"); + VertexConsumer getBuffer(RenderType type) { + require(type.primitiveTopology() == PrimitiveTopology.QUADS && type.format().equals(DefaultVertexFormat.ENTITY), "Unexpected production renderer vertex format/mode"); BufferBuilder buffer = buffers.get(type); if (buffer == null) { require(buffers.size() < 8, "Renderer requested too many buffers"); var allocation = new com.mojang.blaze3d.vertex.ByteBufferBuilder(4096); storage.add(allocation); - buffer = new BufferBuilder(allocation, type.mode(), type.format()); + buffer = new BufferBuilder(allocation, type.primitiveTopology(), type.format()); buffers.put(type, buffer); } return buffer; @@ -271,10 +271,10 @@ void finish() throws Exception { require(count <= 4096, "Renderer exceeded the bounded geometry fixture"); VertexFormat format = built.drawState().format(); ByteBuffer data = built.vertexBuffer(); - int p = offset(format, VertexFormatElement.POSITION); - int uv = offset(format, VertexFormatElement.UV0); - int n = offset(format, VertexFormatElement.NORMAL); - int c = offset(format, VertexFormatElement.COLOR); + int p = offset(format, DefaultVertexFormat.POSITION_SEMANTIC_NAME); + int uv = offset(format, DefaultVertexFormat.UV0_SEMANTIC_NAME); + int n = offset(format, DefaultVertexFormat.NORMAL_SEMANTIC_NAME); + int c = offset(format, DefaultVertexFormat.COLOR_SEMANTIC_NAME); for (int i = 0; i < count; i++) { int base = i * format.getVertexSize(); float[] vertex = {data.getFloat(base + p), data.getFloat(base + p + 4), data.getFloat(base + p + 8), @@ -370,12 +370,10 @@ private static void set(Object object, String name, Object value) throws Reflect throw new NoSuchFieldException(object.getClass().getName() + "." + name); } - private static int offset(VertexFormat format, VertexFormatElement required) { - for (int i = 0; i < format.getElements().size(); i++) { - VertexFormatElement element = format.getElements().get(i); - if (element.equals(required)) return format.getOffset(i); - } - throw new IllegalStateException("Native vertex format lacks " + required); + private static int offset(VertexFormat format, String required) { + VertexFormatElement element = format.getElement(required); + require(element != null, "Native vertex format lacks " + required); + return element.offset(); } private static boolean different(List a, List b) { 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 bfe83387..e8bd712b 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 @@ -72,7 +72,7 @@ public static void tick(TickEvent.ClientTickEvent.Post event) { failDisconnected(minecraft); if (Boolean.getBoolean("advantageworks.childRendererGeometry") && preLoginGeometry == null && minecraft.level == null && minecraft.player == null && minecraft.getSingleplayerServer() == null - && minecraft.getOverlay() == null && minecraft.getModelManager().getBlockStateModelSet() != null) { + && minecraft.gui.overlay() == null && minecraft.getModelManager().getBlockStateModelSet() != null) { preLoginGeometry = new JsonObject(); inspect("child_geometry", preLoginGeometry, () -> ChildRendererGeometryProbe.verify(minecraft, preLoginGeometry)); JsonObject context = new JsonObject(); @@ -85,7 +85,7 @@ public static void tick(TickEvent.ClientTickEvent.Post event) { } FIXTURE_GATE.tick(minecraft, ticks, lastReadinessState); } catch (RuntimeException | LinkageError | AssertionError failure) { readinessFailure = failure; } - if (readinessFailure == null && (ticks < 5 || ((!componentsBound || minecraft.getOverlay() != null + if (readinessFailure == null && (ticks < 5 || ((!componentsBound || minecraft.gui.overlay() != null || minecraft.getModelManager().getBlockStateModelSet() == null) && ticks < 2400))) return; if (readinessFailure == null && loggedIn && componentsBound && !worldOilComplete) { if (worldOil == null) worldOil = new NativeWorldOilProbe(); @@ -108,8 +108,8 @@ public static void tick(TickEvent.ClientTickEvent.Post event) { if (preLoginGeometry != null) for (var row : preLoginGeometry.entrySet()) result.add(row.getKey(), row.getValue().deepCopy()); if (worldOilReport != null) result.add("world_oil_visual", worldOilReport.deepCopy()); if (readinessFailure != null) throw new IllegalStateException("Native login/fixture gate failed", readinessFailure); - require("26.1.2".equals(result.get("minecraft").getAsString()) - && "64.1.5".equals(result.get("forge").getAsString()) + require("26.2".equals(result.get("minecraft").getAsString()) + && "65.1.0".equals(result.get("forge").getAsString()) && Runtime.version().feature() == 25, "Wrong native client target"); require(loggedIn && componentsBound, "Native real-world login/component binding did not complete"); var expectedWorld = Path.of("saves/AdvantageClientResources").toRealPath(); @@ -148,12 +148,12 @@ public static void tick(TickEvent.ClientTickEvent.Post event) { 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.16.2601021".equals(versions.get("basepower").getAsString()), "Wrong native Power candidate"); + require("2.4.17.2602001".equals(versions.get("basepower").getAsString()), "Wrong native Power candidate"); for (String id : new String[]{"basesteam", "baseelectric"}) { - if (versions.has(id)) require("2.2.17.2601021".equals(versions.get(id).getAsString()), "Wrong native expansion candidate: " + id); + if (versions.has(id)) require("2.2.18.2602001".equals(versions.get(id).getAsString()), "Wrong native expansion candidate: " + id); } require(minecraft.getModelManager().getBlockStateModelSet() != null, "Timed out before model baking"); - require(minecraft.getOverlay() == null, "Timed out before native resource loading completed"); + require(minecraft.gui.overlay() == null, "Timed out before native resource loading completed"); verifyVanillaParticleControl(minecraft, result); boolean primary = inspect("registered_resources", result, () -> { int items = 0; @@ -193,7 +193,7 @@ public static void tick(TickEvent.ClientTickEvent.Post event) { require(items > 0 && states > 0, "Power Advantage content is not registered"); require(!contents.isEmpty(), "Power Advantage creative tab is empty"); require(!tab.getIconItem().isEmpty(), "Power Advantage creative tab has no icon"); - require(I18n.exists("itemGroup.basepower") && tab.getDisplayName().getString().equals(I18n.get("itemGroup.basepower")), + require(net.minecraft.locale.Language.getInstance().has("itemGroup.basepower") && tab.getDisplayName().getString().equals(I18n.get("itemGroup.basepower")), "The shared creative tab title is not translated"); var creativeIds = new java.util.HashSet(); for (ItemStack stack : contents) { @@ -230,7 +230,7 @@ public static void tick(TickEvent.ClientTickEvent.Post event) { && "PASS".equals(worldOilReport.get("status").getAsString()), "One or more native client probe scopes failed; inspect scope_results/world_oil_visual"); result.addProperty("status", "PASS"); - result.addProperty("screen", minecraft.screen == null ? "none" : minecraft.screen.getClass().getName()); + result.addProperty("screen", minecraft.gui.screen() == null ? "none" : minecraft.gui.screen().getClass().getName()); } catch (IOException | RuntimeException | LinkageError | AssertionError failure) { result.addProperty("status", "FAIL"); result.addProperty("error", failure.toString()); @@ -244,20 +244,20 @@ public static void tick(TickEvent.ClientTickEvent.Post event) { } private static void failDisconnected(Minecraft minecraft) { - if (minecraft.screen == null || minecraft.screen.getClass() != net.minecraft.client.gui.screens.DisconnectedScreen.class - || minecraft.getOverlay() != null) return; - if (minecraft.screen != disconnectedScreen) { disconnectedScreen = minecraft.screen; disconnectedTick = ticks; return; } + if (minecraft.gui.screen() == null || minecraft.gui.screen().getClass() != net.minecraft.client.gui.screens.DisconnectedScreen.class + || minecraft.gui.overlay() != null) return; + if (minecraft.gui.screen() != disconnectedScreen) { disconnectedScreen = minecraft.gui.screen(); disconnectedTick = ticks; return; } if (ticks - disconnectedTick < 20) return; try { - lastReadinessState.addProperty("nativeDisconnectNarration", minecraft.screen.getNarrationMessage().getString()); + lastReadinessState.addProperty("nativeDisconnectNarration", minecraft.gui.screen().getNarrationMessage().getString()); captureReadinessScreen(minecraft, lastReadinessState, Path.of("login-readiness/disconnected.png")); Files.writeString(Path.of("login-readiness/disconnected.json"), lastReadinessState.toString(), java.nio.file.StandardOpenOption.CREATE_NEW); } catch (IOException failure) { throw new IllegalStateException("Could not retain native disconnect failure", failure); } - throw new IllegalStateException("Native client disconnected: " + minecraft.screen.getNarrationMessage().getString()); + throw new IllegalStateException("Native client disconnected: " + minecraft.gui.screen().getNarrationMessage().getString()); } private static void captureReadinessScreen(Minecraft minecraft, JsonObject state, Path screenshot) throws IOException { - var image = NativeGuiProjection.read(minecraft.getMainRenderTarget()); + var image = NativeGuiProjection.read(minecraft.gameRenderer.mainRenderTarget()); try (var output = Files.newOutputStream(screenshot, java.nio.file.StandardOpenOption.CREATE_NEW)) { require(javax.imageio.ImageIO.write(image, "png", output), "No diagnostic PNG writer"); } @@ -275,10 +275,10 @@ private static void captureReadinessScreen(Minecraft minecraft, JsonObject state private static void recordReadiness(Minecraft minecraft, boolean loggedIn, boolean componentsBound) { JsonObject state = new JsonObject(); - state.addProperty("screenClass", minecraft.screen == null ? "none" : minecraft.screen.getClass().getName()); - state.addProperty("screenTitle", minecraft.screen == null ? "" : minecraft.screen.getTitle().getString()); - state.addProperty("screenTitleComponent", minecraft.screen == null ? "" : minecraft.screen.getTitle().toString()); - state.addProperty("overlayClass", minecraft.getOverlay() == null ? "none" : minecraft.getOverlay().getClass().getName()); + state.addProperty("screenClass", minecraft.gui.screen() == null ? "none" : minecraft.gui.screen().getClass().getName()); + state.addProperty("screenTitle", minecraft.gui.screen() == null ? "" : minecraft.gui.screen().getTitle().getString()); + state.addProperty("screenTitleComponent", minecraft.gui.screen() == null ? "" : minecraft.gui.screen().getTitle().toString()); + state.addProperty("overlayClass", minecraft.gui.overlay() == null ? "none" : minecraft.gui.overlay().getClass().getName()); state.addProperty("levelPresent", minecraft.level != null); state.addProperty("playerPresent", minecraft.player != null); state.addProperty("integratedServer", minecraft.hasSingleplayerServer()); diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeExperimentalFixtureGate.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeExperimentalFixtureGate.java index 09a244ad..635c3a12 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeExperimentalFixtureGate.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeExperimentalFixtureGate.java @@ -29,8 +29,8 @@ /** One approved native warning action for an authenticated, completed native broad fixture copy. */ final class NativeExperimentalFixtureGate { - static final String PROTOCOL = "NATIVE64_SEALED_EXPERIMENTAL_FIXTURE_SKIP_V2"; - static final String SCOPE = "COMPLETED_NATIVE64_BROAD_PACKAGED_CURRENT_COPY"; + static final String PROTOCOL = "NATIVE65_SEALED_EXPERIMENTAL_FIXTURE_SKIP_V2"; + static final String SCOPE = "COMPLETED_NATIVE65_BROAD_PACKAGED_CURRENT_COPY"; static final String TITLE = "selectWorld.backupQuestion.experimental"; static final String SKIP = "selectWorld.backupJoinSkipButton"; private Screen observed; @@ -52,20 +52,20 @@ static Button select(Class screenClass, Component title, List children) { } void tick(Minecraft minecraft, int tick, JsonObject readiness) { - Screen screen = minecraft.screen; + Screen screen = minecraft.gui.screen(); if (screen == null || screen.getClass() != BackupConfirmScreen.class || !exactTranslation(screen.getTitle(), TITLE)) return; if (used) { require(screen == observed, "Repeated experimental warning after the once-only fixture action"); return; } - if (minecraft.getOverlay() != null) { observed = screen; firstTick = tick; return; } + if (minecraft.gui.overlay() != null) { observed = screen; firstTick = tick; return; } if (screen != observed) { observed = screen; firstTick = tick; return; } if (tick - firstTick < 20) return; try { Path folder = Path.of("login-readiness"); Files.createDirectories(folder); Path screenshot = folder.resolve("experimental-before-action.png"); - var image = NativeGuiProjection.read(minecraft.getMainRenderTarget()); + var image = NativeGuiProjection.read(minecraft.gameRenderer.mainRenderTarget()); try (var output = Files.newOutputStream(screenshot, StandardOpenOption.CREATE_NEW)) { require(ImageIO.write(image, "png", output), "Missing native warning PNG writer"); } @@ -86,7 +86,7 @@ void tick(Minecraft minecraft, int tick, JsonObject readiness) { Files.writeString(folder.resolve("experimental-screen.json"), action.toString(), StandardOpenOption.CREATE_NEW); Button skip = select(screen.getClass(), screen.getTitle(), screen.children()); require(minecraft.level == null && minecraft.player == null && minecraft.getConnection() == null - && minecraft.getSingleplayerServer() == null && !minecraft.hasSingleplayerServer() && minecraft.getOverlay() == null, + && minecraft.getSingleplayerServer() == null && !minecraft.hasSingleplayerServer() && minecraft.gui.overlay() == null, "Experimental fixture action is only allowed before native server/login"); JsonObject policy = verifyPolicy(Path.of("")); action.addProperty("status", "RECORDED_BEFORE_NATIVE_ACTION"); @@ -131,9 +131,9 @@ static JsonObject verifyPolicy(Path directory) throws IOException { Path sourceReceipt = Path.of(input.get("sourceWorldReceipt").getAsString()); require(sha(sourceReceipt).equals(policy.get("sourceReceiptSha256").getAsString()), "Unapproved source receipt"); JsonObject receipt = read(sourceReceipt), profileData = read(profile); - require("26.1.2".equals(profileData.get("minecraftVersion").getAsString()) - && "26.1.2-64.1.5".equals(profileData.getAsJsonObject("forge").get("version").getAsString()) - && "26.1.2".equals(receipt.get("minecraft").getAsString()) && "64.1.5".equals(receipt.get("forge").getAsString()) + require("26.2".equals(profileData.get("minecraftVersion").getAsString()) + && "26.2-65.1.0".equals(profileData.getAsJsonObject("forge").get("version").getAsString()) + && "26.2".equals(receipt.get("minecraft").getAsString()) && "65.1.0".equals(receipt.get("forge").getAsString()) && "PASS".equals(receipt.get("status").getAsString()) && receipt.get("inputsUnchanged").getAsBoolean() && receipt.get("seedUnchanged").getAsBoolean() && profileSha.equals(receipt.get("profileSha256").getAsString()) && "PACKAGED_WALKABLE_WORKS_AND_TWELVE_DRILL_WITNESSES".equals(receipt.get("scope").getAsString()), "Source receipt is not an eligible completed native broad run"); @@ -151,8 +151,8 @@ static JsonObject verifyPolicy(Path directory) throws IOException { require(levelSha.matches("[0-9A-F]{64}") && levelSha.equals(sha(source.resolve("level.dat"))), "Unapproved source/copied level bytes"); // Pinned BM6786 removes only this empty obsolete Forge index during getUnfixedDataTag. CompoundTag sourceLevel = NbtIo.readCompressed(source.resolve("level.dat"), NbtAccounter.unlimitedHeap()); - require(sourceLevel.getCompound("Data").orElseThrow().getInt("DataVersion").orElseThrow() == 4790 - && "26.1.2".equals(sourceLevel.getCompound("Data").orElseThrow().getCompound("Version").orElseThrow().getString("Name").orElseThrow()), "Not a stopped native world"); + require(sourceLevel.getCompound("Data").orElseThrow().getInt("DataVersion").orElseThrow() == 4903 + && "26.2".equals(sourceLevel.getCompound("Data").orElseThrow().getCompound("Version").orElseThrow().getString("Name").orElseThrow()), "Not a stopped native world"); String copyTransform = verifyCopiedLevel(sourceLevel, NbtIo.readCompressed(copy.resolve("level.dat"), NbtAccounter.unlimitedHeap())); JsonObject copyCustody = new JsonObject(); diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeGuiEvidence.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeGuiEvidence.java index 18ef3023..2bfa493f 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeGuiEvidence.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeGuiEvidence.java @@ -56,12 +56,12 @@ static final class State extends GuiRenderState { @Override public void addText(GuiTextRenderState text) { JsonObject row = new JsonObject(); StringBuilder value = new StringBuilder(); - text.text.accept((index, style, codepoint) -> { value.appendCodePoint(codepoint); return true; }); + field(text, "text", net.minecraft.util.FormattedCharSequence.class).accept((index, style, codepoint) -> { value.appendCodePoint(codepoint); return true; }); row.addProperty("text", value.toString()); - row.addProperty("x", text.x); - row.addProperty("y", text.y); - row.addProperty("color", text.color); - row.addProperty("dropShadow", text.dropShadow); + row.addProperty("x", field(text, "x", Integer.class)); + row.addProperty("y", field(text, "y", Integer.class)); + row.addProperty("color", field(text, "color", Integer.class)); + row.addProperty("dropShadow", field(text, "dropShadow", Boolean.class)); row.add("pose", matrix(text.pose)); texts.add(row); super.addText(text); @@ -109,6 +109,16 @@ JsonObject extracted() { } } + private static T field(GuiTextRenderState state, String name, Class type) { + try { + var field = GuiTextRenderState.class.getDeclaredField(name); + field.setAccessible(true); + return type.cast(field.get(state)); + } catch (ReflectiveOperationException failure) { + throw new IllegalStateException("Cannot inspect native GUI text field " + name, failure); + } + } + private static JsonArray matrix(org.joml.Matrix3x2fc pose) { JsonArray result = new JsonArray(); result.add(pose.m00()); result.add(pose.m01()); result.add(pose.m10()); @@ -132,7 +142,7 @@ static void retain(BufferedImage image, JsonObject extraction) { output.write(color & 255); output.write(color >>> 24 & 255); } } - extraction.addProperty("protocol", "NATIVE64_GUI_FRAMEBUFFER_FENCED_V1"); + extraction.addProperty("protocol", "NATIVE65_GUI_FRAMEBUFFER_FENCED_V1"); extraction.addProperty("width", image.getWidth()); extraction.addProperty("height", image.getHeight()); Files.writeString(nativeState, extraction.toString(), StandardOpenOption.CREATE_NEW); 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 2ec96405..1591ce24 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 @@ -17,7 +17,6 @@ import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.render.GuiRenderer; import net.minecraft.client.renderer.state.gui.GuiRenderState; -import net.minecraft.client.renderer.fog.FogRenderer; import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.client.renderer.state.WindowRenderState; @@ -36,23 +35,23 @@ final class NativeGuiProjection implements AutoCloseable { private NativeGuiProjection(Minecraft minecraft, int width, int height, int scale) throws ReflectiveOperationException { this.minecraft = minecraft; - original = minecraft.getMainRenderTarget(); + original = minecraft.gameRenderer.mainRenderTarget(); var window = minecraft.getWindow(); this.width = window.getWidth(); this.height = window.getHeight(); this.scale = window.getGuiScale(); - windowState = minecraft.gameRenderer.getGameRenderState().windowRenderState; + windowState = minecraft.gameRenderer.gameRenderState().windowRenderState; stateWidth = windowState.width; stateHeight = windowState.height; stateScale = windowState.guiScale; - targetField = Minecraft.class.getDeclaredField("mainRenderTarget"); + targetField = net.minecraft.client.renderer.GameRenderer.class.getDeclaredField("mainRenderTarget"); targetField.setAccessible(true); widthField = window.getClass().getDeclaredField("framebufferWidth"); heightField = window.getClass().getDeclaredField("framebufferHeight"); widthField.setAccessible(true); heightField.setAccessible(true); - target = new TextureTarget("Advantage native GUI witness", width, height, true); - targetField.set(minecraft, target); + target = new TextureTarget("Advantage native GUI witness", width, height, true, original.getColorTexture().getFormat()); + targetField.set(minecraft.gameRenderer, target); widthField.setInt(window, width); heightField.setInt(window, height); window.setGuiScale(scale); @@ -85,7 +84,7 @@ static com.google.gson.JsonObject calibrate(Minecraft minecraft) { cases.add(row); } com.google.gson.JsonObject result = new com.google.gson.JsonObject(); - result.addProperty("protocol", "NATIVE64_GUI_PROJECTION_PIXEL_RULER_V1"); + result.addProperty("protocol", "NATIVE65_GUI_PROJECTION_PIXEL_RULER_V1"); result.addProperty("status", "PASS"); result.add("cases", cases); return result; @@ -119,18 +118,15 @@ static BufferedImage render(Minecraft minecraft, int width, int height, int scal Function graphicsFactory, Consumer draw) { NativeGuiEvidence.State state = new NativeGuiEvidence.State(); try (NativeGuiProjection scope = new NativeGuiProjection(minecraft, width, height, scale); - GuiRenderer renderer = new GuiRenderer(state, minecraft.renderBuffers().bufferSource(), - minecraft.gameRenderer.getSubmitNodeStorage(), - minecraft.gameRenderer.getFeatureRenderDispatcher(), List.of())) { + GuiRenderer renderer = new GuiRenderer(state, minecraft.gameRenderer.featureRenderDispatcher(), List.of())) { RenderSystem.getDevice().createCommandEncoder().clearColorAndDepthTextures( - scope.target.getColorTexture(), background, scope.target.getDepthTexture(), 1.0); + scope.target.getColorTexture(), new org.joml.Vector4f((background >>> 16 & 255) / 255F, + (background >>> 8 & 255) / 255F, (background & 255) / 255F, (background >>> 24 & 255) / 255F), + scope.target.getDepthTexture(), RenderSystem.DEFAULT_DEPTH_CLEAR_VALUE); GuiGraphicsExtractor graphics = graphicsFactory.apply(state); draw.accept(graphics); graphics.extractDeferredElements(-100, -100, 0F); - Field fogField = minecraft.gameRenderer.getClass().getDeclaredField("fogRenderer"); - fogField.setAccessible(true); - FogRenderer fog = (FogRenderer) fogField.get(minecraft.gameRenderer); - renderer.render(fog.getBuffer(FogRenderer.FogMode.NONE)); + renderer.render(); BufferedImage image = read(scope.target); var extraction = state.extracted(); extraction.addProperty("extractor", graphics.getClass().getName()); @@ -148,7 +144,9 @@ static BufferedImage render(Minecraft minecraft, int width, int height, int scal static BufferedImage read(RenderTarget target) { AtomicReference captured = new AtomicReference<>(); Screenshot.takeScreenshot(target, captured::set); - try (var fence = RenderSystem.getDevice().createCommandEncoder().createFence()) { + var encoder = RenderSystem.getDevice().createCommandEncoder(); + try (var fence = encoder.createFence()) { + encoder.submit(); if (!fence.awaitCompletion(10_000_000_000L)) throw new IllegalStateException("Native GUI readback fence timed out"); RenderSystem.executePendingTasks(); NativeImage image = captured.get(); @@ -164,7 +162,7 @@ static BufferedImage read(RenderTarget target) { @Override public void close() throws ReflectiveOperationException { try { - targetField.set(minecraft, original); + targetField.set(minecraft.gameRenderer, original); widthField.setInt(minecraft.getWindow(), width); heightField.setInt(minecraft.getWindow(), height); minecraft.getWindow().setGuiScale(scale); @@ -173,8 +171,6 @@ static BufferedImage read(RenderTarget target) { windowState.guiScale = stateScale; RenderSystem.setProjectionMatrix(projection, projectionType); } finally { - minecraft.gameRenderer.getSubmitNodeStorage().endFrame(); - minecraft.gameRenderer.getFeatureRenderDispatcher().endFrame(); target.destroyBuffers(); } } diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeVanillaResourceProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeVanillaResourceProbe.java index 0f8fd673..747cfee8 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeVanillaResourceProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeVanillaResourceProbe.java @@ -39,7 +39,7 @@ public CompletableFuture reload(SharedState reload, Executor prepare, private static JsonObject capture(ResourceManager resources) { JsonObject result = new JsonObject(); - result.addProperty("protocol", "NATIVE64_VANILLA_RESOURCE_ROOTS_V1"); + result.addProperty("protocol", "NATIVE65_VANILLA_RESOURCE_ROOTS_V1"); result.addProperty("phase", "reload-preparation-before-atlas-upload"); JsonArray rows = new JsonArray(); result.add("resources", rows); diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeWorldOilProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeWorldOilProbe.java index ad98a707..04d62d95 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeWorldOilProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/NativeWorldOilProbe.java @@ -52,10 +52,10 @@ JsonObject tick(Minecraft minecraft, int tick) { require(minecraft.getSingleplayerServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toRealPath() .equals(Path.of("saves/AdvantageClientResources").toRealPath()), "Oil scene cannot write outside the disposable copy"); if (phase.equals("NEW")) { - report.addProperty("protocol", "NATIVE64_LEVEL_RENDERER_OIL_RELOAD_V1"); + report.addProperty("protocol", "NATIVE65_LEVEL_RENDERER_OIL_RELOAD_V1"); report.addProperty("status", "RUNNING"); report.add("frames", frames); report.addProperty("scope", "ACTUAL_LEVEL_RENDERER_NOT_GUI_OR_SYNTHETIC_WORLD"); - originalHideGui = minecraft.options.hideGui; minecraft.options.hideGui = true; + originalHideGui = minecraft.gui.hud.isHidden(); setHudHidden(minecraft, true); require(minecraft.options.mipmapLevels().get() == 4, "World oil proof requires default mipmap level4"); report.addProperty("mipmapLevel", 4); serverWork = scene(minecraft, "oil", true); @@ -85,7 +85,7 @@ JsonObject tick(Minecraft minecraft, int tick) { report.add("nativeGeometryAfterReload", geometry); JsonArray controls = compareControls(); report.add("pixelControls", controls); report.addProperty("status", "PASS"); phase = "DONE"; - minecraft.options.hideGui = originalHideGui; + setHudHidden(minecraft, originalHideGui); Files.writeString(Path.of("world-oil-evidence/result.json"), report.toString(), StandardOpenOption.CREATE_NEW); return report(); } @@ -93,7 +93,7 @@ JsonObject tick(Minecraft minecraft, int tick) { } return null; } catch (IOException | RuntimeException | LinkageError | AssertionError failure) { - minecraft.options.hideGui = originalHideGui; + setHudHidden(minecraft, originalHideGui); report.addProperty("status", "FAIL"); report.addProperty("phase", phase); report.addProperty("error", failure.toString()); throw new IllegalStateException("Native world oil proof failed in " + phase, failure); } @@ -108,8 +108,8 @@ private CompletableFuture scene(Minecraft minecraft, String mode, boolean require(level != null, "Missing actual Overworld"); if (initialize) { for (int x = 502; x <= 521; x++) for (int z = 508; z <= 515; z++) { - level.setBlock(new BlockPos(x, Y - 1, z), Blocks.WHITE_CONCRETE.defaultBlockState(), 3); - level.setBlock(new BlockPos(x, Y, z), Blocks.WHITE_CONCRETE.defaultBlockState(), 3); + level.setBlock(new BlockPos(x, Y - 1, z), Blocks.CONCRETE.white().defaultBlockState(), 3); + level.setBlock(new BlockPos(x, Y, z), Blocks.CONCRETE.white().defaultBlockState(), 3); } server.getCommands().performPrefixedCommand(server.createCommandSourceStack(), "time set noon"); server.getCommands().performPrefixedCommand(server.createCommandSourceStack(), "weather clear"); @@ -130,14 +130,14 @@ private CompletableFuture scene(Minecraft minecraft, String mode, boolean } private boolean ready(Minecraft minecraft, String mode) { - if (minecraft.getOverlay() != null || minecraft.screen != null || !minecraft.level.dimension().equals(Level.OVERWORLD)) return false; + if (minecraft.gui.overlay() != null || minecraft.gui.screen() != null || !minecraft.level.dimension().equals(Level.OVERWORLD)) return false; if (minecraft.player.position().distanceToSqr(512.5, 14, 527) > 0.0001) return false; for (int pool = 0; pool < 3; pool++) for (int x = 0; x < 4; x++) for (int z = 0; z < 4; z++) { BlockPos pos = new BlockPos(X[pool] + x, Y, Z + z); Fluid expected = pool == 2 || mode.equals("water") ? Fluids.WATER : mode.equals("empty") ? Fluids.EMPTY : OILS[pool]; if (minecraft.level.getFluidState(pos).getType() != expected || !minecraft.levelRenderer.isSectionCompiledAndVisible(pos)) return false; } - return minecraft.levelRenderer.countRenderedSections() > 0; + return !minecraft.levelRenderer.visibleSections().isEmpty(); } private JsonArray models(Minecraft minecraft, boolean before) { @@ -168,7 +168,7 @@ private JsonArray models(Minecraft minecraft, boolean before) { } private void capture(Minecraft minecraft, String name) throws IOException { - BufferedImage image = NativeGuiProjection.read(minecraft.getMainRenderTarget()); + BufferedImage image = NativeGuiProjection.read(minecraft.gameRenderer.mainRenderTarget()); Path folder = Path.of("world-oil-evidence"); Files.createDirectories(folder); Path png = folder.resolve(name + ".png"), raw = folder.resolve(name + ".rgba"); try (var output = Files.newOutputStream(png, StandardOpenOption.CREATE_NEW)) { require(ImageIO.write(image, "png", output), "No world PNG encoder"); } @@ -182,12 +182,12 @@ private void capture(Minecraft minecraft, String name) throws IOException { frame.add("png", artifact(png)); frame.add("rgba", artifact(raw)); frame.addProperty("fenceCompleted", true); frame.addProperty("renderer", minecraft.levelRenderer.getClass().getName()); frame.addProperty("dimension", minecraft.level.dimension().identifier().toString()); - frame.addProperty("hudHidden", minecraft.options.hideGui); - frame.addProperty("renderedSections", minecraft.levelRenderer.countRenderedSections()); - JsonArray camera = new JsonArray(); Vec3 position = minecraft.gameRenderer.getMainCamera().position(); + frame.addProperty("hudHidden", minecraft.gui.hud.isHidden()); + frame.addProperty("renderedSections", minecraft.levelRenderer.visibleSections().size()); + JsonArray camera = new JsonArray(); Vec3 position = minecraft.gameRenderer.mainCamera().position(); camera.add(position.x); camera.add(position.y); camera.add(position.z); frame.add("camera", camera); JsonArray matrix = new JsonArray(); float[] values = new float[16]; - minecraft.gameRenderer.getMainCamera().getViewRotationProjectionMatrix(new org.joml.Matrix4f()).get(values); + minecraft.gameRenderer.mainCamera().getViewRotationProjectionMatrix(new org.joml.Matrix4f()).get(values); for (float value : values) matrix.add(value); frame.add("viewRotationProjection", matrix); JsonArray pools = new JsonArray(); for (int pool = 0; pool < 3; pool++) { @@ -236,5 +236,10 @@ private static JsonObject artifact(Path path) throws IOException { catch (NoSuchAlgorithmException failure) { throw new IllegalStateException(failure); } return row; } + private static void setHudHidden(Minecraft minecraft, boolean hidden) { + if (minecraft.gui.hud.isHidden() != hidden) minecraft.gui.hud.toggle(); + require(minecraft.gui.hud.isHidden() == hidden, "Native HUD did not change visibility"); + } + private static void require(boolean value, String message) { if (!value) throw new IllegalStateException(message); } } 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 255d6391..b04962aa 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 @@ -100,7 +100,7 @@ private static boolean check(Minecraft minecraft, FluidState state, String textu } }; try (var storage = new com.mojang.blaze3d.vertex.ByteBufferBuilder(4096)) { - BufferBuilder buffer = new BufferBuilder(storage, com.mojang.blaze3d.vertex.VertexFormat.Mode.QUADS, DefaultVertexFormat.BLOCK); + BufferBuilder buffer = new BufferBuilder(storage, com.mojang.blaze3d.PrimitiveTopology.QUADS, DefaultVertexFormat.BLOCK); var models = minecraft.getModelManager().getFluidStateModelSet(); var nativeModel = net.minecraftforge.client.extensions.common.IClientFluidTypeExtensions.of(state) .getModel(state, world, BlockPos.ZERO, models.get(state)); @@ -119,10 +119,10 @@ private static boolean check(Minecraft minecraft, FluidState state, String textu .getSprite(Identifier.parse(texture)); require(vertices >= 4, "Native fluid renderer emitted no top face for " + state); ByteBuffer data = built.vertexBuffer(); - int uvOffset = elementOffset(VertexFormatElement.UV0); + int uvOffset = elementOffset(DefaultVertexFormat.UV0_SEMANTIC_NAME); float u = data.getFloat(uvOffset); float v = data.getFloat(uvOffset + 4); - int colorOffset = elementOffset(VertexFormatElement.COLOR); + int colorOffset = elementOffset(DefaultVertexFormat.COLOR_SEMANTIC_NAME); int rgb = (data.get(colorOffset) & 255) << 16 | (data.get(colorOffset + 1) & 255) << 8 | data.get(colorOffset + 2) & 255; boolean correctSprite = !"missingno".equals(sprite.contents().name().getPath()) && u >= sprite.getU0() && u <= sprite.getU1() && v >= sprite.getV0() && v <= sprite.getV1(); @@ -140,12 +140,10 @@ private static boolean check(Minecraft minecraft, FluidState state, String textu } } - private static int elementOffset(VertexFormatElement required) { - for (int i = 0; i < DefaultVertexFormat.BLOCK.getElements().size(); i++) { - VertexFormatElement element = DefaultVertexFormat.BLOCK.getElements().get(i); - if (element.equals(required)) return DefaultVertexFormat.BLOCK.getOffset(i); - } - throw new IllegalStateException("Native block vertex format lacks " + required); + private static int elementOffset(String required) { + VertexFormatElement element = DefaultVertexFormat.BLOCK.getElement(required); + if (element == null) throw new IllegalStateException("Native block vertex format lacks " + required); + return element.offset(); } private static BlockState blockAt(FluidState state, boolean flowing, BlockPos pos) { 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 df387d0e..569c046a 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 @@ -60,17 +60,17 @@ static void verify(Minecraft minecraft, JsonObject report) { try (Stream files = Files.list(root)) { Map registry = RegistryManager.ACTIVE.takeSnapshot(false); CompoundTag level = NbtIo.readCompressed(root.resolve("server-registries.dat"), NbtAccounter.unlimitedHeap()); - require("26.1.2".equals(compound(compound(level, "Data"), "Version").getString("Name").orElseThrow()) - && compound(level, "Data").getInt("DataVersion").orElseThrow() == 4790, - "Recipe fixture is not a native 26.1.2 server export"); + require("26.2".equals(compound(compound(level, "Data"), "Version").getString("Name").orElseThrow()) + && compound(level, "Data").getInt("DataVersion").orElseThrow() == 4903, + "Recipe fixture is not a native 26.2 server export"); boolean nativeForge = false; for (var entry : compound(level, "fml").getList("LoadingModList").orElseThrow()) { require(entry instanceof CompoundTag, "Malformed native loading-mod entry"); CompoundTag mod = (CompoundTag) entry; if ("forge".equals(mod.getString("ModId").orElseThrow())) - nativeForge = "64.1.5".equals(mod.getString("ModVersion").orElseThrow()); + nativeForge = "65.1.0".equals(mod.getString("ModVersion").orElseThrow()); } - require(nativeForge, "Recipe fixture was not saved by Forge 64.1.5"); + require(nativeForge, "Recipe fixture was not saved by Forge 65.1.0"); CompoundTag saved = compound(compound(level, "fml"), "Registries"); for (String key : List.of(ForgeRegistries.ITEMS.getRegistryName().toString(), ForgeRegistries.BLOCKS.getRegistryName().toString())) { diff --git a/test-rig/client-resources/resources/META-INF/mods.toml b/test-rig/client-resources/resources/META-INF/mods.toml index 0e49b981..31aabfc1 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="[64,65)" +loaderVersion="[65,66)" license="GPL-3.0-only" showAsResourcePack=false @@ -12,20 +12,20 @@ description='''Build-only model, texture and translation checks. Never distribut [[dependencies.advantageclientresources]] modId="basepower" mandatory=true -versionRange="[2.4.16.2601021]" +versionRange="[2.4.17.2602001]" ordering="AFTER" side="CLIENT" [[dependencies.advantageclientresources]] modId="forge" mandatory=true -versionRange="[64.1.5]" +versionRange="[65.1.0]" ordering="NONE" side="CLIENT" [[dependencies.advantageclientresources]] modId="minecraft" mandatory=true -versionRange="[26.1.2]" +versionRange="[26.2]" ordering="NONE" side="CLIENT" diff --git a/test-rig/client-resources/resources/pack.mcmeta b/test-rig/client-resources/resources/pack.mcmeta index ea24dda9..28f8e5c9 100644 --- a/test-rig/client-resources/resources/pack.mcmeta +++ b/test-rig/client-resources/resources/pack.mcmeta @@ -1,7 +1,7 @@ { "pack": { "description": "Build-only Advantage client resource checks", - "min_format": [101,1], - "max_format": [101,1] + "min_format": [107,1], + "max_format": [107,1] } } diff --git a/test-rig/client-resources/test/zone/moddev/mc/advantageworks/client/NativeExperimentalFixtureGateTest.java b/test-rig/client-resources/test/zone/moddev/mc/advantageworks/client/NativeExperimentalFixtureGateTest.java index 8d451538..ee74dc1b 100644 --- a/test-rig/client-resources/test/zone/moddev/mc/advantageworks/client/NativeExperimentalFixtureGateTest.java +++ b/test-rig/client-resources/test/zone/moddev/mc/advantageworks/client/NativeExperimentalFixtureGateTest.java @@ -78,14 +78,14 @@ private static Button skip(AtomicInteger calls) { index.put("aliases", new ListTag()); index.put("ids", new ListTag()); index.put("overrides", new ListTag()); index.putIntArray("blocked", new int[0]); registries.put("minecraft:worldgen/biome", index); fml.put("Registries", registries); source.put("fml", fml); - CompoundTag data = new CompoundTag(); data.putInt("DataVersion", 4790); source.put("Data", data); + CompoundTag data = new CompoundTag(); data.putInt("DataVersion", 4903); source.put("Data", data); CompoundTag original = source.copy(), observed = source.copy(); assertEquals("UNCHANGED_TYPED_ROOT", NativeExperimentalFixtureGate.verifyCopiedLevel(source, observed)); observed.getCompound("fml").orElseThrow().getCompound("Registries").orElseThrow().remove("minecraft:worldgen/biome"); assertEquals("ONLY_FML_LOWERCASE_EMPTY_BIOME_INDEX_REMOVED", NativeExperimentalFixtureGate.verifyCopiedLevel(source, observed)); for (int mode = 0; mode < 4; mode++) { CompoundTag wrong = observed.copy(); - if (mode == 0) wrong.getCompound("Data").orElseThrow().putLong("DataVersion", 4790L); + if (mode == 0) wrong.getCompound("Data").orElseThrow().putLong("DataVersion", 4903L); if (mode == 1) wrong.getCompound("Data").orElseThrow().putInt("SpawnX", 1); if (mode == 2) wrong.getCompound("fml").orElseThrow().putString("unknown", "change"); if (mode == 3) wrong.getCompound("fml").orElseThrow().getCompound("Registries").orElseThrow().put("other:index", new CompoundTag()); diff --git a/test-rig/client-resources/test/zone/moddev/mc/advantageworks/client/NativeFamilyAtlasRegistrationTest.java b/test-rig/client-resources/test/zone/moddev/mc/advantageworks/client/NativeFamilyAtlasRegistrationTest.java index b20d3520..c08cfdab 100644 --- a/test-rig/client-resources/test/zone/moddev/mc/advantageworks/client/NativeFamilyAtlasRegistrationTest.java +++ b/test-rig/client-resources/test/zone/moddev/mc/advantageworks/client/NativeFamilyAtlasRegistrationTest.java @@ -51,7 +51,7 @@ public class NativeFamilyAtlasRegistrationTest { @BeforeClass public static void nativeBootstrap() { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); - assertEquals("26.1.2", SharedConstants.getCurrentVersion().name()); + assertEquals("26.2", SharedConstants.getCurrentVersion().name()); var sample = JsonParser.parseString("{\"type\":\"minecraft:single\",\"resource\":\"minecraft:block/stone\"}"); if (SpriteSources.CODEC.parse(JsonOps.INSTANCE, sample).error().isPresent()) SpriteSources.bootstrap(); assertTrue(SpriteSources.CODEC.parse(JsonOps.INSTANCE, sample).result().isPresent()); diff --git a/test-rig/gradle.properties b/test-rig/gradle.properties index edb93fbf..e0e0a2c8 100644 --- a/test-rig/gradle.properties +++ b/test-rig/gradle.properties @@ -1,11 +1,11 @@ org.gradle.jvmargs=-Xmx2G org.gradle.daemon=false -minecraft_version=26.1.2 -forge_version=64.1.5 +minecraft_version=26.2 +forge_version=65.1.0 mapping_channel=official -mapping_version=26.1.2 -mcp_version=20260409.101008 -harness_version=1.1.6.2601021 +mapping_version=26.2 +mcp_version=20260616.103818 +harness_version=1.1.7.2602001 # Local unqualified Power rebuilt against the released OreSpawn 4.1.1. -works_power_sha256=23ABCC1FEFDAA6988BF709437EB165C8948C757E773ADAB738092EB93CA8D2B0 +works_power_sha256=7E456DE1A45140E86415A701D186FEDE08C4AB63D53C6B7848935DB70CBC68B1 diff --git a/test-rig/migration/fixtures/native262-client-platform-sequences.json b/test-rig/migration/fixtures/native262-client-platform-sequences.json new file mode 100644 index 00000000..bb0a2d5e --- /dev/null +++ b/test-rig/migration/fixtures/native262-client-platform-sequences.json @@ -0,0 +1,596 @@ +{ + "purpose": "EXACT_NATIVE262_CLIENT_HOST_AND_CAPABILITY_DIAGNOSTICS_NOT_GAMEPLAY", + "source": "NativeSystemReportPlatformControl and isolated NettyJava25PlatformControl; real failing client06 appender frames retained separately", + "variants": { + "debug": [ + { + "thread": "main", + "level": "ERROR", + "logger": "oshi.driver.windows.registry.HkeyPerformanceDataUtil/", + "message": "Unable to locate English counter names in registry Perflib 009. Counters may need to be rebuilt: ", + "body": [ + "com.sun.jna.platform.win32.Win32Exception: The parameter is incorrect.", + "\tat SECURE-BOOTSTRAP/com.sun.jna.platform@5.17.0/com.sun.jna.platform.win32.Advapi32Util.registryGetStringArray(Advapi32Util.java:921) ~[jna-platform-5.17.0.jar!/:5.17.0 (b0)]", + "\tat SECURE-BOOTSTRAP/com.sun.jna.platform@5.17.0/com.sun.jna.platform.win32.Advapi32Util.registryGetStringArray(Advapi32Util.java:898) ~[jna-platform-5.17.0.jar!/:5.17.0 (b0)]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.HkeyPerformanceDataUtil.mapCounterIndicesFromRegistry(HkeyPerformanceDataUtil.java:274) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.HkeyPerformanceDataUtil.(HkeyPerformanceDataUtil.java:51) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.ProcessPerformanceData.buildProcessMapFromRegistry(ProcessPerformanceData.java:45) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.queryProcessMapFromRegistry(WindowsOperatingSystem.java:328) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.util.Memoizer$1.get(Memoizer.java:65) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.processMapToList(WindowsOperatingSystem.java:298) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.getProcess(WindowsOperatingSystem.java:292) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.OperatingSystem.getCurrentProcess(OperatingSystem.java:246) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.client.main.Main.main(Main.java:159) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) ~[modlauncher-10.2.6.jar:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.launcher.Main$Launcher.run(Main.java:276) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main$Launcher.access$000(Main.java:262) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main.main(Main.java:57) [slime-launcher-0.2.2.jar:0.2.2]" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "net.minecraft.SystemReport/", + "message": "Failed to get system info for Process Loads", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getUserTime()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$0(SystemReport.java:191) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:188) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.client.main.Main.main(Main.java:159) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) ~[modlauncher-10.2.6.jar:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.launcher.Main$Launcher.run(Main.java:276) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main$Launcher.access$000(Main.java:262) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main.main(Main.java:57) [slime-launcher-0.2.2.jar:0.2.2]" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "net.minecraft.SystemReport/", + "message": "Failed to get system info for Process Virtual Size (MiB)", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getVirtualSize()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$1(SystemReport.java:208) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:208) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.client.main.Main.main(Main.java:159) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) ~[modlauncher-10.2.6.jar:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.launcher.Main$Launcher.run(Main.java:276) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main$Launcher.access$000(Main.java:262) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main.main(Main.java:57) [slime-launcher-0.2.2.jar:0.2.2]" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "net.minecraft.SystemReport/", + "message": "Failed to get system info for Process Resident Size (MiB)", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getResidentSetSize()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$2(SystemReport.java:209) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:209) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.client.main.Main.main(Main.java:159) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) ~[modlauncher-10.2.6.jar:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.launcher.Main$Launcher.run(Main.java:276) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main$Launcher.access$000(Main.java:262) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main.main(Main.java:57) [slime-launcher-0.2.2.jar:0.2.2]" + ] + }, + { + "thread": "Server thread", + "level": "DEBUG", + "logger": "io.netty.util.internal.CleanerJava9/", + "message": "java.nio.ByteBuffer.cleaner(): unavailable", + "body": [ + "java.lang.UnsupportedOperationException: sun.misc.Unsafe unavailable", + "\tat SECURE-BOOTSTRAP/io.netty.common@4.2.15.Final/io.netty.util.internal.CleanerJava9.(CleanerJava9.java:70) ~[netty-common-4.2.15.Final.jar:4.2.15.Final]", + "\tat SECURE-BOOTSTRAP/io.netty.common@4.2.15.Final/io.netty.util.internal.PlatformDependent.(PlatformDependent.java:209) ~[netty-common-4.2.15.Final.jar:4.2.15.Final]", + "\tat SECURE-BOOTSTRAP/io.netty.buffer@4.2.15.Final/io.netty.buffer.UnpooledByteBufAllocator.(UnpooledByteBufAllocator.java:38) ~[netty-buffer-4.2.15.Final.jar:4.2.15.Final]", + "\tat SECURE-BOOTSTRAP/io.netty.buffer@4.2.15.Final/io.netty.buffer.Unpooled.(Unpooled.java:75) ~[netty-buffer-4.2.15.Final.jar:4.2.15.Final]", + "\tat TRANSFORMER/net.minecraftforge.forge@65.1.0/net.minecraftforge.network.ServerStatusPing.toBuf(ServerStatusPing.java:156) ~[forge-26.2-65.1.0.jar:?]", + "\tat TRANSFORMER/net.minecraftforge.forge@65.1.0/net.minecraftforge.network.ServerStatusPing.lambda$static$1(ServerStatusPing.java:88) ~[forge-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.codecs.RecordCodecBuilder$Instance.lambda$ap2$4(RecordCodecBuilder.java:217) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.codecs.RecordCodecBuilder$Instance.lambda$ap3$6(RecordCodecBuilder.java:284) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.codecs.RecordCodecBuilder$2.encode(RecordCodecBuilder.java:112) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.MapCodec$MapCodecCodec.encode(MapCodec.java:164) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.Encoder.encodeStart(Encoder.java:14) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.codecs.OptionalFieldCodec.encode(OptionalFieldCodec.java:44) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.codecs.OptionalFieldCodec.encode(OptionalFieldCodec.java:17) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.codecs.RecordCodecBuilder$Instance$6.encode(RecordCodecBuilder.java:298) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.codecs.RecordCodecBuilder$2.encode(RecordCodecBuilder.java:112) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.MapCodec$MapCodecCodec.encode(MapCodec.java:164) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.Encoder.encodeStart(Encoder.java:14) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.MinecraftServer.resetStatusCache(MinecraftServer.java:2256) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.MinecraftServer.runServer(MinecraftServer.java:736) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.MinecraftServer.lambda$spin$0(MinecraftServer.java:299) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat java.base/java.lang.Thread.run(Thread.java:1474) [?:?]" + ] + }, + { + "thread": "Server thread", + "level": "DEBUG", + "logger": "io.netty.util.internal.CleanerJava24Linker/", + "message": "java.nio.ByteBuffer.cleaner(): unavailable", + "body": [ + "java.lang.UnsupportedOperationException: Native access (restricted methods) is not enabled for the io.netty.common module.", + "\tat SECURE-BOOTSTRAP/io.netty.common@4.2.15.Final/io.netty.util.internal.CleanerJava24Linker.(CleanerJava24Linker.java:78) ~[netty-common-4.2.15.Final.jar:4.2.15.Final]", + "\tat SECURE-BOOTSTRAP/io.netty.common@4.2.15.Final/io.netty.util.internal.PlatformDependent.(PlatformDependent.java:211) ~[netty-common-4.2.15.Final.jar:4.2.15.Final]", + "\tat SECURE-BOOTSTRAP/io.netty.buffer@4.2.15.Final/io.netty.buffer.UnpooledByteBufAllocator.(UnpooledByteBufAllocator.java:38) ~[netty-buffer-4.2.15.Final.jar:4.2.15.Final]", + "\tat SECURE-BOOTSTRAP/io.netty.buffer@4.2.15.Final/io.netty.buffer.Unpooled.(Unpooled.java:75) ~[netty-buffer-4.2.15.Final.jar:4.2.15.Final]", + "\tat TRANSFORMER/net.minecraftforge.forge@65.1.0/net.minecraftforge.network.ServerStatusPing.toBuf(ServerStatusPing.java:156) ~[forge-26.2-65.1.0.jar:?]", + "\tat TRANSFORMER/net.minecraftforge.forge@65.1.0/net.minecraftforge.network.ServerStatusPing.lambda$static$1(ServerStatusPing.java:88) ~[forge-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.codecs.RecordCodecBuilder$Instance.lambda$ap2$4(RecordCodecBuilder.java:217) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.codecs.RecordCodecBuilder$Instance.lambda$ap3$6(RecordCodecBuilder.java:284) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.codecs.RecordCodecBuilder$2.encode(RecordCodecBuilder.java:112) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.MapCodec$MapCodecCodec.encode(MapCodec.java:164) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.Encoder.encodeStart(Encoder.java:14) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.codecs.OptionalFieldCodec.encode(OptionalFieldCodec.java:44) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.codecs.OptionalFieldCodec.encode(OptionalFieldCodec.java:17) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.codecs.RecordCodecBuilder$Instance$6.encode(RecordCodecBuilder.java:298) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.codecs.RecordCodecBuilder$2.encode(RecordCodecBuilder.java:112) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.MapCodec$MapCodecCodec.encode(MapCodec.java:164) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat SECURE-BOOTSTRAP/datafixerupper@10.0.21/com.mojang.serialization.Encoder.encodeStart(Encoder.java:14) ~[datafixerupper-10.0.21.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.MinecraftServer.resetStatusCache(MinecraftServer.java:2256) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.MinecraftServer.runServer(MinecraftServer.java:736) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.MinecraftServer.lambda$spin$0(MinecraftServer.java:299) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat java.base/java.lang.Thread.run(Thread.java:1474) [?:?]" + ] + }, + { + "thread": "Server thread", + "level": "DEBUG", + "logger": "io.netty.util.internal.CleanerJava25/", + "message": "java.nio.ByteBuffer.cleaner(): available", + "body": [] + } + ], + "latest": [ + { + "thread": "main", + "level": "ERROR", + "logger": "oshi.driver.windows.registry.HkeyPerformanceDataUtil/", + "message": "Unable to locate English counter names in registry Perflib 009. Counters may need to be rebuilt: ", + "body": [ + "com.sun.jna.platform.win32.Win32Exception: The parameter is incorrect.", + "\tat SECURE-BOOTSTRAP/com.sun.jna.platform@5.17.0/com.sun.jna.platform.win32.Advapi32Util.registryGetStringArray(Advapi32Util.java:921) ~[jna-platform-5.17.0.jar!/:5.17.0 (b0)]", + "\tat SECURE-BOOTSTRAP/com.sun.jna.platform@5.17.0/com.sun.jna.platform.win32.Advapi32Util.registryGetStringArray(Advapi32Util.java:898) ~[jna-platform-5.17.0.jar!/:5.17.0 (b0)]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.HkeyPerformanceDataUtil.mapCounterIndicesFromRegistry(HkeyPerformanceDataUtil.java:274) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.HkeyPerformanceDataUtil.(HkeyPerformanceDataUtil.java:51) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.ProcessPerformanceData.buildProcessMapFromRegistry(ProcessPerformanceData.java:45) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.queryProcessMapFromRegistry(WindowsOperatingSystem.java:328) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.util.Memoizer$1.get(Memoizer.java:65) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.processMapToList(WindowsOperatingSystem.java:298) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.getProcess(WindowsOperatingSystem.java:292) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.OperatingSystem.getCurrentProcess(OperatingSystem.java:246) ~[oshi-core-6.9.0.jar!/:6.9.0]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.client.main.Main.main(Main.java:159) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) ~[modlauncher-10.2.6.jar:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.launcher.Main$Launcher.run(Main.java:276) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main$Launcher.access$000(Main.java:262) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main.main(Main.java:57) [slime-launcher-0.2.2.jar:0.2.2]" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "net.minecraft.SystemReport/", + "message": "Failed to get system info for Process Loads", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getUserTime()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$0(SystemReport.java:191) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:188) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.client.main.Main.main(Main.java:159) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) ~[modlauncher-10.2.6.jar:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.launcher.Main$Launcher.run(Main.java:276) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main$Launcher.access$000(Main.java:262) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main.main(Main.java:57) [slime-launcher-0.2.2.jar:0.2.2]" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "net.minecraft.SystemReport/", + "message": "Failed to get system info for Process Virtual Size (MiB)", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getVirtualSize()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$1(SystemReport.java:208) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:208) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.client.main.Main.main(Main.java:159) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) ~[modlauncher-10.2.6.jar:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.launcher.Main$Launcher.run(Main.java:276) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main$Launcher.access$000(Main.java:262) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main.main(Main.java:57) [slime-launcher-0.2.2.jar:0.2.2]" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "net.minecraft.SystemReport/", + "message": "Failed to get system info for Process Resident Size (MiB)", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getResidentSetSize()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$2(SystemReport.java:209) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:209) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:115) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.client.main.Main.main(Main.java:159) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) ~[fmlloader-26.2-65.1.0.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) ~[modlauncher-10.2.6.jar:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) ~[modlauncher-10.2.6.jar:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) ~[bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.launcher.Main$Launcher.run(Main.java:276) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main$Launcher.access$000(Main.java:262) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main.main(Main.java:57) [slime-launcher-0.2.2.jar:0.2.2]" + ] + } + ], + "stdout": [ + { + "thread": "main", + "level": "ERROR", + "logger": "os.dr.wi.re.HkeyPerformanceDataUtil/", + "message": "Unable to locate English counter names in registry Perflib 009. Counters may need to be rebuilt: ", + "body": [ + "com.sun.jna.platform.win32.Win32Exception: The parameter is incorrect.", + "\tat SECURE-BOOTSTRAP/com.sun.jna.platform@5.17.0/com.sun.jna.platform.win32.Advapi32Util.registryGetStringArray(Advapi32Util.java:921) ~[jna-platform-5.17.0.jar!/:5.17.0 (b0)]", + "\tat SECURE-BOOTSTRAP/com.sun.jna.platform@5.17.0/com.sun.jna.platform.win32.Advapi32Util.registryGetStringArray(Advapi32Util.java:898) ~[jna-platform-5.17.0.jar!/:5.17.0 (b0)]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.HkeyPerformanceDataUtil.mapCounterIndicesFromRegistry(HkeyPerformanceDataUtil.java:274) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.HkeyPerformanceDataUtil.(HkeyPerformanceDataUtil.java:51) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.ProcessPerformanceData.buildProcessMapFromRegistry(ProcessPerformanceData.java:45) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.queryProcessMapFromRegistry(WindowsOperatingSystem.java:328) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.util.Memoizer$1.get(Memoizer.java:65) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.processMapToList(WindowsOperatingSystem.java:298) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.getProcess(WindowsOperatingSystem.java:292) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.OperatingSystem.getCurrentProcess(OperatingSystem.java:246) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:115) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:115) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.client.main.Main.main(Main.java:159) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) [modlauncher-10.2.6.jar!/:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) [bootstrap-2.1.8.jar!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.launcher.Main$Launcher.run(Main.java:276) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main$Launcher.access$000(Main.java:262) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main.main(Main.java:57) [slime-launcher-0.2.2.jar:0.2.2]", + "Transformer Audit:", + " net.minecraft.CrashReport", + " REASON: classloading", + " net.minecraft.SystemReport", + " REASON: classloading", + " net.minecraft.client.main.Main", + " REASON: classloading" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "minecraft/SystemReport", + "message": "Failed to get system info for Process Loads", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getUserTime()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$0(SystemReport.java:191) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:188) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:115) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:115) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.client.main.Main.main(Main.java:159) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) [modlauncher-10.2.6.jar!/:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) [bootstrap-2.1.8.jar!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.launcher.Main$Launcher.run(Main.java:276) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main$Launcher.access$000(Main.java:262) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main.main(Main.java:57) [slime-launcher-0.2.2.jar:0.2.2]", + "Transformer Audit:", + " net.minecraft.CrashReport", + " REASON: classloading", + " net.minecraft.SystemReport", + " REASON: classloading", + " net.minecraft.client.main.Main", + " REASON: classloading" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "minecraft/SystemReport", + "message": "Failed to get system info for Process Virtual Size (MiB)", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getVirtualSize()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$1(SystemReport.java:208) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:208) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:115) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:115) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.client.main.Main.main(Main.java:159) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) [modlauncher-10.2.6.jar!/:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) [bootstrap-2.1.8.jar!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.launcher.Main$Launcher.run(Main.java:276) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main$Launcher.access$000(Main.java:262) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main.main(Main.java:57) [slime-launcher-0.2.2.jar:0.2.2]", + "Transformer Audit:", + " net.minecraft.CrashReport", + " REASON: classloading", + " net.minecraft.SystemReport", + " REASON: classloading", + " net.minecraft.client.main.Main", + " REASON: classloading" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "minecraft/SystemReport", + "message": "Failed to get system info for Process Resident Size (MiB)", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getResidentSetSize()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$2(SystemReport.java:209) ~[forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:209) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:115) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:115) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:120) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.client.main.Main.main(Main.java:159) [forge-26.2-65.1.0.jar%230!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) [modlauncher-10.2.6.jar!/:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) [bootstrap-2.1.8.jar!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.launcher.Main$Launcher.run(Main.java:276) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main$Launcher.access$000(Main.java:262) [slime-launcher-0.2.2.jar:0.2.2]", + "\tat net.minecraftforge.launcher.Main.main(Main.java:57) [slime-launcher-0.2.2.jar:0.2.2]", + "Transformer Audit:", + " net.minecraft.CrashReport", + " REASON: classloading", + " net.minecraft.SystemReport", + " REASON: classloading", + " net.minecraft.client.main.Main", + " REASON: classloading" + ] + } + ] + } +} diff --git a/test-rig/migration/fixtures/native262-file-host-perflib.json b/test-rig/migration/fixtures/native262-file-host-perflib.json new file mode 100644 index 00000000..e635ec49 --- /dev/null +++ b/test-rig/migration/fixtures/native262-file-host-perflib.json @@ -0,0 +1,169 @@ +{ + "purpose": "EXACT_NATIVE262_WINDOWS_HOST_DIAGNOSTICS_FIXTURE_NOT_GAMEPLAY", + "source": "Same independently reproduced host issue, full-name Forge file appender", + "blocks": [ + { + "thread": "main", + "level": "ERROR", + "logger": "oshi.driver.windows.registry.HkeyPerformanceDataUtil/", + "message": "Unable to locate English counter names in registry Perflib 009. Counters may need to be rebuilt: ", + "body": [ + "com.sun.jna.platform.win32.Win32Exception: The parameter is incorrect.", + "\tat SECURE-BOOTSTRAP/com.sun.jna.platform@5.17.0/com.sun.jna.platform.win32.Advapi32Util.registryGetStringArray(Advapi32Util.java:921)", + "\tat SECURE-BOOTSTRAP/com.sun.jna.platform@5.17.0/com.sun.jna.platform.win32.Advapi32Util.registryGetStringArray(Advapi32Util.java:898)", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.HkeyPerformanceDataUtil.mapCounterIndicesFromRegistry(HkeyPerformanceDataUtil.java:274)", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.HkeyPerformanceDataUtil.(HkeyPerformanceDataUtil.java:51)", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.ProcessPerformanceData.buildProcessMapFromRegistry(ProcessPerformanceData.java:45)", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.queryProcessMapFromRegistry(WindowsOperatingSystem.java:328)", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.util.Memoizer$1.get(Memoizer.java:65)", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.processMapToList(WindowsOperatingSystem.java:298)", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.getProcess(WindowsOperatingSystem.java:292)", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.OperatingSystem.getCurrentProcess(OperatingSystem.java:246)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:116)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:116)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.Main.main(Main.java:120)", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565)", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96)", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17)", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188)", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565)", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133)", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53)", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19)", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565)", + "\tat net.minecraftforge.bootstrap.shim.Main.main(Main.java:101)" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "net.minecraft.SystemReport/", + "message": "Failed to get system info for Process Loads", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getUserTime()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$0(SystemReport.java:190)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:189)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:116)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:116)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.Main.main(Main.java:120)", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565)", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96)", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17)", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188)", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565)", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133)", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53)", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19)", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565)", + "\tat net.minecraftforge.bootstrap.shim.Main.main(Main.java:101)" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "net.minecraft.SystemReport/", + "message": "Failed to get system info for Process Virtual Size (MiB)", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getVirtualSize()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$1(SystemReport.java:202)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:202)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:116)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:116)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.Main.main(Main.java:120)", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565)", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96)", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17)", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188)", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565)", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133)", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53)", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19)", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565)", + "\tat net.minecraftforge.bootstrap.shim.Main.main(Main.java:101)" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "net.minecraft.SystemReport/", + "message": "Failed to get system info for Process Resident Size (MiB)", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getResidentSetSize()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$2(SystemReport.java:203)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:203)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:116)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:116)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.Main.main(Main.java:120)", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565)", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96)", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110)", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17)", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188)", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565)", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133)", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53)", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19)", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565)", + "\tat net.minecraftforge.bootstrap.shim.Main.main(Main.java:101)" + ] + } + ] +} diff --git a/test-rig/migration/fixtures/native262-server-host-perflib.json b/test-rig/migration/fixtures/native262-server-host-perflib.json new file mode 100644 index 00000000..b88e5aaa --- /dev/null +++ b/test-rig/migration/fixtures/native262-server-host-perflib.json @@ -0,0 +1,197 @@ +{ + "purpose": "EXACT_NATIVE262_WINDOWS_HOST_DIAGNOSTICS_FIXTURE_NOT_GAMEPLAY", + "source": "Failed strict original110 run, independently reproduced without Advantage by NativeSystemReportPlatformControl", + "blocks": [ + { + "thread": "main", + "level": "ERROR", + "logger": "os.dr.wi.re.HkeyPerformanceDataUtil/", + "message": "Unable to locate English counter names in registry Perflib 009. Counters may need to be rebuilt: ", + "body": [ + "com.sun.jna.platform.win32.Win32Exception: The parameter is incorrect.", + "\tat SECURE-BOOTSTRAP/com.sun.jna.platform@5.17.0/com.sun.jna.platform.win32.Advapi32Util.registryGetStringArray(Advapi32Util.java:921) ~[jna-platform-5.17.0.jar!/:5.17.0 (b0)]", + "\tat SECURE-BOOTSTRAP/com.sun.jna.platform@5.17.0/com.sun.jna.platform.win32.Advapi32Util.registryGetStringArray(Advapi32Util.java:898) ~[jna-platform-5.17.0.jar!/:5.17.0 (b0)]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.HkeyPerformanceDataUtil.mapCounterIndicesFromRegistry(HkeyPerformanceDataUtil.java:274) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.HkeyPerformanceDataUtil.(HkeyPerformanceDataUtil.java:51) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.driver.windows.registry.ProcessPerformanceData.buildProcessMapFromRegistry(ProcessPerformanceData.java:45) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.queryProcessMapFromRegistry(WindowsOperatingSystem.java:328) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.util.Memoizer$1.get(Memoizer.java:65) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.processMapToList(WindowsOperatingSystem.java:298) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.windows.WindowsOperatingSystem.getProcess(WindowsOperatingSystem.java:292) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat SECURE-BOOTSTRAP/com.github.oshi@6.9.0/oshi.software.os.OperatingSystem.getCurrentProcess(OperatingSystem.java:246) [oshi-core-6.9.0.jar!/:6.9.0]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:116) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:116) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.Main.main(Main.java:120) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) [modlauncher-10.2.6.jar!/:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) [bootstrap-2.1.8.jar!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) [bootstrap-2.1.8.jar:2.1.8]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.shim.Main.main(Main.java:101) [forge-26.2-65.1.0-shim.jar:2.1.8]", + "Transformer Audit:", + " net.minecraft.CrashReport", + " REASON: classloading", + " net.minecraft.SystemReport", + " REASON: classloading", + " net.minecraft.server.Main", + " REASON: classloading" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "minecraft/SystemReport", + "message": "Failed to get system info for Process Loads", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getUserTime()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$0(SystemReport.java:190) ~[forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:189) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:116) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:116) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.Main.main(Main.java:120) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) [modlauncher-10.2.6.jar!/:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) [bootstrap-2.1.8.jar!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) [bootstrap-2.1.8.jar:2.1.8]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.shim.Main.main(Main.java:101) [forge-26.2-65.1.0-shim.jar:2.1.8]", + "Transformer Audit:", + " net.minecraft.CrashReport", + " REASON: classloading", + " net.minecraft.SystemReport", + " REASON: classloading", + " net.minecraft.server.Main", + " REASON: classloading" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "minecraft/SystemReport", + "message": "Failed to get system info for Process Virtual Size (MiB)", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getVirtualSize()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$1(SystemReport.java:202) ~[forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:202) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:116) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:116) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.Main.main(Main.java:120) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) [modlauncher-10.2.6.jar!/:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) [bootstrap-2.1.8.jar!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) [bootstrap-2.1.8.jar:2.1.8]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.shim.Main.main(Main.java:101) [forge-26.2-65.1.0-shim.jar:2.1.8]", + "Transformer Audit:", + " net.minecraft.CrashReport", + " REASON: classloading", + " net.minecraft.SystemReport", + " REASON: classloading", + " net.minecraft.server.Main", + " REASON: classloading" + ] + }, + { + "thread": "main", + "level": "WARN", + "logger": "minecraft/SystemReport", + "message": "Failed to get system info for Process Resident Size (MiB)", + "body": [ + "java.lang.NullPointerException: Cannot invoke \"oshi.software.os.OSProcess.getResidentSetSize()\" because \"process\" is null", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putProcessDetails$2(SystemReport.java:203) ~[forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.setDetail(SystemReport.java:96) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putProcessDetails(SystemReport.java:203) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$putSoftware$0(SystemReport.java:116) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.putSoftware(SystemReport.java:116) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.lambda$new$7(SystemReport.java:68) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.ignoreErrors(SystemReport.java:121) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.SystemReport.(SystemReport.java:68) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.(CrashReport.java:32) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.CrashReport.preload(CrashReport.java:227) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.Main.main(Main.java:120) [forge-26.2-65.1.0-server.jar!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.runTarget(CommonLaunchHandler.java:96) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/net.minecraftforge.fmlloader@26.2-65.1.0/net.minecraftforge.fml.loading.targets.CommonLaunchHandler.lambda$makeService$0(CommonLaunchHandler.java:79) [fmlloader-26.2-65.1.0.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:84) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:104) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.run(Launcher.java:151) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.Launcher.main(Launcher.java:110) [modlauncher-10.2.6.jar!/:?]", + "\tat SECURE-BOOTSTRAP/cpw.mods.modlauncher@10.2.6/cpw.mods.modlauncher.BootstrapEntry.main(BootstrapEntry.java:17) [modlauncher-10.2.6.jar!/:?]", + "\tat net.minecraftforge.bootstrap@2.1.7/net.minecraftforge.bootstrap.Bootstrap.moduleMain(Bootstrap.java:188) [bootstrap-2.1.8.jar!/:?]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.Bootstrap.bootstrapMain(Bootstrap.java:133) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.Bootstrap.start(Bootstrap.java:53) [bootstrap-2.1.8.jar:2.1.8]", + "\tat net.minecraftforge.bootstrap.ForgeBootstrap.main(ForgeBootstrap.java:19) [bootstrap-2.1.8.jar:2.1.8]", + "\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]", + "\tat java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]", + "\tat net.minecraftforge.bootstrap.shim.Main.main(Main.java:101) [forge-26.2-65.1.0-shim.jar:2.1.8]", + "Transformer Audit:", + " net.minecraft.CrashReport", + " REASON: classloading", + " net.minecraft.SystemReport", + " REASON: classloading", + " net.minecraft.server.Main", + " REASON: classloading" + ] + } + ] +} diff --git a/test-rig/migration/fixtures/native262-server-netty-java25.json b/test-rig/migration/fixtures/native262-server-netty-java25.json new file mode 100644 index 00000000..532a82c0 --- /dev/null +++ b/test-rig/migration/fixtures/native262-server-netty-java25.json @@ -0,0 +1,53 @@ +{ + "purpose": "EXACT_SOURCE_AND_BINARY_BOUND_NATIVE_NETTY_CAPABILITY_FALLBACK", + "source": "NettyJava25PlatformControl independently ran direct allocation/cleaning against Netty 4.2.15.Final on Java25, without Minecraft or mods", + "blocks": [ + { + "thread": "Server thread", + "level": "DEBUG", + "logger": "io.netty.util.internal.CleanerJava9/", + "message": "java.nio.ByteBuffer.cleaner(): unavailable", + "body": [ + "java.lang.UnsupportedOperationException: sun.misc.Unsafe unavailable", + "\tat SECURE-BOOTSTRAP/io.netty.common@4.2.15.Final/io.netty.util.internal.CleanerJava9.(CleanerJava9.java:70)", + "\tat SECURE-BOOTSTRAP/io.netty.common@4.2.15.Final/io.netty.util.internal.PlatformDependent.(PlatformDependent.java:209)", + "\tat SECURE-BOOTSTRAP/io.netty.common@4.2.15.Final/io.netty.util.internal.ClassInitializerUtil.tryLoadClasses(ClassInitializerUtil.java:32)", + "\tat SECURE-BOOTSTRAP/io.netty.transport.classes.kqueue@4.2.15.Final/io.netty.channel.kqueue.Native.(Native.java:65)", + "\tat SECURE-BOOTSTRAP/io.netty.transport.classes.kqueue@4.2.15.Final/io.netty.channel.kqueue.KQueue.(KQueue.java:38)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.network.EventLoopGroupHolder.remote(EventLoopGroupHolder.java:59)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.network.ServerConnectionListener.startTcpServerListener(ServerConnectionListener.java:60)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.dedicated.DedicatedServer.initServer(DedicatedServer.java:217)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.MinecraftServer.runServer(MinecraftServer.java:728)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.MinecraftServer.lambda$spin$0(MinecraftServer.java:299)", + "\tat java.base/java.lang.Thread.run(Thread.java:1474)" + ] + }, + { + "thread": "Server thread", + "level": "DEBUG", + "logger": "io.netty.util.internal.CleanerJava24Linker/", + "message": "java.nio.ByteBuffer.cleaner(): unavailable", + "body": [ + "java.lang.UnsupportedOperationException: Native access (restricted methods) is not enabled for the io.netty.common module.", + "\tat SECURE-BOOTSTRAP/io.netty.common@4.2.15.Final/io.netty.util.internal.CleanerJava24Linker.(CleanerJava24Linker.java:78)", + "\tat SECURE-BOOTSTRAP/io.netty.common@4.2.15.Final/io.netty.util.internal.PlatformDependent.(PlatformDependent.java:211)", + "\tat SECURE-BOOTSTRAP/io.netty.common@4.2.15.Final/io.netty.util.internal.ClassInitializerUtil.tryLoadClasses(ClassInitializerUtil.java:32)", + "\tat SECURE-BOOTSTRAP/io.netty.transport.classes.kqueue@4.2.15.Final/io.netty.channel.kqueue.Native.(Native.java:65)", + "\tat SECURE-BOOTSTRAP/io.netty.transport.classes.kqueue@4.2.15.Final/io.netty.channel.kqueue.KQueue.(KQueue.java:38)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.network.EventLoopGroupHolder.remote(EventLoopGroupHolder.java:59)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.network.ServerConnectionListener.startTcpServerListener(ServerConnectionListener.java:60)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.dedicated.DedicatedServer.initServer(DedicatedServer.java:217)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.MinecraftServer.runServer(MinecraftServer.java:728)", + "\tat TRANSFORMER/minecraft@26.2/net.minecraft.server.MinecraftServer.lambda$spin$0(MinecraftServer.java:299)", + "\tat java.base/java.lang.Thread.run(Thread.java:1474)" + ] + }, + { + "thread": "Server thread", + "level": "DEBUG", + "logger": "io.netty.util.internal.CleanerJava25/", + "message": "java.nio.ByteBuffer.cleaner(): available", + "body": [] + } + ] +} diff --git a/test-rig/migration/native262.py b/test-rig/migration/native262.py new file mode 100644 index 00000000..eafb92e2 --- /dev/null +++ b/test-rig/migration/native262.py @@ -0,0 +1,451 @@ +"""Forge 65.1.0 server custody and process control; historical engines stay unchanged.""" + +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import socket +import subprocess +from zipfile import ZipFile + +from matrix import digest, stage as stage_server, write_json +from native117 import jar_identity, require_disjoint +from native117_code import NativeLaunch, code_files +from native117_interop import mark_runtime +from native1201 import FLAT_SETTINGS +from native12111 import (compat_tile_nbt, compat_tile_data, parse_endurance_snapshot, + log_findings as previous_log_findings) +from native262_platform_diagnostics import COMMON_JAR_SHA, classify as classify_platform_log + + +RIG = Path(__file__).resolve().parents[1] +MINECRAFT = "26.2" +FORGE = "65.1.0" +MCP = "20260616.103818" +FORGE_VERSION = f"{MINECRAFT}-{FORGE}" +SHIM = f"forge-{FORGE_VERSION}-shim.jar" +SHIM_SHA = "8D113D3DFFF0F0978A1989E22A8040FAD0FE5E332EF766B3702EB29687C9F4D2" +INSTALLER_SHA = "813FBBFECC3086A492783B9F5023AE15007737B42E17AAB75F48BA77403314DF" +JAVA_SHA = "D5F1B9E03E298EBFB04D2326ECC1E2AF3E6817F9B4D7F9F151FE856D56975650" +JAVA_VERSION = "25.0.3+9" +LOGGING_JAR_SHA = "7D1A733519C86FE2665110BF660DCF4B7358B97F416975444906E5CC77F163BE" +LOGGING_XML_SHA = "9680BAF9F936C8CD009CD3A8D75253B476FCA9C316F8D5B5C51700053BEAE440" +LAUNCHER = f"libraries/net/minecraftforge/forge/{FORGE_VERSION}/" + ( + "win_args.txt" if os.name == "nt" else "unix_args.txt") +INVENTORY = "native262-code-inventory.json" +EXPLORATORY = "EXPLORATORY_UNQUALIFIED" +VERSIONS = {"basepower": "2.4.17.2602001", "advantageworkstest": "1.1.7.2602001", + "basesteam": "2.2.18.2602001", "baseelectric": "2.2.18.2602001", + "basemetals": "3.1.2.2602001", "orespawn": "4.1.1.2602001"} + + +def read_json(path): + def unique(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError("Duplicate JSON field: " + key) + result[key] = value + return result + path = Path(path) + if path.stat().st_size > 16 * 1024 * 1024: + raise ValueError("Unbounded input document: " + str(path)) + return json.loads(path.read_text(encoding="utf-8-sig"), object_pairs_hook=unique) + + +def validate_hash(path, expected): + if not isinstance(expected, str) or not re.fullmatch(r"[A-Fa-f0-9]{64}", expected): + raise ValueError("An exact SHA-256 pin is required: " + str(path)) + if digest(path) != expected.upper(): + raise AssertionError("Artifact SHA-256 mismatch: " + str(path)) + + +def native_launch_environment(runtime=None, label=None, *, diagnostic_environment=None, + diagnostic_environment_sha256=None): + environment = os.environ.copy() + for key in ("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS", "CLASSPATH"): + if environment.get(key, "").strip(): + raise ValueError("Forge 65 cannot inherit launch options from " + key) + if diagnostic_environment is None and diagnostic_environment_sha256 is None: + return environment + if diagnostic_environment is None or diagnostic_environment_sha256 is None: + raise ValueError("Diagnostic manifest and exact SHA-256 must be supplied together") + if runtime is None or not isinstance(label, str) or not re.fullmatch(r"[A-Za-z0-9_-][A-Za-z0-9_.-]{0,99}", label): + raise ValueError("Diagnostic launch needs a bounded runtime and label") + runtime = Path(runtime).resolve(strict=True) + path = Path(diagnostic_environment).resolve(strict=True) + if path.stat().st_size > 16384: + raise ValueError("Unbounded diagnostic manifest") + validate_hash(path, diagnostic_environment_sha256) + document = read_json(path) + if (set(document) != {"format", "purpose", "runtime", "label", "properties"} + or type(document["format"]) is not int or document["format"] != 1 + or document["purpose"] != "NATIVE65_MIXIN_EXPORT_DIAGNOSTIC" + or document["runtime"] != str(runtime) or document["label"] != label): + raise ValueError("Diagnostic manifest is not bound to this native65 launch") + properties = document["properties"] + allowed = {"mixin.debug.export", "mixin.debug.export.decompile", "mixin.debug.export.filter"} + if (not isinstance(properties, dict) or set(properties) - allowed + or properties.get("mixin.debug.export") is not True): + raise ValueError("Only explicitly sealed mixin export properties are permitted") + options = ["-Dmixin.debug.export=true"] + if "mixin.debug.export.decompile" in properties: + value = properties["mixin.debug.export.decompile"] + if type(value) is not bool: + raise ValueError("Mixin decompile must be boolean") + options.append("-Dmixin.debug.export.decompile=" + str(value).lower()) + if "mixin.debug.export.filter" in properties: + value = properties["mixin.debug.export.filter"] + if not isinstance(value, str) or not re.fullmatch(r"[A-Za-z0-9_.$*?]{1,256}", value): + raise ValueError("Unsafe mixin export filter") + options.append("-Dmixin.debug.export.filter=" + value) + injected = {"JAVA_TOOL_OPTIONS": " ".join(options)} + with (runtime / f"native65-launch-environment-{label}.json").open("x", encoding="utf-8") as output: + json.dump(dict(status="DIAGNOSTIC_NOT_QUALIFIED", qualificationAllowed=False, + manifestSha256=diagnostic_environment_sha256.upper(), manifest=document, + environment=injected), output, indent=2, sort_keys=True) + environment.update(injected) + return environment + + +def java25(executable): + java = Path(executable).resolve(strict=True) + validate_hash(java, JAVA_SHA) + result = subprocess.run([str(java), "-XX:+UseCompactObjectHeaders", "-version"], + env=native_launch_environment(), capture_output=True, text=True, + check=True, timeout=30) + version = (result.stderr or result.stdout).strip() + if not re.search(r'version "25\.0\.3"', version) or "25.0.3+9" not in version: + raise ValueError("Forge 65 requires the pinned Java 25.0.3+9: " + version) + return java, version + + +def require_free_port(port): + if type(port) is not int or not 1024 <= port <= 65535: + raise ValueError("Supply a nonprivileged TCP port") + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe: + if hasattr(socket, "SO_EXCLUSIVEADDRUSE"): + probe.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) + try: + probe.bind(("127.0.0.1", port)) + except OSError as failure: + raise ValueError(f"TCP port {port} is occupied; no process will be terminated") from failure + + +def inspect_seed(seed): + seed = Path(seed).resolve(strict=True) + for path in [seed, *(seed / "libraries").rglob("*"), *seed.glob("*.jar")]: + if path.is_symlink() or path.is_junction(): + raise ValueError("Server code may not use filesystem aliases: " + str(path)) + if path.is_file() and path.stat().st_nlink != 1: + raise ValueError("Server code may not share writable hard links: " + str(path)) + validate_hash(seed / SHIM, SHIM_SHA) + validate_hash(seed / "libraries/io/netty/netty-common/4.2.15.Final/netty-common-4.2.15.Final.jar", COMMON_JAR_SHA) + command = (seed / LAUNCHER).read_text(encoding="utf-8").split() + if command != ["-Djava.net.preferIPv6Addresses=system", "-XX:+UseCompactObjectHeaders", "-jar", SHIM]: + raise AssertionError("Forge 65 launcher is not the selected official shim command") + files = code_files(seed) + if set(seed.glob("*.jar")) != {seed / SHIM}: + raise AssertionError("Unexpected root server code") + return dict(forge=FORGE_VERSION, javaVersion=JAVA_VERSION, javaSha256=JAVA_SHA, + installerSha256=INSTALLER_SHA, files=files) + + +def seal(seed, inventory): + seed, inventory = Path(seed).resolve(strict=True), Path(inventory).resolve() + require_disjoint(inventory, seed) + if inventory.exists(): + raise FileExistsError(inventory) + allowed = {SHIM, "libraries"} + if {path.name for path in seed.iterdir()} != allowed: + raise ValueError("Seal only a fresh code-only SDK, never a configured server/world") + result = inspect_seed(seed) + inventory.parent.mkdir(parents=True, exist_ok=True) + with inventory.open("x", encoding="utf-8") as output: + json.dump(result, output, indent=2, sort_keys=True) + output.write("\n") + return result + + +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): + seed = Path(seed).resolve(strict=True) + validate_hash(inventory_file, inventory_sha256) + inventory = read_json(inventory_file) + if inspect_seed(seed) != inventory: + raise AssertionError("Selected Forge 65 server differs from its sealed inventory") + document = read_json(profile) + if (document.get("mode"), document.get("minecraftVersion"), document.get("forge", {}).get("version"), + document.get("qualificationStatus")) != ("packaged", MINECRAFT, FORGE_VERSION, EXPLORATORY): + raise ValueError("Expected an explicitly materialized Forge 65 packaged profile") + mods = {} + for item in document["mods"]: + identity, version = item["id"], item["version"] + path = (RIG / item["path"]).resolve(strict=True) + if identity in mods or version != VERSIONS.get(identity) or path.name != item["file"] or not path.name.endswith(f"-{version}.jar"): + raise ValueError("Invalid native262 ordinary mod pin: " + identity) + validate_hash(path, item["sha256"]) + jar_identity(path, identity, version) + mods[identity] = version, path, item["sha256"].upper() + if set(mods) != set(VERSIONS): + raise ValueError("Full family qualification requires all six exact native262 inputs") + verify_compile_pin(mods["advantageworkstest"][1], mods["basepower"][2]) + return seed, inventory, mods + + +def verify_runtime(runtime, inventory, expected): + runtime = Path(runtime) + if inspect_seed(runtime) != inventory: + raise AssertionError("Staged Forge 65 code changed") + mods = runtime / "mods" + if any(not path.is_file() or path.is_symlink() or path.stat().st_nlink != 1 for path in mods.iterdir()): + raise AssertionError("Unexpected aliased or nested mod input") + if {path.name: digest(path) for path in mods.iterdir()} != expected: + raise AssertionError("Staged mod jars changed or unexpected files were added") + + +def audit_staged(runtime): + runtime = Path(runtime) + expected = {item["file"]: item["sha256"] for item in read_json(runtime / "input-jars.json")} + verify_runtime(runtime, read_json(runtime / INVENTORY), expected) + + +def stage(runtime, seed, inventory, mods, port): + runtime = Path(runtime).resolve() + native_launch_environment() + if set(mods) != set(VERSIONS) or any(mods[key][0] != version for key, version in VERSIONS.items()): + raise ValueError("Staging requires all six actual native262 family inputs") + if len({mod[1].name.casefold() for mod in mods.values()}) != len(mods): + raise ValueError("Mod filenames must be unique") + require_free_port(port) + if runtime.exists(): + raise FileExistsError(runtime) + for path in [Path(seed), *(mod[1] for mod in mods.values())]: + require_disjoint(runtime, Path(path).resolve(strict=True)) + if inspect_seed(seed) != inventory: + raise AssertionError("Seed custody changed before staging") + for _, path, sha in mods.values(): + validate_hash(path, sha) + 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) + configure_runtime(runtime, port) + verify_runtime(runtime, inventory, expected) + return expected + + +def configure_runtime(runtime, port, profile=None, recipe_profile=None): + if recipe_profile is not None: + raise ValueError("Recipe configuration needs an explicit reviewed native262 adapter") + runtime = Path(runtime) + profile = profile or {} + overrides = {"level-type": profile.get("levelType", "minecraft:flat"), + "generator-settings": json.dumps(FLAT_SETTINGS, separators=(",", ":")), + "server-ip": "127.0.0.1", "server-port": port, + "generate-structures": "false", "simulation-distance": 4, "allow-nether": "true", + "pause-when-empty-seconds": "-1"} + settings = runtime / "server.properties" + old = [line for line in settings.read_text(encoding="utf-8").splitlines() + if line.partition("=")[0] not in overrides] + settings.write_text("\n".join(old + [f"{key}={value}" for key, value in overrides.items()]) + "\n", encoding="utf-8") + config = runtime / "config/fml.toml" + config.parent.mkdir(exist_ok=True) + config.write_text("versionCheck = false\n", encoding="utf-8") + mark_runtime(runtime) + + +def require_launch_settings(runtime): + settings = {} + for line in (Path(runtime) / "server.properties").read_text(encoding="utf-8").splitlines(): + if not line or line.startswith(("#", "!")): + continue + key, separator, value = line.partition("=") + if not separator or key in settings: + raise ValueError("Ambiguous server launch property: " + key) + settings[key] = value + if settings.get("pause-when-empty-seconds") != "-1" or settings.get("server-ip") != "127.0.0.1": + raise ValueError("Native262 requires explicit loopback binding and disabled empty-server pausing") + require_free_port(int(settings["server-port"])) + + +def plain_logging_bytes(original): + if hashlib.sha256(original).hexdigest().upper() != LOGGING_XML_SHA or original.count(b"%xEx") != 2: + raise AssertionError("Selected Forge 65 logging source changed; review the formatter adapter") + # Byte preservation keeps every level, filter, marker and appender unchanged. + return original.replace(b"%xEx", b"%ex") + + +def logging_configuration(runtime, logging_jar): + runtime, logging_jar = Path(runtime), Path(logging_jar) + validate_hash(logging_jar, LOGGING_JAR_SHA) + with ZipFile(logging_jar) as jar: + content = plain_logging_bytes(jar.read("log4j2.xml")) + config = runtime / "forge65-log4j2-plain-stacktraces.xml" + custody = runtime / "forge65-logging-custody.json" + receipt = dict(sourceJarSha256=LOGGING_JAR_SHA, sourceXmlSha256=LOGGING_XML_SHA, + configSha256=hashlib.sha256(content).hexdigest().upper(), + modification="Exactly two %xEx renderers to %ex; all other source bytes retained") + if config.exists() or custody.exists(): + if not config.is_file() or not custody.is_file() or config.read_bytes() != content or read_json(custody) != receipt: + raise AssertionError("Forge 65 logging custody changed") + else: + with config.open("xb") as output: + output.write(content) + with custody.open("x", encoding="utf-8") as output: + json.dump(receipt, output, indent=2, sort_keys=True) + return config.resolve().as_uri() + + +def log_findings(text): + failures, diagnostics = classify_platform_log(text, previous_log_findings) + for line in text.splitlines(): + if re.search(r"Server empty for \d+ seconds, pausing|Unable to write to stream|Unable to invoke factory method", line): + if line not in failures: + failures.append(line) + return failures, diagnostics + + +def require_clean_log(path): + failures, diagnostics = log_findings(Path(path).read_text(encoding="utf-8", errors="replace")) + if failures: + raise AssertionError(f"Native65 runtime errors in {path}: {failures[:5]}") + return diagnostics + + +def require_complete_boot(text, *, stop_sent, returncode): + if returncode != 0 or not stop_sent: + raise AssertionError("Native65 requires its owned stop command and a zero process exit") + markers = (f"Forge mod loading, version {FORGE}, for MC {MINECRAFT} with MCP {MCP}", + f"Starting minecraft server version {MINECRAFT}", "Done (", "Stopping server", + "All dimensions are saved") + cursor = 0 + for marker in markers: + position = text.find(marker, cursor) + if position < 0: + raise AssertionError("Missing or out-of-order native65 startup / cold-stop witness: " + marker) + cursor = position + len(marker) + + +class Server(NativeLaunch): + forge_version = FORGE_VERSION + + def __init__(self, runtime, java, launcher, label, *, diagnostic_environment=None, + diagnostic_environment_sha256=None): + runtime = Path(runtime).resolve(strict=True) + if not re.fullmatch(r"[A-Za-z0-9_-][A-Za-z0-9_.-]{0,99}", label): + raise ValueError("Unsafe launch label") + if str(launcher).replace("\\", "/") != LAUNCHER: + raise ValueError("Launch only the selected Forge 65 official argument file") + java25(java) + audit_staged(runtime) + require_launch_settings(runtime) + self.diagnostic_environment = diagnostic_environment + self.diagnostic_environment_sha256 = diagnostic_environment_sha256 + self.stop_sent = False + self.closed = False + super().__init__(runtime, java, launcher, label) + write_json(runtime / f"native65-process-{label}.json", dict( + pid=self.process.pid, runtime=str(runtime), javaSha256=JAVA_SHA, + javaVersion=JAVA_VERSION, forge=FORGE_VERSION, stdin="OWNED_PIPE", + qualificationAllowed=diagnostic_environment is None)) + + def launch_environment(self): + return native_launch_environment(self.native_runtime, self.native_label, + diagnostic_environment=self.diagnostic_environment, + diagnostic_environment_sha256=self.diagnostic_environment_sha256) + + @staticmethod + def unexpected_line(line): + return re.search(r"Server empty for \d+ seconds, pausing", line) is not None + + def logging_configuration(self, logging_jar): + return logging_configuration(self.native_runtime, logging_jar) + + def send(self, command): + if not isinstance(command, str) or not command or "\n" in command or "\r" in command: + raise ValueError("Expected one bounded server command") + if command.startswith("gamerule "): + words = command.split() + if len(words) not in (2, 3): + raise ValueError("Expected one gamerule query or assignment") + aliases = {"doMobSpawning": "spawn_mobs", "randomTickSpeed": "random_tick_speed", + "doWeatherCycle": "advance_weather", "doDaylightCycle": "advance_time", + "doTileDrops": "block_drops", "doEntityDrops": "entity_drops", + "keepInventory": "keep_inventory", "mobGriefing": "mob_griefing"} + rule = aliases.get(words[1], words[1]) + if not re.fullmatch(r"(?:[a-z0-9_.-]+:)?[a-z0-9_./-]+", rule): + raise ValueError("Unknown legacy gamerule: " + words[1]) + if ":" not in rule: + rule = "minecraft:" + rule + super().send(" ".join(["gamerule", rule, *words[2:]])) + shown = rule.removeprefix("minecraft:") + self.wait(f"Gamerule {shown} is now set to: {words[2]}" if len(words) == 3 + else f"Gamerule {shown} is currently set to: ", 15) + return + super().send(command) + if command == "stop": + self.stop_sent = True + + 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): + if self.closed: + audit_staged(self.native_runtime) + return + try: + super().close() + text = (self.native_runtime / f"console-{self.native_label}.log").read_text(encoding="utf-8") + require_complete_boot(text, stop_sent=self.stop_sent, returncode=self.process.returncode) + logging_jar = self.native_runtime / f"libraries/net/minecraftforge/fmlloader/{FORGE_VERSION}/fmlloader-{FORGE_VERSION}.jar" + self.logging_configuration(logging_jar) + finally: + audit_staged(self.native_runtime) + self.closed = self.process.poll() is not None + + +def launch(runtime, java, label="native65"): + return Server(runtime, java, LAUNCHER, label) + + +def cold_stop(server): + server.close() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + command = sub.add_parser("seal") + command.add_argument("--seed", type=Path, required=True) + command.add_argument("--inventory", type=Path, required=True) + command = sub.add_parser("verify") + command.add_argument("--profile", type=Path, required=True) + command.add_argument("--seed", type=Path, required=True) + command.add_argument("--inventory", type=Path, required=True) + command.add_argument("--inventory-sha256", required=True) + command.add_argument("--java", type=Path, required=True) + args = parser.parse_args() + if args.command == "seal": + seal(args.seed, args.inventory) + else: + java25(args.java) + read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + print("Native262 inputs verified; no world opened") + + +if __name__ == "__main__": + main() diff --git a/test-rig/migration/native262_actor_comparison.py b/test-rig/migration/native262_actor_comparison.py new file mode 100644 index 00000000..f84e6154 --- /dev/null +++ b/test-rig/migration/native262_actor_comparison.py @@ -0,0 +1,124 @@ +"""Read-only actor restart diagnosis; never replaces or upgrades the frozen verdict.""" + +import argparse +import hashlib +import json +import math +from pathlib import Path +import struct +import uuid + +import nbtlib + +import native117_machine_operations as frozen + + +CASES = {"steam/extra-explosive-0", "steam/extra-explosive-1"} +SOURCES_SHA256 = "9175F1245E5E2907EA3ECAB13DC40F9B8994E61759E75363F6FB14322335F013" + + +def differences(before, after, path="$"): + if isinstance(before, dict) and isinstance(after, dict): + result = [] + for key in sorted(set(before) | set(after)): + name = path + "." + key + if key not in before or key not in after: + result.append(dict(path=name, before=before.get(key), after=after.get(key), + beforePresent=key in before, afterPresent=key in after)) + else: + result.extend(differences(before[key], after[key], name)) + return result + if (isinstance(before, list) and isinstance(after, list) and len(before) == len(after) == 3 + and before[0] == after[0] == "List" and before[1] == after[1] + and len(before[2]) == len(after[2])): + return [delta for index, (a, b) in enumerate(zip(before[2], after[2])) + for delta in differences(a, b, path + "[" + str(index) + "]")] + return [] if before == after else [dict(path=path, before=before, after=after)] + + +def actors(case): + result = {} + for row in case.get("entities", []): + identity = str(uuid.UUID(row["uuid"])) + if identity != row["uuid"] or identity in result: + raise AssertionError("Duplicate or noncanonical native actor UUID") + tag = nbtlib.parse_nbt(row["nbt"]) + if not isinstance(tag.get("UUID"), nbtlib.IntArray) or len(tag["UUID"]) != 4: + raise AssertionError("Missing native actor UUID array") + actual = uuid.UUID(bytes=struct.pack(">4i", *(int(part) for part in tag["UUID"]))) + if str(actual) != identity: + raise AssertionError("Native actor metadata and saved UUID disagree") + result[identity] = tag + return result + + +def stationary_cows(case_id, before, after): + return case_id in CASES and all( + isinstance(tag.get("id"), nbtlib.String) and str(tag["id"]) == "minecraft:cow" + and type(tag.get("NoAI")) is nbtlib.Byte and int(tag["NoAI"]) == 1 + and type(tag.get("NoGravity")) is nbtlib.Byte and int(tag["NoGravity"]) == 1 + for tag in (before, after)) + + +def explanation(case_id, before, after, delta): + if not stationary_cows(case_id, before, after): + return None + for index in range(3): + if delta["path"] != "$.Motion[" + str(index) + "]": + continue + a, b = before.get("Motion"), after.get("Motion") + if (isinstance(a, nbtlib.List) and isinstance(b, nbtlib.List) + and a.subtype is b.subtype is nbtlib.Double and len(a) == len(b) == 3 + and all(math.isfinite(float(value)) for value in (*a, *b)) + and abs(float(a[index])) > 10.0 and float(b[index]) == 0.0): + return "Entity.load: only this >10 native velocity component is clamped to zero" + if delta["path"] == "$.HurtByTimestamp": + if (type(before.get("HurtByTimestamp")) is nbtlib.Int + and type(after.get("HurtByTimestamp")) is nbtlib.Int + and int(before["HurtByTimestamp"]) >= 0 and int(after["HurtByTimestamp"]) == 0 + and all(key not in tag for tag in (before, after) + for key in ("last_hurt_by_mob", "ticks_since_last_hurt_by_mob"))): + return "LivingEntity.readAdditionalSaveData: absent relative hurt key rebases to unticked zero" + return None + + +def diagnose(fixture, inspected): + # Recheck exact selected native load methods before explaining any transition. + from native262_operations import native_load_sources + native_load_sources() + verdict = frozen.validate_restart(fixture, inspected) + actual = {row["id"]: row for row in inspected["cases"]} + observations = [] + for case in fixture["cases"]: + before, after = actors(case), actors(actual[case["id"]]) + for identity in sorted(set(before) | set(after)): + if identity not in before or identity not in after: + observations.append(dict(case=case["id"], uuid=identity, path="$", + beforePresent=identity in before, afterPresent=identity in after, + explanation=None)) + continue + for delta in differences(frozen.saved_actor_nbt(before[identity]), frozen.saved_actor_nbt(after[identity])): + observations.append(dict(case=case["id"], uuid=identity, **delta, + explanation=explanation(case["id"], before[identity], after[identity], delta))) + unknown = sum(row["explanation"] is None for row in observations) + return dict(diagnosticOnly=True, frozenRestartResult=verdict, actorDifferences=observations, + unexplainedActorDifferences=unknown, primarySourcesSha256=SOURCES_SHA256, + scope="Explains exact native load transitions only; no NBT writes or passing reclassification") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("runtime", type=Path) + args = parser.parse_args() + paths = [args.runtime / "machine-operation-fixture.json", + args.runtime / "advantage-works-results/machine-inspection-restarted.json"] + data = [path.read_bytes() for path in paths] + report = diagnose(*(json.loads(value) for value in data)) + report["inputSha256"] = {str(path.resolve()): hashlib.sha256(value).hexdigest().upper() + for path, value in zip(paths, data)} + print(json.dumps(report, indent=2, sort_keys=True)) + return int(report["frozenRestartResult"]["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native262_actor_fixtures.py b/test-rig/migration/native262_actor_fixtures.py new file mode 100644 index 00000000..fdd3b595 --- /dev/null +++ b/test-rig/migration/native262_actor_fixtures.py @@ -0,0 +1,152 @@ +"""Explicit derived fixture copies: native-owned FML config authority and legal battery slots.""" + +import argparse +import base64 +import copy +import gzip +import io +from pathlib import Path +import shutil + +import nbtlib +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 native262 as native +import native262_actors as actors + +BATTERIES = {namespace + ":" + item for namespace in ("electricadvantage", "baseelectric") + for item in ("lithium_battery", "lead_acid_battery", "alkaline_battery", "nickel_hydride_battery")} + + +def legal_ender_batteries(player): + """Split only the four known artificial overstacked witnesses, retaining every item and tag.""" + inventory = player.get("EnderItems", ()) + slots = [int(item["Slot"]) for item in inventory] + if len(slots) != len(set(slots)) or any(not 0 <= slot < 27 for slot in slots): + raise AssertionError("Malformed historical EnderItems slots") + free = [slot for slot in range(27) if slot not in slots] + required = 0 + for item in inventory: + key = "Count" if "Count" in item else "count" + count = int(item[key]) + if str(item["id"]) in BATTERIES and count > 1: + if count != 3 or ("Count" in item and "count" in item): + raise AssertionError("Unreviewed battery overstack") + required += 2 + if len(free) < required: + raise AssertionError("Insufficient free EnderItems slots") + changes, appended = [], [] + for item in inventory: + key = "Count" if "Count" in item else "count" + count = int(item[key]) + if str(item["id"]) not in BATTERIES or count <= 1: + continue + original = copy.deepcopy(item) + item[key] = type(item[key])(1) + destinations = [int(item["Slot"])] + for _ in range(2): + sibling = copy.deepcopy(item) + destination = free.pop(0) + sibling["Slot"] = type(item["Slot"])(destination) + appended.append(sibling) + destinations.append(destination) + for sibling in (item, *appended[-2:]): + old_payload = {key: value for key, value in original.items() if key not in ("Slot", "Count", "count")} + new_payload = {key: value for key, value in sibling.items() if key not in ("Slot", "Count", "count")} + if old_payload != new_payload: + raise AssertionError("Fixture item metadata changed during legal splitting") + changes.append(dict(id=str(item["id"]), fromSlot=int(item["Slot"]), toSlots=destinations, preservedTotal=count)) + if appended: + inventory.extend(appended) + return changes + + +def derive(source, receipt, checksum, output, kind, legalize=False): + native.validate_hash(receipt, checksum) + approval = native.read_json(receipt) + before = fingerprint(source) + if Path(approval["source"]).resolve() != source or before != approval["sourceFingerprint"]: + raise AssertionError("Protected fixture differs from its explicit source receipt") + output = output_path(output) + require_disjoint(output, source) + require_disjoint(output, receipt) + output.mkdir(parents=True) + world_before = fingerprint(source / WORLD) + shutil.copytree(source / WORLD, output / WORLD) + config_evidence = output / "config-overlay-evidence" + config_evidence.mkdir() + for path in (source / "config").rglob("*"): + if not path.is_file(): + continue + relative = path.relative_to(source / "config") + target = config_evidence / "historical-fml.toml" if relative.as_posix() == "fml.toml" else output / "config" / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, target) + name = "dimension" if kind == "dimensions" else "actor" + for filename in (name + "-fixture.json", "migration-fixture.json"): + shutil.copy2(source / filename, output / filename) + prefix = "dimension-semantic-" if kind == "dimensions" else "semantic-" + label = next(label for label in ("runtime-restart", "restarted", "source") if (source / (prefix + label + ".json")).is_file()) + record_name = ("dimensions-" if kind == "dimensions" else "actors-") + label + ".json" + record = native.read_json(source / record_name) + definition = native.read_json(source / (name + "-fixture.json")) + changes = {} + if legalize: + player_path = output / WORLD / "playerdata" / (definition["playerUuid"] + ".dat") + player = nbtlib.load(player_path) + edits = legal_ender_batteries(player) + if edits: + player.save() + changes["loosePlayer"] = edits + if record.get("playerAdapter"): + raw = nbtlib.File.parse(io.BytesIO(gzip.decompress(base64.b64decode(record["player"]["nbtBase64"], validate=True)))) + edits = legal_ender_batteries(raw) + if edits: + data = io.BytesIO() + raw.write(data) + record["player"]["nbtBase64"] = base64.b64encode(gzip.compress(data.getvalue(), mtime=0)).decode() + changes["serializerPlayer"] = edits + level_path = output / WORLD / "level.dat" + level = nbtlib.load(level_path) + if "Player" in level["Data"]: + edits = legal_ender_batteries(level["Data"]["Player"]) + if edits: + level.save() + changes["embeddedPlayer"] = edits + write_json(output / record_name, record) + capture = bind(capture_dimensions, semantic=actors.player_semantic) if kind == "dimensions" else capture_actors + write_json(output / (prefix + label + ".json"), capture(output, label)) + world_after = fingerprint(output / WORLD) + changed = [key for key in world_before if world_before[key] != world_after.get(key)] + allowed = {"level.dat", str(Path("playerdata") / (definition["playerUuid"] + ".dat"))} + if set(world_before) != set(world_after) or not set(changed) <= allowed or (not legalize and changed): + raise AssertionError("Unexpected world change in derived fixture") + if fingerprint(source) != before: + raise AssertionError("Protected source changed during derivation") + retained_fml = config_evidence / "historical-fml.toml" + report = dict(source=str(output), sourceFingerprint=fingerprint(output), originalSource=str(source), + originalReceiptSha256=checksum, originalUnchanged=True, exactWorldFileChanges=changed, + legalBatterySplits=changes, kind=kind, + configOverlay=dict(owner="Forge65 native262.stage", authority="native target fml.toml; no historical config overwrite", + historicalFmlRetainedSha256=digest(retained_fml) if retained_fml.exists() else None, + nativeFmlContent="versionCheck = false\n"), + scope="Explicit derived fixture, not a new old-engine serializer qualification") + target_receipt = output.with_name(output.name + "-receipt.json") + if target_receipt.exists(): + raise FileExistsError(target_receipt) + write_json(target_receipt, report) + return target_receipt + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + for name in ("source", "receipt", "output"): + parser.add_argument("--" + name, type=lambda value: Path(value).resolve(), required=True) + parser.add_argument("--receipt-sha256", required=True) + parser.add_argument("--kind", choices=("actors", "dimensions"), required=True) + parser.add_argument("--legal-batteries", action="store_true") + args = parser.parse_args() + result = derive(args.source, args.receipt, args.receipt_sha256, args.output, args.kind, args.legal_batteries) + print(result) + print(digest(result)) diff --git a/test-rig/migration/native262_actor_transformer_order.py b/test-rig/migration/native262_actor_transformer_order.py new file mode 100644 index 00000000..2e3c081e --- /dev/null +++ b/test-rig/migration/native262_actor_transformer_order.py @@ -0,0 +1,330 @@ +"""Actual two-order actor proof with source-numeric enchantments; never production qualification.""" + +import argparse +import base64 +import copy +from datetime import datetime, timezone +from functools import partial +import gzip +import io +import json +from pathlib import Path +from types import SimpleNamespace +import traceback + +import nbtlib + +from matrix import WORLD, digest, fingerprint, write_json +from native117 import output_path, require_disjoint +from native117_actor_io import bind +from native117_identity import target_id +import native262 as native +import native262_actors as actors +import native262_transformer_order as order +import native262_upgrade as upgrade + + +SCOPE = "INSTRUMENTED_ACTOR_PLAYER_ENTITY_ORDER_PROOF" +WITNESSES = ("cart", "dropped-musket") +PHASES = ("converted", "adapter", "restarted", "runtime", "runtime-restart") +STOP_PHASES = ("conversion", "restart", "runtime-restart") +ENCHANTMENTS = {10: "basesteam:high_explosive", 11: "basesteam:powderless", + 12: "basesteam:rapid_reload", 13: "basesteam:recoil"} +MUTATIONS = ("curse", "missing", "extra", "level", "count", "wear", "payload") + + +def item(entity, witness, *, native_format): + if witness == "cart": + values = entity.get("Items", ()) + matches = [value for value in values if str(value.get("id", "")).endswith(":musket")] + if len(matches) != 1 or matches[0].get("Slot") != 1: + raise AssertionError("Missing/duplicate cart musket at its source slot") + result = matches[0] + else: + result = entity.get("Item", {}) + if not str(result.get("id", "")).endswith(":musket"): + raise AssertionError("Missing " + witness + " musket") + if native_format and result.get("id") != "basesteam:musket": + raise AssertionError("Native musket identity was not preserved") + return result + + +def exact_value(expected, observed): + if type(expected) is not type(observed): + return False + if isinstance(expected, dict): + return expected.keys() == observed.keys() and all(exact_value(value, observed[key]) for key, value in expected.items()) + if isinstance(expected, list): + return len(expected) == len(observed) and all(exact_value(left, right) for left, right in zip(expected, observed)) + return expected == observed + + +def source_context(level, record, expected): + version = level.get("Data", {}).get("DataVersion") + if type(version) is not nbtlib.Int or int(version) != 512: + raise AssertionError("Context regression requires the approved raw original110 schema") + registry = level.get("FML", {}).get("Registries", {}).get("minecraft:enchantments", {}).get("ids", ()) + saved = {} + for entry in registry: + number = entry.get("V") + if type(number) is not nbtlib.Int or int(number) in saved or type(entry.get("K")) is not nbtlib.String: + raise AssertionError("Ambiguous saved Forge enchantment index") + saved[int(number)] = str(entry["K"]) + if {number: target_id(saved.get(number, "")) for number in ENCHANTMENTS} != ENCHANTMENTS: + raise AssertionError("Saved IDs10..13 do not name the four Steam enchantments") + rows = record.get("entities", []) + result = {} + for witness in WITNESSES: + matches = [row for row in rows if row.get("witness") == witness] + if len(matches) != 1: + raise AssertionError("Missing/duplicate raw source witness: " + witness) + raw = gzip.decompress(base64.b64decode(matches[0]["nbtBase64"], validate=True)) + stream = io.BytesIO(raw) + entity = nbtlib.File.parse(stream) + if stream.read(1): + raise AssertionError("Trailing raw source entity data") + stack = item(entity, witness, native_format=False) + entries = stack.get("tag", {}).get("ench", ()) + if len(entries) != 4 or any(type(row.get("id")) is not nbtlib.Short + or type(row.get("lvl")) is not nbtlib.Short or int(row["lvl"]) != 1 for row in entries): + raise AssertionError("Source enchantments must remain four numeric short IDs at level1") + ids = [int(row["id"]) for row in entries] + if len(set(ids)) != 4 or set(ids) != set(ENCHANTMENTS): + raise AssertionError("Source numeric enchantment witnesses were changed") + goal = item(expected["entities"][witness], witness, native_format=True) + if goal.get("components", {}).get("minecraft:enchantments") != dict.fromkeys(ENCHANTMENTS.values(), 1): + raise AssertionError("Historical oracle disagrees with the independently saved registry") + result[witness] = dict(numericIds=ids, savedNames=[saved[number] for number in ids], levels=[1] * 4, + expectedStack=copy.deepcopy(goal)) + return dict(sourceDataVersion=512, savedForgeIds=saved, witnesses=result, targetObservationsUsed=False) + + +def preflight(args): + source = Path(args.fixture).resolve(strict=True) + upgrade.validate_hash(args.fixture_receipt, args.fixture_receipt_sha256) + approved = upgrade.read_json(args.fixture_receipt) + before = fingerprint(source) + if Path(approved["source"]).resolve() != source or approved["sourceFingerprint"] != before: + raise AssertionError("Actor fixture differs from its approved independent receipt") + expected, label = actors.baseline(source, "actors") + context = source_context(nbtlib.load(source / WORLD / "level.dat"), + upgrade.read_json(source / ("actors-" + label + ".json")), expected) + if fingerprint(source) != before: + raise AssertionError("Read-only source preflight changed its fixture") + return dict(source=str(source), fixtureReceipt=str(args.fixture_receipt), + fixtureReceiptSha256=args.fixture_receipt_sha256.upper(), + sourceFingerprint=before, sourceLabel=label, context=context) + + +def assert_witnesses(expected, observed): + result = {} + for witness in WITNESSES: + if witness not in observed.get("entities", {}): + raise AssertionError("Missing native witness: " + witness) + actual = item(observed["entities"][witness], witness, native_format=True) + before = expected["witnesses"][witness]["expectedStack"] + if not exact_value(before, actual): + raise AssertionError("Exact native musket payload changed: " + witness + ": " + repr(actual)) + result[witness] = copy.deepcopy(actual) + return result + + +def negative_controls(expected, observed): + assert_witnesses(expected, observed) + results = {"baseline": "PASS"} + for witness in WITNESSES: + for mutation in MUTATIONS: + changed = copy.deepcopy(observed) + stack = item(changed["entities"][witness], witness, native_format=True) + components = stack["components"] + levels = components["minecraft:enchantments"] + first = next(iter(levels)) + if mutation == "curse": components["minecraft:enchantments"] = {"minecraft:binding_curse": 1} + elif mutation == "missing": del levels[first] + elif mutation == "extra": levels["minecraft:unbreaking"] = 1 + elif mutation == "level": levels[first] = 2 + elif mutation == "count": stack["count"] += 1 + elif mutation == "wear": components["minecraft:damage"] += 1 + elif mutation == "payload": components.setdefault("minecraft:custom_data", {})["order_probe_mutation"] = 1 + try: + assert_witnesses(expected, changed) + except AssertionError: + results[witness + "/" + mutation] = "REJECTED" + else: + raise AssertionError("Actor witness mutation accepted: " + witness + "/" + mutation) + return dict(status="PASS", scope="DERIVED_IN_MEMORY_NATIVE_ACTOR_WITNESS_NEGATIVES", + qualificationAllowed=False, controls=results, sourceOrWorldFilesMutated=False) + + +def raw_receipt(path): + raw = upgrade.read_json(path) + if raw.get("status") != "PASS" or type(raw.get("matchedEntities")) is not int or raw["matchedEntities"] < 2: + raise AssertionError("Missing complete, nonempty stopped entity audit: " + str(path)) + return dict(file=str(path), sha256=digest(path), **raw) + + +def all_orders_proven(primary, modes): + controls = {"baseline": "PASS", **{witness + "/" + mutation: "REJECTED" + for witness in WITNESSES for mutation in MUTATIONS}} + return (primary.get("status") == "PASS" and set(modes) == set(order.MODES) + and all(mode.get("status") == "PASS" and mode.get("sourceUnchanged") is True + and set(mode.get("witnessChecks", {})) == set(PHASES) + and all(row.get("status") == "PASS" for row in mode["witnessChecks"].values()) + and set(mode.get("rawAudits", {})) == set(STOP_PHASES) + and all(row.get("status") == "PASS" for row in mode["rawAudits"].values()) + and mode.get("entityControls", {}).get("controls") == controls for mode in modes.values())) + + +def run_actor_campaign(args, *, server_type, stage_function, hook_only=False): + if hook_only is not False: + raise ValueError("Actor order proof always requires all three native actor boots") + approved = preflight(args) + target = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + java, _ = native.java25(args.java) + output = Path(args.output) + output.mkdir(parents=True, exist_ok=False) + runtime = output / "server" + report = output / "native262-actor-order-campaign.json" + result = dict(status="RUNNING", scope=SCOPE, qualificationAllowed=False, + profileSha256=digest(args.profile), sourceContext=approved["context"], + checks={}, witnessChecks={}, rawAudits={}, entityControls=dict(status="NOT_RUN")) + write_json(report, result) + expected_jars = {} + + def stage(destination, actual_target, port): + expected_jars.update(stage_function(destination, *actual_target, port)) + return native.LAUNCHER + + class CheckedServer(server_type): + def __init__(self, *values): + native.verify_runtime(runtime, target[1], expected_jars) + super().__init__(*values) + + def close(self): + failure = None + try: + super().close() + except BaseException as error: + failure = error + try: + native.verify_runtime(runtime, target[1], expected_jars) + except BaseException as audit: + if failure is None: + raise + failure.add_note("Pinned seven-jar audit also failed: " + repr(audit)) + if failure is not None: + raise failure + + values = SimpleNamespace(**vars(args)) + values.target_manifest, values.java17, values.kind = args.profile, java, "actors" + try: + result["checks"] = actors.run_route(Path(args.fixture), runtime, target, values, stage=stage, + server_type=CheckedServer, stop_audit=partial(actors.audit_stopped, + minecraft_version=native.MINECRAFT, data_version=4903, stopping_phase="ServerStoppingEvent"), + check_log=native.require_clean_log, minecraft=native.MINECRAFT) + for label in PHASES: + path = runtime / ("semantic-" + label + ".json") + try: + observed = upgrade.read_json(path) + stacks = assert_witnesses(approved["context"], observed) + result["witnessChecks"][label] = dict(status="PASS", file=str(path), sha256=digest(path), stacks=stacks) + except (AssertionError, OSError, ValueError) as error: + result["witnessChecks"][label] = dict(status="FAIL", file=str(path), error=repr(error)) + for label in STOP_PHASES: + path = runtime / "stopped-entity-evidence" / label / "receipt.json" + result["rawAudits"][label] = raw_receipt(path) + failures = [key for key, value in result["checks"].items() if value.get("status") != "PASS"] + failures.extend("witness-" + key for key, value in result["witnessChecks"].items() if value["status"] != "PASS") + if not result["checks"] or failures: + raise AssertionError("Actor order payload failed: " + repr(failures) + "; retained " + str(report)) + result["entityControls"] = negative_controls(approved["context"], upgrade.read_json(runtime / "semantic-converted.json")) + result["status"] = "PASS" + except BaseException as error: + result.update(status="FAIL", error=repr(error), traceback=traceback.format_exc()) + checkpoint = runtime / "native117-comparison.json" + if checkpoint.is_file(): result["checks"] = upgrade.read_json(checkpoint) + raise + finally: + result["sourceUnchanged"] = fingerprint(Path(args.fixture)) == approved["sourceFingerprint"] + if not result["sourceUnchanged"]: result["status"] = "FAIL" + write_json(report, result) + return int(result["status"] != "PASS") + + +def run(args): + args.source, args.legacy_reference, args.hook_only = args.fixture, None, False + approved = preflight(args) + root = output_path(args.output) + for path in (args.fixture, args.fixture_receipt, args.profile, args.seed, args.inventory, args.build_receipt, args.pre_run_seal): + require_disjoint(root, path) + for path in (args.fixture, args.fixture_receipt, args.profile, args.seed, args.inventory, args.build_receipt): + require_disjoint(args.pre_run_seal, path) + upgrade.validate_hash(args.build_receipt, args.build_receipt_sha256) + build = upgrade.read_json(args.build_receipt) + target = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + java, _ = native.java25(args.java) + files = order.validate_build(build, args.profile, java) + files.update(upgrade.oracle_closure()) + files.update({Path(args.fixture_receipt): digest(args.fixture_receipt), Path(args.build_receipt): digest(args.build_receipt), + Path(args.profile): digest(args.profile), Path(args.inventory): digest(args.inventory), java: digest(java)}) + files.update({Path(path): sha for _, path, sha in target[2].values()}) + seed_before = fingerprint(Path(args.seed)) + with Path(args.pre_run_seal).open("x", encoding="utf-8") as output: + json.dump(dict(scope="GENUINE_PRE_RUN_ACTOR_INCLUSIVE_ORDER_CUSTODY", createdUtc=datetime.now(timezone.utc).isoformat(), + setupNotStarted=True, qualificationAllowed=False, source=approved, seedFingerprint=seed_before, + files={str(path): sha for path, sha in files.items()}, modes=list(reversed(order.MODES)) if args.bm_first else order.MODES), + output, indent=2, sort_keys=True) + seal_sha = digest(args.pre_run_seal) + facade = SimpleNamespace(**{**vars(upgrade), "SCOPE": SCOPE, "read_source": preflight, + "run": run_actor_campaign, "oracle_closure": lambda: {**upgrade.oracle_closure(), + Path(args.fixture_receipt): args.fixture_receipt_sha256.upper()}}) + campaign = bind(order.run, upgrade=facade, MODES=tuple(reversed(order.MODES)) if args.bm_first else order.MODES) + exit_code = 1 + proven = False + try: + order.verify_files(files) + exit_code = campaign(args) + finally: + if root.is_dir(): + base = root / "native262-transformer-order-results.json" + primary = upgrade.read_json(base) if base.is_file() else dict(status="FAIL", error="Missing order receipt") + modes = {} + for mode in order.MODES: + path = root / mode / "native262-actor-order-campaign.json" + if path.is_file(): modes[mode] = dict(file=str(path), sha256=digest(path), **upgrade.read_json(path)) + custody_failure = None + try: + order.verify_files({**files, Path(args.pre_run_seal): seal_sha}) + if fingerprint(Path(args.seed)) != seed_before or fingerprint(Path(args.fixture)) != approved["sourceFingerprint"]: + raise AssertionError("Pre-run protected tree changed") + except Exception as error: + custody_failure = repr(error) + proven = custody_failure is None and all_orders_proven(primary, modes) + write_json(root / "native262-actor-order-results.json", dict(status="PASS" if proven else "FAIL", scope=SCOPE, + qualificationAllowed=False, profileSha256=digest(args.profile), preRunSeal=str(args.pre_run_seal), + preRunSealSha256=seal_sha, preRunCustodyFailure=custody_failure, sourceContext=approved["context"], + orderReceipt=str(base), orderReceiptSha256=digest(base) if base.is_file() else None, actorCampaigns=modes, + finalBothOrdersProven=proven)) + return exit_code or int(not proven) + + +def parser(): + cli = argparse.ArgumentParser(description=__doc__) + for name in ("fixture", "fixture-receipt", "profile", "seed", "inventory", "java", "build-receipt", "output", "pre-run-seal"): + cli.add_argument("--" + name, type=lambda value: Path(value).resolve(), required=True) + for name in ("fixture-receipt-sha256", "inventory-sha256", "build-receipt-sha256"): + cli.add_argument("--" + name, required=True) + cli.add_argument("--port", type=int, required=True) + cli.add_argument("--bm-first", action="store_true", help="Run actual BM-first priority mode first; failures remain failures") + return cli + + +def main(): + cli = parser() + args = cli.parse_args() + if not 26450 <= args.port <= 65534: cli.error("Require two assigned consecutive disposable ports") + return run(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native262_actors.py b/test-rig/migration/native262_actors.py new file mode 100644 index 00000000..b222a712 --- /dev/null +++ b/test-rig/migration/native262_actors.py @@ -0,0 +1,738 @@ +"""Sealed historical actor routes on Forge65 with explicit native target storage.""" + +import argparse +import builtins +import copy +from functools import partial +import math +from pathlib import Path +import shutil +import socket +import traceback +import uuid +import json +import sys +from types import SimpleNamespace + +import nbtlib + +import actors +import dimensions +from matrix import WORLD, digest, fingerprint, plain, write_json +from native117 import output_path, require_disjoint +from native117_actor_io import bind, capture_actors, capture_dimensions, semantic as historical_semantic +import native117_actors as historical +from native117_dimension_io import parse_dimension_nbt, project_entity +from native117_format import DIMENSIONS, dimension_id +import native117_entity_storage as entity_storage +import native117_helper_contract as helper_contract +import native117_stopped_entities as stopped +import native262 as native +import native262_layout as layout + + +DATA_VERSION = 4903 +HELPER_CONTRACT_SHA = "272085E5730D6C95525017714BC496BC6AF358F35ED6FDF8678A428F740D4EF5" +DIMENSION_PATHS = {key: (number, "dimensions/" + key.replace(":", "/")) + for key, number in zip(layout.VANILLA_DIMENSIONS, (0, -1, 1))} +PLAYER_EQUIPMENT_SLOTS = {100: "feet", 101: "legs", 102: "chest", 103: "head", -106: "offhand"} +NATIVE_ATTRIBUTE_ALIASES = { + "minecraft:" + name: "minecraft:generic." + name + for name in ("max_health", "knockback_resistance", "movement_speed", "armor", + "armor_toughness", "follow_range", "attack_damage") +} +NATIVE_ATTRIBUTE_ALIASES["minecraft:jump_strength"] = "minecraft:horse.jump_strength" +BOAT_VARIANTS = frozenset(("oak", "spruce", "birch", "jungle", "acacia", "cherry", "dark_oak", "mangrove", "bamboo")) +WOLF_CONTROL_HASHES = { + 512: "74BD836164A474FF95F180B147442BC3E2D7C7754A0C4B2087E3AD456A1961F0", + 1343: "B04F792F3D2634CF3FA3ABD66881813BB6B7F39817988FBA6517C7C350BE437F", +} +WOLF_RUNTIME_SHA = "C3925901C27836E789E87FD8A680742A837BBD96FCF60BFA4A53FA2CC840F05C" +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 + 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 source_components(state, version): + if version in (3839, 3955): + from native12111_upgrade import expected_components as modern_components + return modern_components(state, source_version={3839: "1.20.6", 3955: "1.21.1"}[version]) + if version >= 3839: + raise ValueError("Unreviewed native component player source version: " + str(version)) + return expected_components(state) + + +def baseline(source, kind): + source_baseline = bind(historical.baseline, + capture_dimensions=bind(capture_dimensions, semantic=player_semantic)) + state, label = source_baseline(source, kind) + definition = native.read_json(source / ("dimension-fixture.json" if kind == "dimensions" else "actor-fixture.json")) + player = nbtlib.load(source / WORLD / "playerdata" / (definition["playerUuid"] + ".dat")) + result = source_components(state, int(player.get("DataVersion", -1))) + level = nbtlib.load(source / WORLD / "level.dat")["Data"] + host = level.get("Player") + if host is not None: + if "UUID" in host: + authority = str(layout.uuid_tag(host["UUID"])) + else: + authority = str(uuid.UUID(int=((int(host["UUIDMost"]) & ((1 << 64) - 1)) << 64) + | (int(host["UUIDLeast"]) & ((1 << 64) - 1)))) + if authority != definition["playerUuid"]: + raise AssertionError("Source embedded host differs from the sealed fixture player") + result["hostAuthority"] = authority + else: + result["hostAuthority"] = None + return result, label + + +def expected_player_equipment(player): + """Apply native PlayerEquipmentFix's exact placement to expectations only.""" + result = copy.deepcopy(player) + if "Inventory" not in result: + return result + inventory = result["Inventory"] + if not isinstance(inventory, list): + raise ValueError("Malformed expected player inventory") + equipment = result.get("equipment", {}) + if not isinstance(equipment, dict): + raise ValueError("Malformed expected player equipment") + slots, remaining, moved = set(), [], {} + for item in inventory: + if not isinstance(item, dict) or type(item.get("Slot")) is not int: + raise ValueError("Missing or malformed expected inventory Slot") + slot = item["Slot"] + if slot in slots: + raise ValueError("Duplicate expected inventory Slot") + slots.add(slot) + name = PLAYER_EQUIPMENT_SLOTS.get(slot) + if name is None: + remaining.append(item) + else: + if "equipment" in result or not isinstance(item.get("id"), str) or type(item.get("count")) is not int or item["count"] <= 0: + raise ValueError("Ambiguous or malformed expected legacy equipment") + moved[name] = {key: value for key, value in item.items() if key != "Slot"} + result["Inventory"] = remaining + if moved: + result["equipment"] = moved + return result + + +def expected_state(value, kind, survival=False, adapter=True): + result = historical.expected_state(value, kind, survival, adapter) + # Conversion leaves the unopened loose file historical. Level.dat host data + # has already undergone DFU, and every read phase loads/saves the loose player. + for container in ("file", "adapter", "embedded"): + if container in result and (container != "file" or adapter): + result[container] = expected_player_equipment(result[container]) + if kind == "dimensions": + result[container] = expected_player_respawn(result[container]) + if kind == "dimensions": + for entity in result.get("entities", {}).values(): + expected_dimension_entity(entity) + # Native26 replaces embedded host NBT with explicit singleplayer_uuid authority. + # Its identity is checked separately from the existing loose-file payload. + result.pop("embedded", None) + return result + + +def expected_player_respawn(player): + result = copy.deepcopy(player) + fields = ("SpawnX", "SpawnY", "SpawnZ", "SpawnDimension", "SpawnAngle", "SpawnForced") + if not any(key in result for key in fields): + return result + if "respawn" in result or not all(type(result.get(key)) is int for key in fields[:3]): + raise ValueError("Incomplete or ambiguous expected respawn position") + yaw = result.get("SpawnAngle", 0.0) + forced = result.get("SpawnForced", 0) + if (type(yaw) not in (int, float) or not math.isfinite(yaw) + or type(forced) not in (int, bool) or forced not in (0, 1)): + raise ValueError("Malformed expected respawn angle or forced flag") + respawn = dict(pos=[result[key] for key in fields[:3]], + dimension=DIMENSIONS[dimension_id(result.get("SpawnDimension", 0))], yaw=yaw, pitch=0.0) + if forced: + respawn["forced"] = 1 + for key in fields: + result.pop(key, None) + result["respawn"] = respawn + return result + + +def expected_dimension_entity(entity): + if entity.get("id") in ("minecraft:boat", "minecraft:chest_boat"): + variant = entity.get("Type", "oak") + if variant not in BOAT_VARIANTS: + raise ValueError("Unproven historical boat variant") + chest = entity["id"] == "minecraft:chest_boat" + entity["id"] = "minecraft:" + variant + ("_chest" if chest else "") + ("_raft" if variant == "bamboo" else "_boat") + entity.pop("Type", None) + if entity.get("id") == "minecraft:horse": + moved = {} + for old, slot in (("SaddleItem", "saddle"), ("ArmorItem", "body")): + if old in entity: + if "equipment" in entity: + raise ValueError("Ambiguous expected horse equipment") + moved[slot] = entity.pop(old) + if moved: + entity["equipment"] = moved + for passenger in entity.get("Passengers", ()): + expected_dimension_entity(passenger) + + +def player_semantic(value, keys, registry): + if "SpawnX" in keys: + keys = (*keys, "SpawnAngle") + result = historical_semantic(value, keys, registry) + if "Inventory" in keys and "equipment" in value: + equipment = value["equipment"] + if not isinstance(equipment, dict): + raise ValueError("Malformed observed player equipment") + result["equipment"] = plain(equipment) + if "SpawnX" in keys and "respawn" in value: + if not isinstance(value["respawn"], dict): + raise ValueError("Malformed observed player respawn") + if any(key in value for key in ("SpawnX", "SpawnY", "SpawnZ", "SpawnDimension", "SpawnAngle", "SpawnForced")): + raise ValueError("Ambiguous observed native and historical respawn") + result["respawn"] = plain(value["respawn"]) + return result + + +def dimension_entity_state(value): + result = dimensions.entity_state(value) + if "equipment" in value: + if not isinstance(value["equipment"], dict): + raise ValueError("Malformed observed entity equipment") + if any(key in value for key in ("SaddleItem", "ArmorItem", "body_armor_item", "saddle", "ArmorItems", "HandItems")): + raise ValueError("Ambiguous observed native and historical entity equipment") + result["equipment"] = copy.deepcopy(value["equipment"]) + if "Passengers" in value: + result["Passengers"] = [dimension_entity_state(entry) for entry in value["Passengers"]] + return result + + +def leash_position(value): + if (len(value) != 3 or any(type(coordinate) is not int or not -(1 << 31) <= coordinate < (1 << 31) + for coordinate in value)): + raise ValueError("Expected the native leash knot's block-position int[3]") + return dict(zip(("X", "Y", "Z"), value)) + + +def dimension_entity_view(value): + if value.get("id") == "minecraft:horse": + if "leash" in value: + if "Leash" in value: + raise ValueError("Ambiguous saved horse leash") + value["Leash"] = leash_position(value.pop("leash")) + if "body_armor_item" in value: + if "ArmorItem" in value: + raise ValueError("Ambiguous saved horse armor") + value["ArmorItem"] = value.pop("body_armor_item") + for attribute in value.get("Attributes", ()): + if attribute["Name"] == "minecraft:generic.jump_strength": + if any(other["Name"] == "horse.jumpStrength" for other in value["Attributes"]): + raise ValueError("Ambiguous saved horse jump strength") + attribute["Name"] = "horse.jumpStrength" + for passenger in value.get("Passengers", ()): + dimension_entity_view(passenger) + return value + + +def native_dimension_nbt(entry): + parser = bind(parse_dimension_nbt, project_entity=lambda value, contexts: + project_entity(native_attribute_view(value), contexts)) + return dimension_entity_view(parser(entry)) + + +def native_attribute_view(value): + """Expose the new field spelling to the retained semantic oracle without changing values.""" + result = copy.deepcopy(value) + + def visit(entity): + if "attributes" in entity: + if "Attributes" in entity or not isinstance(entity["attributes"], list): + raise ValueError("Ambiguous native attribute list") + attributes, names = [], set() + for original in entity.pop("attributes"): + if not isinstance(original, dict) or not isinstance(original.get("id"), str): + raise ValueError("Malformed native attribute") + row = dict(original) + raw_name = row.pop("id") + name = NATIVE_ATTRIBUTE_ALIASES.get(raw_name, raw_name) + if name in names or "Name" in row or "Base" in row or "Modifiers" in row: + raise ValueError("Duplicate or ambiguous native attribute") + names.add(name) + row["Name"] = name + if "base" in row: + row["Base"] = row.pop("base") + if "modifiers" in row: + row["Modifiers"] = row.pop("modifiers") + attributes.append(row) + entity["Attributes"] = attributes + for passenger in entity.get("Passengers", ()): + visit(passenger) + visit(result) + return result + + +def capture_player_version(capture, runtime, label, evidence): + state = capture(runtime, label, evidence) + version = int(nbtlib.load(evidence / "player.dat").get("DataVersion", -1)) + if version != DATA_VERSION: + if label != "converted" or version >= DATA_VERSION: + raise AssertionError(f"{label}: native player file was not serialized as data version {DATA_VERSION}: {version}") + # The unopened loose file has not gone through PlayerDataStorage yet. + # Compare the retained source representation but keep its original file and version as evidence. + state["file"] = source_components(state["file"], version) + write_json(evidence / "player-format.json", dict(dataVersion=version, native=version == DATA_VERSION)) + return state + + +def retain(runtime, kind, label): + definition = native.read_json(runtime / ("dimension-fixture.json" if kind == "dimensions" else "actor-fixture.json")) + folder = runtime / ("dimension-evidence" if kind == "dimensions" else "actor-evidence") / label + folder.mkdir(parents=True, exist_ok=False) + world = runtime / WORLD + player = layout.player_file(world, definition["playerUuid"]) + # Never fall back to historical playerdata or an embedded Player observation. + shutil.copy2(player, folder / "player.dat") + shutil.copy2(world / "level.dat", folder / "level.dat") + level = layout.read_level(world) + if "Player" in level: + raise AssertionError("Native target retains ambiguous embedded host data") + authority = layout.singleplayer_uuid(world) + if authority is not None and str(authority) != definition["playerUuid"]: + raise AssertionError("Native target host UUID differs from fixture player") + capture = (bind(capture_dimensions, semantic=player_semantic, entity_state=dimension_entity_state, + parse_nbt=native_dimension_nbt) if kind == "dimensions" + else bind(capture_actors, semantic=player_semantic)) + state = capture_player_version(capture, runtime, label, folder) + state["hostAuthority"] = str(authority) if authority else None + if "embedded" in state: + raise AssertionError("Probe reports historical embedded player data on a native26 target") + if kind == "dimensions": + state = historical.native_dimension_state(state) + write_json(folder / "authority.json", dict(playerPath=str(player), singleplayerUuid=str(authority) if authority else None, + embeddedPlayerAbsent=True, playerFileSha256=digest(player))) + write_json(runtime / (("dimension-semantic-" if kind == "dimensions" else "semantic-") + label + ".json"), state) + return state + + +command = bind(historical.command, retain=retain) +release = bind(historical.release, command=command) + + +def helper_snapshot(record): + expected, rows, roots = entity_storage._snapshots(record) + for row in rows.values(): + if "leash" not in row: + continue + position = row["leash"] + if "Leash" in row or not isinstance(position, nbtlib.IntArray): + raise ValueError("Malformed or ambiguous native stopped leash") + if len(position) == 3: + row["Leash"] = nbtlib.Compound({key: nbtlib.Int(number) + for key, number in leash_position(list(map(int, position))).items()}) + elif len(position) != 4: + raise ValueError("Unexpected native stopped leash array length") + return expected, rows, roots + + +validate_helpers = bind(helper_contract.validate, native=SimpleNamespace( + _snapshots=helper_snapshot, _nbt=entity_storage._nbt, _decompress=entity_storage._decompress, + _identity=entity_storage._identity), CONTRACT_SHA=HELPER_CONTRACT_SHA, + POLICY="native262-leash-knot-nosave-v1") +helper_adapter = SimpleNamespace(CONTRACT="native262-helper-contract.json", LIFECYCLE=helper_contract.LIFECYCLE, + validate=validate_helpers, raw_helpers_absent=helper_contract.raw_helpers_absent) + + +def stop_audit_import(name, globals=None, locals=None, fromlist=(), level=0): + # Only this function's local helper import is adapted. The raw region audit + # still uses the original on-disk NBT and the module is never monkey-patched. + if level == 0 and name == "native117_helper_contract": + return helper_adapter + return builtins.__import__(name, globals, locals, fromlist, level) + + +def audit_entity_storage(snapshot_record, world_path, evidence_folder, *, data_version=DATA_VERSION, dimension=None): + """Frozen complete-NBT comparator, with an explicit native dimension key.""" + world = Path(world_path).resolve(strict=True) + key = dimension or snapshot_record.get("worldDimensionKey") + if key not in DIMENSION_PATHS or not world.as_posix().endswith("/" + DIMENSION_PATHS[key][1]): + raise ValueError("Expected an explicit native dimension path and key") + evidence = Path(evidence_folder).resolve() + require_disjoint(evidence, world) + evidence.mkdir(parents=True, exist_ok=False) + receipt = dict(format=1, status="FAIL", scope="EXACT_STOPPED_ENTITY_NBT_ONLY", world=str(world), + callerMustStopServer=True, files={}, sourceFiles={}, chunks={}, matched={}) + + def retain_raw(relative, data): + path = evidence / relative + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("xb") as output: + output.write(data) + receipt["files"][relative] = entity_storage._sha(data) + + try: + expected, rows, roots = entity_storage._snapshots(snapshot_record) + retain_raw("snapshot.json", (json.dumps(snapshot_record, sort_keys=True, indent=2) + "\n").encode()) + number = DIMENSION_PATHS[key][0] + for field in ("dimension", "worldDimensionKey"): + if field in snapshot_record and dimension_id(snapshot_record[field]) != number: + raise AssertionError("Snapshot dimension disagrees with the explicit native dimension") + source = world / "entities" + if source.is_symlink() or not source.is_dir(): + raise ValueError("Missing regular native entities directory") + paths = sorted(source.glob("*.mca")) + if not paths: + raise ValueError("No native entity regions found") + saved, root_chunks = {}, {} + for path in paths: + match = entity_storage.REGION_NAME.fullmatch(path.name) + if path.is_symlink() or not path.is_file() or match is None or path.stat().st_nlink != 1: + raise ValueError("Unsafe native entity region path") + rx, rz = map(int, match.groups()) + if path.name != "r.%d.%d.mca" % (rx, rz) or any(not -(1 << 26) <= n < (1 << 26) for n in (rx, rz)): + raise ValueError("Noncanonical native entity region coordinates") + data = path.read_bytes() + receipt["sourceFiles"][path.name] = entity_storage._sha(data) + retain_raw("regions/" + path.name, data) + for coordinates, payload in entity_storage._payloads(data, rx, rz): + relative = "chunks/%d.%d.chunk" % coordinates + retain_raw(relative, payload) + receipt["chunks"][relative] = dict(position=list(coordinates), sha256=entity_storage._sha(payload), source=path.name) + for root in entity_storage._entity_chunk(payload, coordinates, data_version): + root_id = entity_storage._identity(root) + if entity_storage._chunk(root) != coordinates: + raise AssertionError("Root entity saved Pos differs from its EntityStorage chunk") + root_chunks[root_id] = coordinates + for identity, tag, parent in entity_storage._tree(root): + if identity in saved: + raise AssertionError("Duplicate UUID in raw entity storage: " + identity) + if "Dimension" in tag and dimension_id(plain(tag["Dimension"].unpack())) != number: + raise AssertionError("Raw entity NBT dimension differs from explicit directory") + saved[identity] = dict(nbt=entity_storage._semantic(tag), root=root_id, chunk=coordinates) + for identity, entity in roots.items(): + if identity not in root_chunks or root_chunks[identity] != entity_storage._chunk(entity): + raise AssertionError("Snapshot root UUID absent from its actual saved Pos chunk: " + identity) + for identity, semantic in expected.items(): + if identity not in saved or saved[identity]["nbt"] != semantic: + raise AssertionError("Complete saved entity NBT differs from actual snapshot: " + identity) + entry = saved[identity] + receipt["matched"][identity] = dict(rootUuid=entry["root"], chunk=list(entry["chunk"]), + payload="chunks/%d.%d.chunk" % entry["chunk"], separatelyObserved=identity in rows) + if sorted(source.glob("*.mca")) != paths or any(entity_storage._sha(p.read_bytes()) != receipt["sourceFiles"][p.name] for p in paths): + raise AssertionError("Entity storage changed during stopped audit") + receipt.update(status="PASS", snapshotRecords=len(rows), matchedEntities=len(expected), rawEntities=len(saved), + unrelatedEntities=len(set(saved) - set(expected)), dimension=number, worldDimensionKey=key) + except Exception as error: + receipt["error"] = repr(error) + raise + finally: + write_json(evidence / "receipt.json", receipt) + return receipt + + +audit_stopped = bind(stopped.audit_stopped, DIMENSIONS=DIMENSION_PATHS, + entity_storage=SimpleNamespace(_snapshots=entity_storage._snapshots, audit=audit_entity_storage), + __builtins__=dict(vars(builtins), __import__=stop_audit_import)) + + +def verdict(oracle, before, after): + result = oracle(before, after) + findings = exact_items(before, after) + findings.extend(exact_native_maps(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 exact_native_maps(expected, actual, path=""): + findings = [] + if isinstance(expected, dict) and isinstance(actual, dict): + for key, default in (("equipment", {}), ("respawn", None)): + before, after = expected.get(key, default), actual.get(key, default) + if before != after: + findings.append(dict(path=f"{path}.{key}.exact-map", expected=before, actual=after)) + for key in expected.keys() & actual.keys(): + findings.extend(exact_native_maps(expected[key], actual[key], f"{path}.{key}")) + elif isinstance(expected, list) and isinstance(actual, list): + for index, (before, after) in enumerate(zip(expected, actual)): + findings.extend(exact_native_maps(before, after, f"{path}[{index}]")) + return findings + + +def assess_native(before, after, strict, proof, scope, adapter): + result = dict(status=strict["status"], rawStrictStatus=strict["status"], + rawStrictResult=copy.deepcopy(strict), nativeDifferences=[]) + if strict["status"] == "PASS" or proof is None: + return result + expected = copy.deepcopy(before) + wolf = expected.get("entities", {}).get("passenger-wolf", {}) + actual = after.get("entities", {}).get("passenger-wolf", {}) + maximum = wolf.get("Attributes", {}).get("generic.maxHealth", {}) + if (wolf.get("id") != "minecraft:wolf" or actual.get("id") != wolf["id"] + or wolf.get("Health") != 8 or maximum.get("Base") != 20 + or any(key not in wolf or actual.get(key) != wolf[key] + for key in ("UUIDMost", "UUIDLeast", "OwnerUUID"))): + return result + # These exact changes were independently reproduced by vanilla DFU and native + # Wolf load/save on a classpath that contains no Advantage classes. + controls = {} + runtime_path = Path(proof) / "control-runtime.json" + native.validate_hash(runtime_path, WOLF_RUNTIME_SHA) + runtime = native.read_json(runtime_path) + if (runtime.get("minecraftVersion") != native.MINECRAFT or runtime.get("targetDataVersion") != DATA_VERSION + or runtime.get("inheritedJvmEnvironmentAbsent") is not True + or runtime.get("javaSha256") != native.JAVA_SHA + or runtime.get("mainClass") != "NativeWolfPlatformControl" + or not any(row.get("sha256") == "7C324FAF36362EDB1E1879EA11536CEA7F6E58BE3A0E783C13607BE7D7364DD6" + for row in runtime.get("classpath", ()))): + raise ValueError("Historical wolf controls cannot qualify the native26 target") + controls["runtime"] = dict(path=str(runtime_path), sha256=WOLF_RUNTIME_SHA, observation=runtime) + for version, checksum in WOLF_CONTROL_HASHES.items(): + path = Path(proof) / f"legacy-{version}-receipt.json" + native.validate_hash(path, checksum) + observation = native.read_json(path) + if (observation.get("sourceDataVersion") != version or observation.get("targetDataVersion") != DATA_VERSION + or observation.get("noPower") is not True + or observation.get("uuidPreserved") is not True or observation.get("ownerPreserved") is not True + or [observation.get(key) for key in ("sourceHealth", "savedHealth", "sourceMaxHealth", "savedMaxHealth")] != [8, 16, 20, 40]): + raise ValueError("Historical wolf controls cannot qualify the native26 target") + controls[str(version)] = dict(path=str(path), sha256=checksum, observation=observation) + wolf["Health"] = 16 + maximum["Base"] = 40 + checked = verdict(dimensions.verdict, expected, after) + if checked["status"] == "PASS": + result.update(status="PASS_WITH_NATIVE_DIFFERENCE", allOtherFieldsStrictMatch=True, + controlledProof=controls, nativeDifferences=copy.deepcopy(strict["differences"])) + return result + + +def summarize_native(checks, assessments): + failures, native_changes = [], [] + for key, check in checks.items(): + if check["status"] == "PASS": + continue + assessment = assessments.get(key) + if (assessment is None or assessment.get("rawStrictResult") != check + or assessment.get("status") != "PASS_WITH_NATIVE_DIFFERENCE" + or assessment.get("allOtherFieldsStrictMatch") is not True): + failures.append(key) + else: + native_changes.append(dict(check=key, assessment=assessment)) + return dict(status="FAIL" if not checks or failures else + "PASS_WITH_NATIVE_DIFFERENCE" if native_changes else "PASS", + rawStrictStatus="FAIL" if native_changes or failures else "PASS", + failures=failures, nativeDifferences=native_changes) + + +def copy_fixture(source, runtime): + shutil.copytree(source / WORLD, runtime / WORLD) + shutil.copy2(source / "migration-fixture.json", runtime / "migration-fixture.json") + config = source / "config" + if config.is_dir(): + for item in config.rglob("*"): + destination = runtime / "config" / item.relative_to(config) + if item.is_file() and item.relative_to(config).as_posix() == "fml.toml": + retained = runtime / "historical-config-custody" / "fml.toml" + retained.parent.mkdir(parents=True, exist_ok=False) + shutil.copy2(item, retained) + continue + if item.is_file() and destination.exists(): + raise FileExistsError(f"Retained fixture config conflicts with native runtime: {destination}") + if item.is_file(): + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(item, destination) + + +run_route = bind(historical.run_route, baseline=baseline, expected_state=expected_state, command=command, release=release, + copy_fixture=copy_fixture, assess=assess_native, + summarize_checks=summarize_native, + native117_dimension_assessment=SimpleNamespace(__file__=__file__), + actors=SimpleNamespace(compare=partial(verdict, actors.compare), freeze_result=actors.freeze_result, + tick_result=actors.tick_result, backup_state=actors.backup_state), + dimensions=SimpleNamespace(verdict=partial(verdict, dimensions.verdict), + tick_result=dimensions.tick_result)) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("fixture", "fixture-receipt", "profile", "seed", "inventory", "java", "output"): + parser.add_argument("--" + name, type=lambda value: Path(value).resolve(), required=True) + parser.add_argument("--fixture-receipt-sha256", required=True) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--kind", choices=("actors", "dimensions"), required=True) + parser.add_argument("--native-wolf-proof", type=lambda value: Path(value).resolve(strict=True)) + parser.add_argument("--source-stopped-reference", type=lambda value: Path(value).resolve(strict=True)) + parser.add_argument("--source-stopped-reference-sha256") + parser.add_argument("--port", type=int, required=True) + parser.add_argument("--provisional", action="store_true", help="Diagnostic input profile, never final qualification") + 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) + profile_document = native.read_json(args.profile) + if profile_document.get("diagnosticScope") is not None and not args.provisional: + raise ValueError("A diagnostic profile requires explicit --provisional scope") + seed, inventory, mods = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + java, version = native.java25(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") + source_reference, reference_before, reference_helper, active_route = None, None, None, run_route + if args.source_stopped_reference is not None or args.source_stopped_reference_sha256 is not None: + if args.kind != "actors" or args.source_stopped_reference is None or args.source_stopped_reference_sha256 is None: + raise ValueError("Stopped source reference requires actors mode and explicit file plus SHA-256") + import native262_stopped_actor_reference as reference_helper + require_disjoint(output, args.source_stopped_reference.parent) + source_reference = reference_helper.validate(args.source_stopped_reference, args.source_stopped_reference_sha256, + source, args.fixture_receipt_sha256) + reference_before = fingerprint(args.source_stopped_reference.parent) + def stopped_baseline(actual_source, kind): + if actual_source.resolve() != source or kind != "actors": + raise AssertionError("Stopped source reference bound to a different route") + return copy.deepcopy(source_reference), "sealed-source-stopped-disk" + active_route = bind(run_route, baseline=stopped_baseline) + 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 / "native262-actors-results.json" + runner = Path(__file__) + closure = {Path(module.__file__).resolve(): digest(Path(module.__file__)) for module in tuple(sys.modules.values()) + if getattr(module, "__file__", None) and Path(module.__file__).suffix == ".py" + and Path(module.__file__).resolve().parent == Path(__file__).resolve().parent + and not Path(module.__file__).name.startswith("test_")} + 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={}, + preRunCustody=True, oracleClosure={str(path): sha for path, sha in closure.items()}, + provisional=args.provisional, finalQualificationAllowed=False, + scope="PROVISIONAL_CONTEXT_NOT_FINAL_ATLAS_QUALIFICATION" if args.provisional else "FROZEN_INPUT_ACTOR_ROUTE_NOT_MANUAL_ACCEPTANCE") + output.mkdir() + shutil.copy2(runner, output / runner.name) + if source_reference is not None: + result["stoppedSourceReference"] = dict(path=str(args.source_stopped_reference), + sha256=digest(args.source_stopped_reference), helperSha256=digest(Path(reference_helper.__file__)), + targetObservationsUsed=False, sealedBeforeTargetLoad=True) + shutil.copy2(Path(reference_helper.__file__), output / "stopped-reference-builder-source.py") + 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"] = active_route( + source, runtime, (seed, inventory, mods), args, stage=stage, server_type=Server, + stop_audit=partial(audit_stopped, minecraft_version=native.MINECRAFT, + data_version=DATA_VERSION, stopping_phase="ServerStoppingEvent"), + check_log=native.require_clean_log, minecraft=native.MINECRAFT) + checks = result["checks"] + result["status"] = "PASS" if checks and all(c["status"] == "PASS" for c in checks.values()) else "FAIL" + if args.kind == "dimensions": + result["nativeAssessment"] = native.read_json(runtime / "native117-platform-assessment.json") + result["status"] = result["nativeAssessment"]["assessment"]["status"] + except BaseException as failure: + result.update(status="FAIL", error=repr(failure), traceback=traceback.format_exc()) + checkpoint = runtime / "native117-comparison.json" + if checkpoint.is_file(): + result["checks"] = native.read_json(checkpoint) + raise + finally: + result["sourceUnchanged"] = fingerprint(source) == before + result["seedUnchanged"] = fingerprint(seed) == seed_before + result["inputsUnchanged"] = all(digest(path) == sha for path, sha in ( + (args.profile, result["profileSha256"]), (args.inventory, result["inventorySha256"]), + (args.fixture_receipt, result["fixtureReceiptSha256"]), (java, result["javaSha256"]), + (runner, result["runnerSha256"]), (Path(historical.__file__), result["historicalOracleSha256"]), + *((path, sha.upper()) for _, path, sha in mods.values()), *closure.items(), + )) + if not all(result[key] for key in ("sourceUnchanged", "seedUnchanged", "inputsUnchanged")): + result["status"] = "FAIL" + if source_reference is not None: + result["stoppedSourceReferenceUnchanged"] = ( + fingerprint(args.source_stopped_reference.parent) == reference_before + and digest(Path(reference_helper.__file__)) == result["stoppedSourceReference"]["helperSha256"]) + if not result["stoppedSourceReferenceUnchanged"]: + result["status"] = "FAIL" + write_json(report, result) + return int(result["status"] not in ("PASS", "PASS_WITH_NATIVE_DIFFERENCE")) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native262_broad.py b/test-rig/migration/native262_broad.py new file mode 100644 index 00000000..3dc9d5fa --- /dev/null +++ b/test-rig/migration/native262_broad.py @@ -0,0 +1,126 @@ +"""Native26 walkable works: retain the 21-bay and twelve-drill operation oracles.""" + +import argparse +from pathlib import Path +import sys +from types import SimpleNamespace +import traceback + +from matrix import digest, fingerprint, write_json +from native117 import output_path, require_disjoint +from native117_actor_io import bind +from native117_broad import campaign_document, run_profile +import native12111_broad as previous +import native262 as native + + +RECEIPT = "native262-broad-results.json" + + +def supported_mode(value): + if value != "packaged-current": + raise argparse.ArgumentTypeError( + f"{value!r} is unsupported for Minecraft 26.2: BaseMetals is mandatory. " + "Use packaged-current with the complete pinned family profile.") + return value + + +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) + drill_tag = staticmethod(previous.Adapter.drill_tag) + drill_tickets = staticmethod(bind(previous.Adapter.drill_tickets, native=native)) + + @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_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 + + +def oracle_closure(): + directory = Path(__file__).resolve().parent + return {Path(module.__file__).resolve(): digest(Path(module.__file__)) for module in tuple(sys.modules.values()) + if getattr(module, "__file__", None) and Path(module.__file__).suffix == ".py" + and Path(module.__file__).resolve().parent == directory + and not Path(module.__file__).name.startswith("test_")} + + +def main(argv=None): + 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("--profile-sha256", 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", type=supported_mode, default="packaged-current", metavar="packaged-current") + args = parser.parse_args(argv) + if not 1 <= args.run_seconds <= 60 or not 1024 <= args.port < 65535: + parser.error("Use a bounded duration and an unprivileged localhost port") + native.validate_hash(args.profile, args.profile_sha256) + seed, inventory, mods = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + if set(mods) != set(native.VERSIONS): + parser.error("BaseMetals and the complete six-jar family are mandatory for Minecraft 26.2") + output = output_path(args.output) + java, version = native.java25(args.java) + for path in (args.profile, seed, args.inventory, java, *(item[1] for item in mods.values())): + require_disjoint(output, path) + seed_before = 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 or len({station["id"] for station in stations}) != 21: + raise AssertionError("The complete 21-bay definition is required") + protected = oracle_closure() + protected.update({path: digest(path) for path in (args.profile, args.inventory, java, spec, + *(value[1] for value in mods.values()))}) + proof = dict(qualificationStatus="EXPLORATORY_UNQUALIFIED", minecraft=native.MINECRAFT, forge=native.FORGE, + scope="PACKAGED_WALKABLE_WORKS_AND_TWELVE_DRILL_WITNESSES", + profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), + stationDefinitionSha256=digest(spec), javaVersion=version, javaSha256=digest(java), + inputHashes={str(path): sha for path, sha in protected.items()}, + jars={key: item["sha256"] for key, item in entries.items()}) + receipt = output / RECEIPT + names = (args.mode,) + results = {} + write_json(receipt, campaign_document(names, results, proof, finished=False)) + try: + result = run_profile(SimpleNamespace(java17=java, run_seconds=args.run_seconds), None, + output, args.mode, args.port, stations, + native118_target=(seed, inventory, entries), native_adapter=Adapter) + results[args.mode] = result + except Exception: + results[args.mode] = dict(status="FAIL", profile=args.mode, error=traceback.format_exc()) + try: + native.verify_runtime(output / args.mode, inventory, + {item["file"]: item["sha256"] for item in entries.values()}) + except Exception: + results[args.mode].update(status="FAIL", custodyError=traceback.format_exc()) + final = campaign_document(names, results, proof, finished=True) + final["seedUnchanged"] = fingerprint(seed) == seed_before + final["inputsUnchanged"] = all(path.is_file() and digest(path) == sha for path, sha in protected.items()) + if not final["seedUnchanged"] or not final["inputsUnchanged"]: + final["status"] = "FAIL" + write_json(receipt, final) + print(args.mode, final["status"], flush=True) + return int(final["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native262_client.py b/test-rig/migration/native262_client.py new file mode 100644 index 00000000..69f16640 --- /dev/null +++ b/test-rig/migration/native262_client.py @@ -0,0 +1,872 @@ +"""Pinned native65 real-world client launch and strict retained resource evidence. + +Actual-world oil/reload and human gameplay remain distinct, mandatory scopes. +""" + +import hashlib +import json +import math +import os +import re +from pathlib import Path +import shlex +import shutil +import struct +import subprocess +import tomllib +import xml.etree.ElementTree as ET +from zipfile import ZipFile +import zlib + +from matrix import digest, fingerprint, write_json +from native117 import output_path, require_disjoint +import native262 as native +import native262_layout as layout +import native262_client_diagnostics as client_diagnostics +from native262_resources import FAMILY, arguments, artifact, preflight, require + + +ASSET_INDEX = "32" +ASSET_INDEX_SHA1 = "941B1EA6AFB4A472D164C0D12E3E3272BF3435F8" +VIEWPORTS = ((320, 240, 1), (426, 240, 2), (640, 360, 1)) +VM_ARGS = ("-Djava.net.preferIPv6Addresses=system", "-XX:+UseCompactObjectHeaders", "--add-opens", + "java.base/java.lang.invoke=ALL-UNNAMED", "-Dforge.enableGameTest=true", + "-Dforge.logging.markers=REGISTRIES", "-Dforge.logging.console.level=info") +RESULT = "native262-client-results.json" +CLIENT_WORLD = "AdvantageClientResources" +CLIENT_USERNAME = "AdvantageProbe" +FIXTURE_PROTOCOL = "NATIVE65_SEALED_EXPERIMENTAL_FIXTURE_SKIP_V2" +FIXTURE_SCOPE = "COMPLETED_NATIVE65_BROAD_PACKAGED_CURRENT_COPY" + + +def verify_world_source(world, receipt, profile_sha256, mods): + require(receipt["status"] == "PASS" and receipt["inputsUnchanged"] is True + and receipt["seedUnchanged"] is True and receipt["profileSha256"] == profile_sha256.upper() + and receipt["jars"] == {key: row[2] for key, row in mods.items()}, + "Client world lacks a completed exact-family source receipt") + require(receipt["minecraft"] == native.MINECRAFT and receipt["forge"] == native.FORGE + and receipt["scope"] == "PACKAGED_WALKABLE_WORKS_AND_TWELVE_DRILL_WITNESSES", + "Client source is not the approved native broad fixture") + completed = receipt["profiles"]["packaged-current"] + require(completed["status"] == "PASS" and completed["profile"] == "packaged-current" + and all(completed["phases"][phase]["status"] == "PASS" for phase in ( + "first", "initial", "operation", "persistence", "restarted")) + and completed["expectedMods"] == {"minecraft": native.MINECRAFT, "forge": native.FORGE, + **{key: row[0] for key, row in mods.items()}}, "Client broad source is incomplete or has a different family") + data = layout.read_level(world) + require(str(data["Version"]["Name"]) == native.MINECRAFT, "Client source world is not native26") + root = layout._load(Path(world) / "level.dat") + versions = {str(row["ModId"]): str(row["ModVersion"]) for row in root["fml"]["LoadingModList"]} + require(versions == {"minecraft": native.MINECRAFT, "forge": native.FORGE, + **{key: row[0] for key, row in mods.items()}}, "Client world mod identities differ") + return fingerprint(world) + + +def validate_username(name): + require(isinstance(name, str) and re.fullmatch(r"[A-Za-z0-9_]{1,16}", name) is not None, + "Native offline probe name must contain 1..16 ASCII letters, digits or underscores") + return name + + +def words(value): + return [word[1:-1] if word.startswith('"') and word.endswith('"') else word + for word in shlex.split(value, posix=False)] + + +def mod_ids(path): + with ZipFile(path) as jar: + if "META-INF/mods.toml" not in jar.namelist(): + return set() + metadata = tomllib.loads(jar.read("META-INF/mods.toml").decode("utf-8")) + return {row["modId"] for row in metadata.get("mods", [])} + + +def record(path): + path = Path(path).resolve(strict=True) + require(path.is_file() and path.stat().st_nlink == 1, "Client code needs independent files") + for part in (path, *path.parents): + require(not part.is_symlink() and not getattr(part, "is_junction", lambda: False)(), "Aliased client code") + return dict(path=str(path), sha256=digest(path)) + + +def inspect_launch(path): + launch = record(path) + tree = ET.parse(path).getroot() + attrs = {} + for row in tree.findall("stringAttribute"): + require(row.attrib["key"] not in attrs, "Duplicate Eclipse launch attribute") + attrs[row.attrib["key"]] = row.attrib["value"] + require(attrs["org.eclipse.jdt.launching.MAIN_TYPE"] == "net.minecraftforge.launcher.Main", + "Unexpected client entrypoint") + vm = words(attrs["org.eclipse.jdt.launching.VM_ARGUMENTS"]) + require(tuple(vm) == VM_ARGS, "Unreviewed JVM options, agent, or altered client classpath") + program = words(attrs["org.eclipse.jdt.launching.PROGRAM_ARGUMENTS"]) + require(len(program) == 19 and program[:3] == ["--main", "net.minecraftforge.bootstrap.ForgeBootstrap", "--cache"] + and program[4] == "--metadata" and program[6:16] == ["--", "--gameDir", ".", "--launchTarget", + "forge_userdev_client", "--version", "MOD_DEV", "--assetIndex", ASSET_INDEX, "--assetsDir"] + and program[17:] == ["--mixin", "basepower.mixins.json"], "Unreviewed native client program arguments") + environment = {} + maps = [row for row in tree.findall("mapAttribute") if row.attrib["key"] == "org.eclipse.debug.core.environmentVariables"] + require(len(maps) == 1, "Expected one explicit Eclipse environment") + for row in maps[0].findall("mapEntry"): + require(row.attrib["key"] not in environment, "Duplicate client environment") + environment[row.attrib["key"]] = row.attrib["value"] + expected_env = dict(MC_VERSION=native.MINECRAFT, FORGE_VERSION=native.FORGE, FORGE_GROUP="net.minecraftforge", + MCP_MAPPINGS="official_26.2", MCP_VERSION=native.MCP, target="fmluserdevclient") + require({key: value for key, value in environment.items() if key != "MOD_CLASSES"} == expected_env, + "Wrong SDK environment or unreviewed injection") + # MOD_CLASSES and IDE/JRE entries are not executed. Construct a jar-only classpath below. + environment["MOD_CLASSES"] = "" + lists = [row for row in tree.findall("listAttribute") if row.attrib["key"] == "org.eclipse.jdt.launching.CLASSPATH"] + require(len(lists) == 1, "Expected one explicit client classpath") + archives, skipped, ide = [], [], [] + for row in lists[0].findall("listEntry"): + value = ET.fromstring(row.attrib["value"]).attrib + if "externalArchive" not in value: + require(value in ({"projectName": "PowerAdvantage", "type": "1", "path": "5"}, + {"containerPath": "org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-25", + "type": "4", "path": "5"}), "Unreviewed IDE classpath output") + ide.append(value) + continue + archive = Path(value["externalArchive"]).resolve(strict=True) + require(archive.suffix == ".jar", "Loose client code is forbidden") + identities = mod_ids(archive) + if identities & FAMILY: + require(len(identities) == 1, "Ambiguous source candidate archive") + skipped.append(str(archive)) + else: + require(identities <= {"forge", "minecraft"}, "Unexpected mod on SDK classpath") + archives.append(record(archive)) + require(len(ide) == len({tuple(sorted(row.items())) for row in ide}) == 2 + and len({row["path"] for row in archives}) == len(archives), "Duplicate/missing SDK classpath entries") + require(sum(Path(row["path"]).name == "forge-26.2-65.1.0.jar" for row in archives) == 1, + "Missing exact mapped Forge65 runtime") + require(sum(Path(row["path"]).name == "client-extra-26.2-20260616.103818.jar" for row in archives) == 1, + "Missing exact native vanilla client resources") + require(not any("forge-1." in Path(row["path"]).name or "client-extra-1." in Path(row["path"]).name for row in archives), + "Historical native archive in client classpath") + metadata = record(program[5]) + with ZipFile(metadata["path"]) as jar: + version = json.loads(jar.read("minecraft/version.json")) + require(version["id"] == native.MINECRAFT and version["javaVersion"]["majorVersion"] == 25 + and version["assetIndex"]["id"] == ASSET_INDEX + and version["assetIndex"]["sha1"].upper() == ASSET_INDEX_SHA1, "Wrong actual native SDK manifest") + index = Path(program[16]) / "indexes" / (ASSET_INDEX + ".json") + require(hashlib.sha1(index.read_bytes()).hexdigest().upper() == ASSET_INDEX_SHA1, "Wrong native client asset index") + return dict(format=1, minecraft=native.MINECRAFT, forge=native.FORGE, mcp=native.MCP, + javaSha256=native.JAVA_SHA, launch=launch, archives=archives, metadata=metadata, + assetIndex=record(index), program=program, vm=vm, environment=environment, + excludedMutableCandidatePaths=skipped) + + +def seal_client_code(launch, output): + output = Path(output).resolve() + require_disjoint(output, Path(launch).resolve(strict=True)) + document = inspect_launch(launch) + with output.open("x", encoding="utf-8") as stream: + json.dump(document, stream, indent=2, sort_keys=True) + stream.write("\n") + return document + + +def png_record(root, name, dimensions): + path = Path(root) / name + row = record(path) + require(Path(row["path"]).is_relative_to(Path(root).resolve()), "Image escaped client runtime") + require(path.stat().st_size <= 64 * 1024 * 1024, "Oversized native image") + data = path.read_bytes() + header = data[:24] + require(header[:8] == b"\x89PNG\r\n\x1a\n" and header[12:16] == b"IHDR" + and len(header) == 24 and struct.unpack(">II", header[16:24]) == dimensions, + "Missing/malformed native image: " + name) + offset, chunks, compressed, channels = 8, [], bytearray(), None + while offset < len(data): + require(offset + 12 <= len(data), "Truncated PNG chunk") + length = struct.unpack(">I", data[offset:offset + 4])[0] + end = offset + 12 + length + require(end <= len(data), "Unbounded PNG chunk") + kind, content = data[offset + 4:offset + 8], data[offset + 8:end - 4] + require(zlib.crc32(kind + content) & 0xFFFFFFFF == struct.unpack(">I", data[end - 4:end])[0], "PNG CRC mismatch") + chunks.append(kind) + if kind == b"IHDR": + require(len(content) == 13 and content[8] == 8 and content[9] in (2, 6) + and content[10:] == b"\0\0\0", "Unexpected native PNG encoding") + channels = 4 if content[9] == 6 else 3 + elif kind == b"IDAT": + compressed.extend(content) + elif kind == b"IEND": + require(length == 0 and end == len(data), "Trailing PNG data") + offset = end + require(chunks[0] == b"IHDR" and chunks.count(b"IHDR") == 1 and chunks[-1] == b"IEND" + and chunks.count(b"IEND") == 1 and compressed, "Incomplete native PNG") + stride = dimensions[0] * channels + 1 + decoder = zlib.decompressobj() + raw = decoder.decompress(compressed, stride * dimensions[1] + 1) + require(len(raw) == stride * dimensions[1] and decoder.eof and not decoder.unused_data + and not decoder.unconsumed_tail and all(raw[row * stride] <= 4 for row in range(dimensions[1])), + "Malformed or oversized native PNG pixel stream") + return dict(file=name, sha256=row["sha256"], width=dimensions[0], height=dimensions[1]) + + +def read_frame(root, frame): + width, height = frame["width"], frame["height"] + require(type(width) is int and type(height) is int and 1 <= width <= 4096 and 1 <= height <= 4096, + "Invalid native framebuffer dimensions") + rgba = artifact(root, frame["rgba"]) + require(rgba.stat().st_size == width * height * 4, "Truncated native framebuffer") + png = artifact(root, frame["png"]) + png_record(root, str(png.relative_to(Path(root).resolve())), (width, height)) + state = native.read_json(artifact(root, frame["native"])) + fills = state.get("fills", []) + require(state["protocol"] == "NATIVE65_GUI_FRAMEBUFFER_FENCED_V1" + and (state["width"], state["height"]) == (width, height) + and state["renderer"] == "net.minecraft.client.gui.render.GuiRenderer" + and state["extractor"] in ("net.minecraft.client.gui.GuiGraphicsExtractor", + "zone.moddev.mc.advantageworks.client.MachineGuiLayoutProbe$RecordingScreen$1") + and all(state[key] is True for key in ("renderSubmitted", "fenceCompleted", "screenshotCallbackCompleted")) + and isinstance(state["texts"], list) and isinstance(state["blits"], list) and isinstance(fills, list) + and 0 < len(state["blits"]) + len(fills) <= 10000, + "No actual native extraction/submission/fenced screenshot evidence") + for blit in state["blits"]: + require(all(type(blit[key]) is int for key in ("x0", "y0", "x1", "y1", "color", "textureViewIdentity")) + and blit["textureViewIdentity"] != 0 and isinstance(blit["pipeline"], str) and blit["pipeline"] + and len(blit["pose"]) == 6 and all(type(value) in (int, float) and math.isfinite(value) + for value in [*blit["pose"], *(blit[key] for key in ("u0", "v0", "u1", "v1"))]), + "Malformed/empty native blit extraction") + for fill in fills: + require(all(type(fill[key]) is int for key in ("x0", "y0", "x1", "y1", "col1", "col2")) + and isinstance(fill["pipeline"], str) and fill["pipeline"] and len(fill["pose"]) == 6 + and all(type(value) in (int, float) and math.isfinite(value) for value in fill["pose"]), + "Malformed native color extraction") + return width, height, rgba.read_bytes(), state + + +def verify_projection_calibration(root, calibration): + require(calibration["protocol"] == "NATIVE65_GUI_PROJECTION_PIXEL_RULER_V1" and calibration["status"] == "PASS", + "Native projection calibration did not complete") + cases = calibration["cases"] + require(len(cases) == 2 and {row["scale"] for row in cases} == {1, 2}, "Missing projection scale control") + checked = 0 + for row in cases: + scale = row["scale"] + require((row["logicalWidth"], row["logicalHeight"]) == (64, 48) + and row["exactPixelsChecked"] == 64 * 48 * scale * scale, "Wrong projection control dimensions") + for key, shift in (("actual", 0), ("shiftedRejected", 1)): + frame = read_frame(root, row[key]) + require(frame[:2] == (64 * scale, 48 * scale) and len(frame[3].get("fills", [])) == 3, + "Calibration lacks actual native rectangle extraction") + expected = bytearray() + for y in range(48 * scale): + for x in range(64 * scale): + u, v = x // scale, y // scale + color = 0xFF37A95C if 7 + shift <= u < 23 + shift and 9 <= v < 25 else ( + 0xFF3158D5 if (u, v) == (2, 2) else 0xFFDD7341 if (u, v) == (63, 47) else 0xFF101820) + expected.extend((color >> 16 & 255, color >> 8 & 255, color & 255, 255)) + require(frame[2] == expected, "Native projection ruler pixels are compressed/clipped/shifted") + require(row["actual"]["rgba"]["sha256"] != row["shiftedRejected"]["rgba"]["sha256"], + "Shifted projection negative control was accepted") + checked += row["exactPixelsChecked"] + return dict(status="PASS", physicalPixels=checked, shiftedGpuControls=2) + + +def source_references(document, mods): + requested = set() + + def collect(value): + if isinstance(value, dict): + if set(value) == {"resource", "sha256", "width", "height"}: + requested.add(value["resource"]) + for child in value.values(): + collect(child) + elif isinstance(value, list): + for child in value: + collect(child) + + collect(document) + require(requested, "Missing source artwork provenance") + wanted = {"assets/" + key.replace(":", "/", 1): key for key in requested} + found = {} + for _, path, _ in mods.values(): + with ZipFile(path) as jar: + for name in set(jar.namelist()) & wanted.keys(): + key = wanted[name] + require(key not in found, "Ambiguous GUI source artwork provider: " + key) + found[key] = hashlib.sha256(jar.read(name)).hexdigest().upper() + require(set(found) == requested, "GUI source artwork is absent from the frozen family") + return found + + +def verify_producer(document): + require(document.get("status") == "PASS", "Native client producer failed: " + json.dumps( + {key: document[key] for key in ("status", "error", "cause", "loginReadiness", "scope_results") if key in document}, + sort_keys=True)) + + +def fixture_policy(runtime, profile, profile_sha, source_receipt, source_world): + native.validate_hash(profile, profile_sha) + document = native.read_json(profile) + require(document["minecraftVersion"] == native.MINECRAFT and document["forge"]["version"] == native.FORGE_VERSION, + "Experimental warning action requires the exact native target") + mods = {row["id"]: (row["version"], Path(row["path"]), row["sha256"]) for row in document["mods"]} + require(len(document["mods"]) == len(mods) == 6 and set(mods) == FAMILY, "Experimental warning policy needs the ordinary six-jar family") + require(Path(source_world).resolve() == Path(source_receipt).resolve().parent / "packaged-current/AdvantageWorksMigration", + "Experimental warning action is not approved for this source-world route") + verify_world_source(source_world, native.read_json(source_receipt), profile_sha, mods) + copy = Path(runtime) / "saves" / CLIENT_WORLD + level_sha = digest(Path(source_world) / "level.dat") + require(level_sha == digest(copy / "level.dat"), + "Experimental warning action requires the exact approved stopped source/copy") + return dict(protocol=FIXTURE_PROTOCOL, eligibility=FIXTURE_SCOPE, profileSha256=profile_sha.upper(), profilePath=str(Path(profile).resolve()), + sourceReceiptSha256=digest(source_receipt), copiedWorld=str(copy.resolve()), copiedLevelSha256=level_sha, + sourceLedgerSha256=digest(Path(runtime) / "client-world-source-files.json")) + + +def verify_fixture_action(root, action): + require(action["protocol"] == FIXTURE_PROTOCOL, "Unknown native fixture action protocol") + if action["status"] == "NOT_OBSERVED": + require(type(action["actionCount"]) is int and action["actionCount"] == 0, "Unreported native fixture action") + return dict(status="NOT_OBSERVED", actionCount=0) + require(action["status"] == "NATIVE_SKIP_BUTTON_RETURNED" and action["onPressReturned"] is True + and type(action["actionCount"]) is int and action["actionCount"] == 1 + and action["screenClass"] == "net.minecraft.client.gui.screens.BackupConfirmScreen" + and action["titleKey"] == "selectWorld.backupQuestion.experimental" + and action["buttonKey"] == "selectWorld.backupJoinSkipButton" + and (action["key"], action["scancode"], action["modifiers"]) == (257, 0, 0) + and action["fenceCompleted"] is True, + "Unapproved/repeated native confirmation action") + policy = native.read_json(Path(root) / "client-fixture-policy.json") + input_doc = native.read_json(Path(root) / "client-input.json") + require(policy["protocol"] == FIXTURE_PROTOCOL and policy["eligibility"] == FIXTURE_SCOPE + and action["profileSha256"] == policy["profileSha256"] == input_doc["profileSha256"] + and action["sourceReceiptSha256"] == policy["sourceReceiptSha256"] == digest(Path(input_doc["sourceWorldReceipt"])) + and digest(Path(policy["profilePath"])) == policy["profileSha256"], "Native warning action has different sealed inputs") + require(digest(Path(root) / "client-fixture-policy.json") == action["policySha256"] + and Path(action["copiedWorld"]) == Path(root).resolve() / "saves" / CLIENT_WORLD, + "Native fixture action lost its sealed copied-world binding") + before = native.read_json(Path(root) / "login-readiness/experimental-before-action.json") + require(before["status"] == "RECORDED_BEFORE_NATIVE_ACTION" and before["actionCount"] == 0 + and {key: value for key, value in before.items() if key not in ("status", "actionCount")} + == {key: value for key, value in action.items() if key not in ("status", "actionCount", "onPressReturned")}, + "Native fixture action lacks its immutable pre-action record") + readiness = action["readinessBeforeAction"] + require(readiness["overlayClass"] == "none" and all(readiness[key] is False for key in ( + "levelPresent", "playerPresent", "integratedServer", "singleplayerServerPresent", "connectionPresent")), + "Native fixture action ran outside the unconnected pre-server screen") + image = png_record(root, action["screenshot"], (action["width"], action["height"])) + require(image["sha256"] == action["screenshotSha256"] and action["fenceCompleted"] is True, + "Native warning frame changed or was not fenced before action") + require(action["copiedLevelCustody"]["initialSha256"] == policy["copiedLevelSha256"] + and action["copiedLevelCustody"]["exactTypedNbtTransform"] in ( + "UNCHANGED_TYPED_ROOT", "ONLY_FML_LOWERCASE_EMPTY_BIOME_INDEX_REMOVED"), "Unapproved copied-level transformation") + return dict(status="PASS", actionCount=1, screenshot=image) + + +def world_sample_pixel(matrix, camera, point, width, height): + offset = [point[index] - camera[index] for index in range(3)] + [1.0] + clip = [sum(matrix[column * 4 + row] * offset[column] for column in range(4)) for row in range(4)] + require(clip[3] > 0 and all(math.isfinite(value) for value in clip), "World oil sample is behind the native camera") + return [int((clip[0] / clip[3] * .5 + .5) * width), int((.5 - clip[1] / clip[3] * .5) * height)] + + +def verify_world_oil(root, report): + require(report["protocol"] == "NATIVE65_LEVEL_RENDERER_OIL_RELOAD_V1" and report["status"] == "PASS" + and report["scope"] == "ACTUAL_LEVEL_RENDERER_NOT_GUI_OR_SYNTHETIC_WORLD" and report["mipmapLevel"] == 4, + "Actual-world oil/reload did not complete") + frames = report["frames"] + require(set(frames) == {"original", "empty", "wrong_water", "restored", "after_reload"}, "Missing world oil control/reload frame") + pixels, dimensions = {}, None + for name, frame in frames.items(): + width, height = frame["width"], frame["height"] + require(type(width) is int and type(height) is int and 320 <= width <= 4096 and 240 <= height <= 2160, + "Invalid world framebuffer dimensions") + if dimensions is None: + dimensions = (width, height) + require(dimensions == (width, height) and frame["renderer"] == "net.minecraft.client.renderer.LevelRenderer" + and frame["dimension"] == "minecraft:overworld" and frame["fenceCompleted"] is True + and frame["hudHidden"] is True and type(frame["renderedSections"]) is int and frame["renderedSections"] > 0, + "Missing actual native world renderer/compiled-section/fence evidence") + raw = artifact(root, frame["rgba"]) + require(raw.stat().st_size == width * height * 4, "Truncated world framebuffer") + pixels[name] = raw.read_bytes() + png = artifact(root, frame["png"]) + png_record(root, str(png.relative_to(Path(root).resolve())), (width, height)) + require(len(frame["camera"]) == 3 and all(type(value) in (int, float) and math.isfinite(value) for value in frame["camera"]) + and abs(frame["camera"][0] - 512.5) < .001 and abs(frame["camera"][2] - 527) < .001 + and 14 <= frame["camera"][1] <= 16 and len(frame["viewRotationProjection"]) == 16 + and all(type(value) in (int, float) and math.isfinite(value) for value in frame["viewRotationProjection"]), "Uncontrolled native oil camera") + require(len(frame["pools"]) == 3, "Missing world control pool") + for index, pool in enumerate(frame["pools"]): + expected = "minecraft:water" if index == 2 or name == "wrong_water" else "minecraft:empty" if name == "empty" else ( + "basepower:crude_oil" if index == 0 else "basepower:refined_oil") + require(pool["pool"] == ("crude-oil", "refined-oil", "water-control")[index] and pool["fluid"] == expected + and pool["compiledAndVisible"] is True and len(pool["samples"]) == 16, "Incorrect actual-world fluid/control state") + for sample, (x, z) in zip(pool["samples"], ((x, z) for x in range(4) for z in range(4))): + require(len(sample) == 5 and sample[:3] == [(504, 510, 516)[index] + .75 + x * .75, 6 + 8 / 9, 510 + .75 + z * .75] + and all(type(value) is int for value in sample[3:]) and 2 <= sample[3] < width - 2 and 2 <= sample[4] < height - 2, + "World oil samples escaped the exact native pool geometry/frame") + require(sample[3:] == world_sample_pixel(frame["viewRotationProjection"], frame["camera"], sample[:3], width, height), + "World oil pixel does not project from the retained native camera/pool point") + checks = [] + for actual in ("original", "after_reload"): + require(frames[actual]["camera"] == frames["empty"]["camera"] == frames["wrong_water"]["camera"] + and frames[actual]["viewRotationProjection"] == frames["empty"]["viewRotationProjection"] + == frames["wrong_water"]["viewRotationProjection"], "World camera/projection moved between negative controls") + for index, pool in enumerate(frames[actual]["pools"]): + row = dict(actual=actual, pool=pool["pool"], samples=144) + for control in ("empty", "wrong_water"): + changed = 0 + for sample in pool["samples"]: + for dy in range(-1, 2): + for dx in range(-1, 2): + at = ((sample[4] + dy) * dimensions[0] + sample[3] + dx) * 4 + distance = sum(abs(a - b) for a, b in zip(pixels[actual][at:at + 3], pixels[control][at:at + 3])) + changed += distance > (32 if index == 2 else 64) + require(changed <= 14 if index == 2 else changed >= 72, "Native world oil blank/wrong-fluid control was accepted") + row[control + "Different"] = changed + checks.append(row) + require(checks == report["pixelControls"], "World pixel-control counts differ from retained raw RGBA") + for key in ("modelsBeforeReload", "modelsAfterReload"): + require(len(report[key]) == 2, "Missing actual-world oil model") + for index, model in enumerate(report[key]): + name = ("crude_oil", "refined_oil")[index] + require(model["fluid"] == "basepower:" + name and model["layer"] == "TRANSLUCENT" + and model["stillSprite"] == f"basepower:blocks/{name}_still" + and model["flowingSprite"] == f"basepower:blocks/{name}_flow", "Actual-world oil used the wrong native model/sprites") + if key == "modelsAfterReload": + require(all(model[field] != report["modelsBeforeReload"][index][field] for field in ("stillIdentity", "flowingIdentity")), + "Actual-world oil retained its pre-reload atlas sprites") + geometry = report["nativeGeometryAfterReload"] + require(len(geometry["fluid_rendering"]) == 6 and all(row["correct_sprite"] is True and row["vertices"] >= 4 + for row in geometry["fluid_rendering"]), "Native post-reload fluid geometry is incomplete or stale") + return dict(status="PASS", frames=5, oilNegativeComparisons=8, waterControlComparisons=4) + + +def require_source(source, references): + require(source["resource"] in references and source["sha256"] == references[source["resource"]], + "Source artwork does not match the frozen jar bytes") + + +def slot_mismatches(frame, proof, left, top, scale): + width, height, data, _ = frame + points, slots = proof["border_points"], proof["slots"] + require(len(points) == 68 and {(x, y) for x, y, _ in points} + == {(x, y) for x in range(18) for y in range(18) if x in (0, 17) or y in (0, 17)}, + "Incomplete independent slot-border pixel template") + require(len(slots) >= 36 and len({tuple(pos) for pos in slots}) == len(slots), "Missing/duplicate slot extraction") + require(proof["logical_texture_size"] == 256 and slots[:-36] == proof["expected_machine_slots"], + "Machine slot/background geometry differs from GUI declaration") + colors = proof["slot_border_colors"] + require(proof["source_sampling"] == "PHYSICAL_PIXEL_CENTERS_ON_256_LOGICAL_UV_V1" + and len(colors) == len(slots) and all(len(row) == 68 * scale * scale + and all(type(value) is int for value in row) for row in colors), + "Missing per-slot source artwork pixels") + require(all(len(set(row)) >= 2 for row in colors), "Blank source slot-border crop") + require(all(row == [value for _, _, value in points for _ in range(scale * scale)] for row in colors[-36:]), "Player inventory template changed") + player = [[7 + i % 9 * 18, 157 + i // 9 * 18] for i in range(27)] + [[7 + i * 18, 215] for i in range(9)] + require(slots[-36:] == player, "Player slot/background geometry changed") + require(proof["checked_pixels"] == len(slots) * 68 * scale * scale, "Incomplete slot pixel coverage") + different = 0 + for index, (slot_x, slot_y) in enumerate(slots): + require(type(slot_x) is int and type(slot_y) is int, "Invalid extracted slot coordinate") + for point_index, (x, y, _) in enumerate(points): + for dy in range(scale): + for dx in range(scale): + argb = colors[index][point_index * scale * scale + dy * scale + dx] + expected = bytes((argb >> 16 & 255, argb >> 8 & 255, argb & 255, argb >> 24 & 255)) + px, py = (left + slot_x + x) * scale + dx, (top + slot_y + y) * scale + dy + require(0 <= px < width and 0 <= py < height, "Slot framebuffer bounds escaped") + at = (py * width + px) * 4 + different += data[at:at + 4] != expected + return different + + +def verify_machine_frame(root, proof, title, inventory, references): + width, height, scale = proof["viewport"] + require((width, height, scale) in VIEWPORTS and type(proof["fill"]) is int and proof["fill"] in range(3), + "Unknown native GUI render case") + left, top = (width - 176) // 2, (height - 240) // 2 + require((proof["left"], proof["top"]) == (left, top), "Native background is not centered/aligned") + actual, expected, background = (read_frame(root, proof[key]) for key in ("actual", "expected", "background")) + require(all(frame[:2] == (width * scale, height * scale) for frame in (actual, expected, background)), + "Viewport/framebuffer size mismatch") + for text in (title, inventory): + signatures = [] + for frame in (actual, expected): + matches = [row for row in frame[3]["texts"] if row["text"] == text] + require(len(matches) == 1 and matches[0]["color"] == -1 and matches[0]["dropShadow"] is True, + "Missing/garbled native caption extraction") + row = matches[0] + signatures.append((row["x"], row["y"], row["pose"])) + require(signatures[0] == signatures[1], "Native caption placement differs from independent labels") + ink_total = 0 + for x0, y0, x1, y1 in ((4, top + 1, width - 4, top + 12), (left + 8, top + 140, left + 168, top + 150)): + ink = 0 + for y in range(y0 * scale, y1 * scale): + for x in range(x0 * scale, x1 * scale): + at = (y * actual[0] + x) * 4 + if expected[2][at:at + 4] != background[2][at:at + 4]: + ink += 1 + require(actual[2][at:at + 4] == expected[2][at:at + 4], "Caption framebuffer pixels are missing/garbled/clipped") + require(ink >= 20 * scale * scale, "Blank expected caption or a missing-text control passed") + ink_total += ink + slots = proof["slot_pixels"] + require_source(slots["inventory_source"], references) + require_source(slots["machine_source"], references) + require(slot_mismatches(actual, slots, left, top, scale) == 0, "Slot framebuffer pixels differ from source artwork") + return dict(captionInkPixels=ink_total, slotPixels=slots["checked_pixels"]) + + +def verify_tank_frames(root, row, references): + scale = row["render_scale"] + require(type(scale) is int and scale in (1, 2), "Unexpected tank pixel scale") + actual, expected, empty = (read_frame(root, row[key]) for key in ("actual_frame", "expected_frame", "empty_frame")) + require(all(frame[:2] == (176 * scale, 240 * scale) for frame in (actual, expected, empty)), "Wrong tank framebuffer dimensions") + require_source(row["source_artwork"], references) + require(actual[2] == expected[2] and row["whole_panel_different_pixels"] == 0, + "Whole tank background/frame/slot pixels differ") + if row["fill_rows"]: + start = 81 - row["fill_rows"] + + def quads(frame): + selected = [blit for blit in frame[3]["blits"] if blit.get("scissor") == [80, start, 16, row["fill_rows"]]] + require(len(selected) == (row["fill_rows"] + 15) // 16 + and {(blit["x0"], blit["y0"], blit["x1"], blit["y1"]) for blit in selected} + == {(80, y, 96, y + 16) for y in range(start, 81, 16)}, + "Native fluid extraction stretched/cropped a tile instead of scissor clipping") + signature = [(blit["u0"], blit["v0"], blit["u1"], blit["v1"], blit["color"], blit["textureViewIdentity"]) + for blit in sorted(selected, key=lambda value: value["y0"])] + require(all(value == signature[0] for value in signature), "Native fluid tiles changed sprite UV or tint") + return signature + + require(quads(actual) == quads(expected), "Native fluid sprite UV/tint differs from independent reference") + visible = sum(expected[2][at:at + 4] != empty[2][at:at + 4] + for y in range(21 * scale, 81 * scale) for x in range(80 * scale, 96 * scale) + for at in [(y * 176 * scale + x) * 4]) + require(visible >= row["fill_rows"] * 8 * scale * scale, "Blank reference fluid sprite") + garbled = read_frame(root, row["garbled_frame"]) + require(garbled[:2] == actual[:2] and garbled[2] != expected[2] and empty[2] != expected[2] + and row["blank_control_different_pixels"] > 0 and row["garbled_control_different_pixels"] > 0, + "Blank/garbled tank control was accepted") + + +def freeze_native_frames(root): + folder = Path(root) / "native-gui-evidence" + if not folder.exists(): + return dict(status="NOT_CAPTURED", frames=[]) + frames, claimed = [], set() + for path in sorted(folder.glob("frame-*-native.json")): + state = native.read_json(path) + stem = path.name.removesuffix("-native.json") + frame = dict(width=state["width"], height=state["height"]) + for key, suffix in (("png", ".png"), ("rgba", ".rgba"), ("native", "-native.json")): + file = folder / (stem + suffix) + frame[key] = dict(file=str(file.relative_to(root)), sha256=digest(file)) + claimed.add(file.resolve()) + read_frame(root, frame) + frames.append(frame) + require(claimed == {path.resolve() for path in folder.iterdir()} and 0 < len(frames) <= 2000, + "Native framebuffer freeze has partial/unknown artifacts") + return dict(status="FROZEN_CAPTURE_EVIDENCE_NOT_MANUAL_ACCEPTANCE", frames=frames) + + +def verify_vanilla_resources(root, code): + folder = Path(root) / "vanilla-resource-controls" + if not folder.exists(): + return dict(status="NOT_CAPTURED", controls=[]) + paths = sorted(folder.iterdir()) + require(1 <= len(paths) <= 4 and [path.name for path in paths] + == [f"control-{number:02d}.json" for number in range(1, len(paths) + 1)], + "Incomplete/unexpected native vanilla resource controls") + wanted = {"minecraft:textures/block/stone.png", "minecraft:atlases/blocks.json"} + sources = {key: [] for key in wanted} + vanilla = [archive for archive in code["archives"] + if Path(archive["path"]).name == "client-extra-26.2-20260616.103818.jar"] + require(len(vanilla) == 1, "Need the exact native vanilla client-resource archive") + for archive in vanilla: + with ZipFile(archive["path"]) as jar: + for key in wanted: + name = "assets/" + key.replace(":", "/", 1) + if name in jar.namelist(): + data = jar.read(name) + require(data, "Empty primary vanilla resource") + sources[key].append(dict(archive=archive, sha256=hashlib.sha256(data).hexdigest().upper(), bytes=len(data))) + require(all(rows and len({(row["sha256"], row["bytes"]) for row in rows}) == 1 for rows in sources.values()), + "Missing/ambiguous pinned SDK vanilla resource source") + controls = [] + for path in paths: + proof = record(path) + document = native.read_json(path) + require(document["protocol"] == "NATIVE65_VANILLA_RESOURCE_ROOTS_V1" and document["status"] == "PASS" + and document["phase"] == "reload-preparation-before-atlas-upload", "Vanilla native root lookup failed") + rows = document["resources"] + require(len(rows) == 2 and {row["resource"] for row in rows} == wanted, "Missing/duplicate vanilla lookup control") + for row in rows: + expected = sources[row["resource"]][0] + require(row["vanilla"]["present"] is True + and all(row["vanilla"][key] == expected[key] for key in ("sha256", "bytes")), + "Vanilla pack differs from pinned SDK resource bytes") + merged = row["merged"] + require(isinstance(merged, list) and merged and all(isinstance(value["pack"], str) and value["pack"] for value in merged) + and sum(all(value[key] == expected[key] for key in ("sha256", "bytes")) for value in merged) == 1, + "Merged resource manager lost/duplicated the actual vanilla resource") + if row["resource"].endswith("stone.png"): + require(len(merged) == 1, "Unexpected override of the vanilla stone control") + controls.append(proof) + return dict(status="PASS", scope="VANILLA_PACK_ROOT_LOOKUP_NOT_ATLAS_UPLOAD_OR_GRAPHICS", controls=controls, primarySources=sources) + + +def validate_result(document, root, labels, references): + require((document["status"], document["minecraft"], document["forge"]) + == ("PASS", native.MINECRAFT, native.FORGE), "Not a completed native65 client result") + login = document["login"] + require(login["status"] == "PASS" and all(login[key] is True for key in ( + "realWorld", "integratedServer", "nativeComponentsBound", "loggingInEventObserved", "connectionConnected", "clientLoaded")) + and login["syntheticComponentBinding"] is False and type(login["itemHolders"]) is int and login["itemHolders"] > 0, + "Client item/GUI checks did not run after actual native login") + calibration = verify_projection_calibration(root, document["projectionCalibration"]) + fixture_action = verify_fixture_action(root, document["experimentalFixtureGate"]) + world_oil = verify_world_oil(root, document["world_oil_visual"]) + require(set(document["advantage_mods"]) == {"basepower", "basesteam", "baseelectric"} + and len(document["advantage_mods"]) == 3, "Incomplete/duplicate client family") + require(document["mod_versions"] == {key: native.VERSIONS[key] for key in document["advantage_mods"]}, + "Wrong client candidate versions") + require(all(type(document[key]) is int and document[key] > 0 for key in ("items", "block_states", "creative_entries")) + and document["resource_errors"] == [] and document["primary_resource_status"] == "PASS", "Empty/failed resources or creative tab") + scopes = document["scope_results"] + require(set(scopes) == {"registered_resources", "tank_pixels", "machine_gui", "oil_renderer", "recipe_book", "child_geometry"} + and all(row["status"] == "PASS" for row in scopes.values()), "Missing or failed client scope") + fluids = document["fluid_rendering"] + require(document["oil_bucket_identities"] is True and len(fluids) == 6 + and {row["fluid"] for row in fluids} == {"basepower:crude_oil", "basepower:flowing_crude_oil", + "basepower:refined_oil", "basepower:flowing_refined_oil", "minecraft:water", "minecraft:lava"} + and all(row["correct_sprite"] is True and type(row["vertices"]) is int and row["vertices"] >= 4 for row in fluids), + "Incomplete worldless native fluid-model extraction") + require(document["machine_gui_layout_status"] == document["machine_gui_text_status"] == "PASS" + and document["machine_gui_framebuffer_spec"] == 3 + and document["machine_gui_text_spec"] == 2 and document["machine_gui_layout_count"] == 39 + and document["machine_gui_text_checks"] == 351 and document["machine_gui_negative_controls_rejected"] == 356, + "Missing all39 GUI/351-caption/background negative controls") + rows = document["machine_gui_layouts"] + require(len(labels["machines"]) == len(rows) == 39 and {row["block"] for row in rows} == set(labels["machines"]), + "Machine GUI identity inventory changed") + images = [] + framebuffer_checks = [] + for row in rows: + require(row["status"] == row["required_text"] == row["tooltip_and_inventory_hitbox"] == "PASS" + and row["renders"] == row["blank_label_controls_rejected"] == 9 + and type(row["background_extraction_calls"]) is int and row["background_extraction_calls"] == 1 + and row["menu_title"] == row["expected_title"] == labels["machines"][row["block"]], + "Missing caption, tooltip, native background-once, or layout case") + proofs = row["framebuffer_cases"] + require(len(proofs) == 9 and {(tuple(proof["viewport"]), proof["fill"]) for proof in proofs} + == {(viewport, fill) for viewport in VIEWPORTS for fill in range(3)}, + "Missing/duplicate GUI framebuffer cases") + for proof in proofs: + framebuffer_checks.append(verify_machine_frame(root, proof, row["expected_title"], labels["inventory"], references)) + control = read_frame(root, row["garbled_background_control"]) + initial = next(proof for proof in proofs if proof["viewport"] == list(VIEWPORTS[0]) and proof["fill"] == 0) + require(control[:2] == VIEWPORTS[0][:2] and slot_mismatches(control, initial["slot_pixels"], + initial["left"], initial["top"], 1) > 0, "Garbled background passed actual slot framebuffer replay") + stem = row["block"].replace(":", "-") + for width, height, scale in VIEWPORTS: + for fill in range(3): + images.append(png_record(root, f"machine-gui-layouts/{stem}-{width}x{height}-scale{scale}-fill{fill}.png", + (width * scale, height * scale))) + images.append(png_record(root, f"machine-gui-layouts/{stem}-tooltip.png", (320, 240))) + gauges = document["tank_gauge_pixels"] + require(document["tank_gauge_pixel_status"] == "PASS" and len(gauges) == 26, "Incomplete real gauge pixels") + for tank in ("portable", "metal"): + selected = [row for row in gauges if row["tank"] == tank] + require(len(selected) == 13 and {(row["fluid"], row["fill_rows"]) for row in selected} + == {("empty", 0)} | {(fluid, rows) for fluid in ("water", "lava", "crude-oil", "refined-oil") for rows in (15, 30, 60)}, + "Gauge omitted a fluid or partial tile height") + for row in gauges: + require(type(row["different_pixels"]) is int and row["different_pixels"] == 0, "Gauge pixel mismatch") + require(row["render_scale"] == 2, "Final gauge evidence must exercise native GUI scale2") + verify_tank_frames(root, row, references) + images.append(png_record(root, f"tank-gauge-{row['tank']}-{row['fluid']}-{row['amount_mb']}.png", (352, 480))) + require(document["recipe_book_grouping_status"] == "PASS", "Native recipe packets were not rendered") + recipes = document["recipe_book_grouping"] + require(len(recipes) == 3 and {row["packet"] for row in recipes} + == {f"recipes-{mode}-sweep.bin" for mode in ("normal", "tech", "apocalyptic")} + and all(row["status"] == "PASS" and all(type(row[key]) is int and row[key] > 0 + for key in ("recipes", "displays", "cells", "item_tags")) for row in recipes), "Incomplete native recipe/tag decoding") + geometry = document["child_renderer_geometry"] + geometry_context = document["child_geometry_context"] + require(geometry_context["stage"] == "GENUINELY_WORLDLESS_BEFORE_LOGIN" and all(geometry_context[key] is False + for key in ("levelPresent", "playerPresent", "singleplayerServerPresent")), "Child geometry lost genuine worldless isolation") + require(document["child_renderer_geometry_status"] == geometry["status"] == "PASS_WITH_MANUAL_GAPS" + and geometry["passed_geometry_cases"] == 25 and type(geometry["manual_gaps"]) is int and geometry["manual_gaps"] == 1 + and len(geometry["cases"]) == 26, "Missing/waived native renderer geometry scope") + require(sum(row["status"] == "PASS" for row in geometry["cases"]) == 25 + and len({row["case"] for row in geometry["cases"]}) == 26 + and [row["case"] for row in geometry["cases"] if row["status"] == "MANUAL_GAP_WORLD_REQUIRED"] + == ["hydroturbine-registration"], + "Manual geometry gap was promoted to PASS") + require(document["recipe_registry_ids_match_actual_login"] is True, "Recipe packets lack actual login registry identity") + return dict(status="PASS", scope="CONNECTED_CLIENT_GUI_RESOURCE_PROBE_NOT_MANUAL_ACCEPTANCE", images=images, + projectionCalibration=calibration, + experimentalFixtureAction=fixture_action, + actualWorldOilAndReload=world_oil, + framebufferChecks=framebuffer_checks, garbledBackgroundControls=39, blankAndGarbledTankControls=48, + manualRequired=["hydroturbine-world-orientation-spin"]) + + +def _run(args, owned): + inputs = preflight(args, [Path(__file__), Path(client_diagnostics.__file__), args.launch, args.client_code, args.probe, args.recipe_exports, + args.client_world_receipt, + native.RIG / "client-resources/resources/gui-label-expectations.json"]) + native.validate_hash(args.client_code, args.client_code_sha256) + code = native.read_json(args.client_code) + diagnostic_bindings = client_diagnostics.verify_bindings(code) + for row in diagnostic_bindings: + inputs.hashes[row["path"]] = row["sha256"] + require(code == inspect_launch(args.launch), "Client SDK differs from the independently sealed inventory") + native.validate_hash(args.probe, args.probe_sha256) + with ZipFile(args.probe) as jar: + metadata = tomllib.loads(jar.read("META-INF/mods.toml").decode("utf-8")) + require(metadata["loaderVersion"] == "[65,66)" and metadata["mods"][0]["modId"] == "advantageclientresources", + "Wrong native client probe") + deps = {row["modId"]: row["versionRange"] for row in metadata["dependencies"]["advantageclientresources"]} + require(deps == {"basepower": "[2.4.17.2602001]", "forge": "[65.1.0]", "minecraft": "[26.2]"}, "Stale client probe dependencies") + for row in [*code["archives"], code["metadata"], code["assetIndex"]]: + inputs.hashes[row["path"]] = row["sha256"] + source_world = args.client_world.resolve(strict=True) + require_disjoint(args.output.resolve(), source_world) + world_before = verify_world_source(source_world, native.read_json(args.client_world_receipt), + args.profile_sha256, inputs.mods) + if args.preflight_only: + return dict(status="READY_NOT_RUN", qualificationAllowed=False, **inputs.receipt()) + runtime = output_path(args.output) + runtime.mkdir(parents=True, exist_ok=False) + owned["runtime"] = runtime + shutil.copy2(Path(__file__), runtime / "native262-client-source.py") + (runtime / "mods").mkdir() + expected = {} + for key, (_, path, sha) in inputs.mods.items(): + shutil.copy2(path, runtime / "mods" / path.name) + expected[path.name] = sha + shutil.copy2(args.probe, runtime / "mods/advantageclientresources.jar") + expected["advantageclientresources.jar"] = args.probe_sha256.upper() + exports = native.read_json(args.recipe_exports) + require(exports["status"] == "PASS" and exports["profileSha256"] == args.profile_sha256.upper() + and exports["target_jars"] == inputs.receipt()["target_jars"], "Recipe exports lack this frozen family binding") + fixture_root = Path(exports["recipe_fixture"]["directory"]) + from native262_resources import verify_packet_exports + packet_records = verify_packet_exports(fixture_root) + destination = runtime / "recipe-book-fixtures" + destination.mkdir() + for row in packet_records: + for path in (Path(row["packet"]), Path(row["packet"]).with_suffix(".json"), Path(row["tags"])): + require_disjoint(runtime, path.resolve(strict=True)) + inputs.hashes[str(path.resolve())] = digest(path) + shutil.copy2(path, destination / path.name) + level = fixture_root / "AdvantageWorksMigration/level.dat" + require_disjoint(runtime, level.resolve(strict=True)) + inputs.hashes[str(level.resolve())] = digest(level) + shutil.copy2(level, destination / "server-registries.dat") + shutil.copytree(source_world, runtime / "saves" / CLIENT_WORLD) + write_json(runtime / "client-world-source-files.json", world_before) + write_json(runtime / "client-fixture-policy.json", fixture_policy(runtime, args.profile, args.profile_sha256, + args.client_world_receipt, source_world)) + (runtime / "options.txt").write_text("onboardAccessibility:false\n", encoding="utf-8") + program = list(code["program"]) + program[3] = str(runtime / "bootstrap-cache") + program += ["--quickPlaySingleplayer", CLIENT_WORLD, "--offlineDeveloperMode", "--username", validate_username(CLIENT_USERNAME)] + logging_jar = [Path(row["path"]) for row in code["archives"] if Path(row["path"]).name == "fmlloader-26.2-65.1.0.jar"] + require(len(logging_jar) == 1, "Missing exact native logging configuration source") + logging = native.logging_configuration(runtime, logging_jar[0]) + environment = native.native_launch_environment() + environment.update(code["environment"]) + command = [str(inputs.java), *code["vm"], "-Xmx2G", "-Dadvantageworks.clientResources=true", + "-Dadvantageworks.requireExtensions=true", "-Dadvantageworks.machineGuiLayouts=true", + "-Dadvantageworks.childRendererGeometry=true", "-Dlog4j.configurationFile=" + str(logging), + "-cp", os.pathsep.join(row["path"] for row in code["archives"]), "net.minecraftforge.launcher.Main", *program] + report = dict(status="RUNNING", **inputs.receipt(), profileSha256=args.profile_sha256.upper(), + clientCodeSha256=args.client_code_sha256.upper(), command=command, runtimeJars=expected, + sourceWorld=str(source_world), sourceWorldReceipt=str(args.client_world_receipt), + worldSourceLedgerSha256=digest(runtime / "client-world-source-files.json")) + report["experimentalFixturePolicy"] = dict(file="client-fixture-policy.json", sha256=digest(runtime / "client-fixture-policy.json")) + write_json(runtime / "client-input.json", report) + process = None + try: + require(type(args.timeout) is int and 30 <= args.timeout <= 1800, "Bound client process timeout") + with (runtime / "launcher.stdout.log").open("wb") as stdout, (runtime / "launcher.stderr.log").open("wb") as stderr: + process = subprocess.Popen(command, cwd=runtime, env=environment, stdout=stdout, stderr=stderr, + creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, + start_new_session=os.name != "nt") + report["pid"] = process.pid + require(process.wait(timeout=args.timeout) == 0, "Native client exited nonzero") + labels = native.read_json(native.RIG / "client-resources/resources/gui-label-expectations.json") + result = native.read_json(runtime / "client-resources.json") + report["nativeProducer"] = result + verify_producer(result) + report["verification"] = validate_result(result, runtime, labels, source_references(result, inputs.mods)) + report["clientLogAudit"] = [] + for path in (runtime / "logs/debug.log", runtime / "logs/latest.log", runtime / "launcher.stdout.log", runtime / "launcher.stderr.log"): + errors, diagnostics = client_diagnostics.classify(path.read_text(encoding="utf-8", errors="strict"), native.previous_log_findings) + report["clientLogAudit"].append(dict(path=str(path), sha256=digest(path), errors=errors, diagnostics=diagnostics)) + require(not errors, "Native client log has unclassified failures: " + str(errors)) + report["clientLogPrimaryBindings"] = diagnostic_bindings + report["status"] = "PASS" + except BaseException as failure: + report.update(status="FAIL", error=repr(failure)) + raise + finally: + try: + if process is not None and process.poll() is None: + if os.name == "nt": + subprocess.run(["taskkill", "/PID", str(process.pid), "/T", "/F"], check=True, capture_output=True) + else: + import signal + os.killpg(process.pid, signal.SIGKILL) + process.wait(timeout=30) + require({path.name: digest(path) for path in (runtime / "mods").iterdir()} == expected, "Client runtime jars changed") + report["vanillaResourceControls"] = verify_vanilla_resources(runtime, code) + report["nativeFramebufferFreeze"] = freeze_native_frames(runtime) + if report["status"] == "PASS": + require(report["vanillaResourceControls"]["status"] == "PASS", "Missing native vanilla resource-root control") + require(len(report["nativeFramebufferFreeze"]["frames"]) >= 1207, + "Incomplete native GUI framebuffer inventory") + report["sourceUnchanged"] = inputs.unchanged() + require(fingerprint(source_world) == world_before, "Protected stopped client-source world changed") + report["sourceWorldUnchanged"] = True + if report["status"] == "PASS": + stopped = runtime / "saves" / CLIENT_WORLD + layout.read_level(stopped) + report["stoppedClientLevelSha256"] = digest(stopped / "level.dat") + except BaseException as failure: + report.update(status="FAIL", custodyError=repr(failure)) + raise + finally: + write_json(runtime / RESULT, report) + return report + + +def run(args): + owned = {} + try: + return _run(args, owned) + except BaseException as failure: + if "runtime" in owned: + runtime = owned["runtime"] + failed = dict(status="FAIL", minecraft=native.MINECRAFT, forge=native.FORGE, + scope="CLIENT_PREPARATION_OR_LAUNCH", error=repr(failure)) + write_json(runtime / "runner-failure.json", failed) + if not (runtime / RESULT).exists(): + write_json(runtime / RESULT, failed) + raise + + +def main(): + parser = arguments(__doc__) + for key in ("launch", "client-code", "probe", "recipe-exports", "client-world", "client-world-receipt"): + parser.add_argument("--" + key, required=True, type=Path) + for key in ("client-code-sha256", "probe-sha256"): + parser.add_argument("--" + key, required=True) + parser.add_argument("--timeout", type=int, default=600) + print(run(parser.parse_args())["status"], flush=True) + + +if __name__ == "__main__": + main() diff --git a/test-rig/migration/native262_client_diagnostics.py b/test-rig/migration/native262_client_diagnostics.py new file mode 100644 index 00000000..32b96381 --- /dev/null +++ b/test-rig/migration/native262_client_diagnostics.py @@ -0,0 +1,116 @@ +"""Bind the Forge 65 client inputs without inheriting earlier log exemptions.""" + +import hashlib +import json +import re +from pathlib import Path +from zipfile import ZipFile + +from matrix import digest +from native262_platform_diagnostics import HOST_CONTROL_SHA, OSHI_JAR_SHA + +SDK_NAME = "forge-26.2-65.1.0.jar" +SDK_PAYLOAD_SHA = "3E10711C65C69662E1AEFE92F07A9B85BB1EF92DEDD429CF856935D6CA81768C" +BINARIES = { + SDK_NAME: SDK_PAYLOAD_SHA, + "netty-common-4.2.15.Final.jar": "78206AA7F6D197CAA926291408C01889B6B910CA0F74017D3FCBDACCF9562959", + "netty-buffer-4.2.15.Final.jar": "1361FD9C9BA85B9831CF54A1B2E45DDC3CE34A768931726C099D3F5EF0EFE4A3", + "datafixerupper-10.0.21.jar": "3547CB1DA8993FB7EBD42494D0E78997D2CFBE90C421F5EFD022863E6909941E", + "oshi-core-6.9.0.jar": OSHI_JAR_SHA, +} +SOURCES = { + SDK_NAME: "D06D76658C43DC91D8EB5BF0C1A519E261B84AAC17940E64E7050449D9DA86D9", + "netty-common-4.2.15.Final.jar": "DE720A128534BA1072B354C863D671AB1999900A8FDDAADC4A68EE74C2E33FD6", + "netty-buffer-4.2.15.Final.jar": "13259B636C4A91C0F34A8536D621178FBA302F9BA61F3E4AAEC3A17CA291DC2A", +} + + +def sdk_payload_sha(path): + with ZipFile(path) as archive: + entries = [entry for entry in archive.infolist() if not entry.is_dir()] + if len({entry.filename for entry in entries}) != len(entries): + raise AssertionError("Ambiguous duplicate SDK payload") + payloads = {entry.filename: hashlib.sha256(archive.read(entry)).hexdigest().upper() for entry in entries} + return hashlib.sha256(json.dumps(payloads, sort_keys=True, separators=(",", ":")).encode("ascii")).hexdigest().upper() + + +def verify_sdk_payload(path): + if sdk_payload_sha(path) != SDK_PAYLOAD_SHA: + raise AssertionError("Client SDK payload differs from the sealed target") + + +def verify_bindings(code): + found = [row for row in code["archives"] if Path(row["path"]).name in BINARIES] + if len(found) != len(BINARIES) or {Path(row["path"]).name for row in found} != set(BINARIES): + raise AssertionError("Missing/duplicate exact client dependencies") + for row in found: + path = Path(row["path"]) + if digest(path) != row["sha256"]: + raise AssertionError("Client dependency bytes differ from their sealed input") + if path.name == SDK_NAME: + verify_sdk_payload(path) + elif row["sha256"] != BINARIES[path.name]: + raise AssertionError("Client dependency bytes differ from the target") + records = [dict(row) for row in found] + for binary, source_sha in SOURCES.items(): + path = Path(next(row["path"] for row in found if Path(row["path"]).name == binary)) + name = path.stem + "-sources.jar" + candidates = [path.with_name(name)] if binary == SDK_NAME else list(path.parent.parent.glob("*/" + name)) + if len(candidates) != 1 or digest(candidates[0]) != source_sha: + raise AssertionError("Exact client primary-source archive absent/changed") + records.append(dict(path=str(candidates[0].resolve()), sha256=source_sha)) + return records + + +FIXTURE = Path(__file__).with_name("fixtures") / "native262-client-platform-sequences.json" +FIXTURE_SHA = "2DF3B239D7A566E96A9DB04E958B574359F947E067E842F3F94824FB104440D5" +HEADER = re.compile(r"^\[[^\]]+\] \[([^\]]+)/([A-Z]+)\] \[([^\]]+)\]: (.*)$") +ANSI = re.compile(r"\x1b\[[0-9;]*m") + + +def variants(): + if digest(FIXTURE) != FIXTURE_SHA: + raise AssertionError("Reviewed native client platform sequences changed") + return json.loads(FIXTURE.read_text(encoding="utf-8"))["variants"] + + +def classify(text, fallback): + blocks = [] + for line in ANSI.sub("", text).splitlines(): + if HEADER.fullmatch(line) or not blocks: + blocks.append([line]) + else: + blocks[-1].append(line) + expected = variants() + names = {row["logger"] for rows in expected.values() for row in rows} + selected = [] + for index, block in enumerate(blocks): + header = HEADER.fullmatch(block[0]) + if not header or header[3] not in names: + continue + if "HkeyPerformanceDataUtil" in header[3] and header[2] == "DEBUG": + continue + if "SystemReport" in header[3] and not header[4].startswith("Failed to get system info for Process "): + continue + selected.append((index, dict(thread=header[1], level=header[2], logger=header[3], message=header[4], + body=[line for line in block[1:] if line.strip() and line.strip() != ">"]))) + if not selected: + return fallback(text) + actual = [row for _, row in selected] + match = next((key for key, rows in expected.items() if actual == rows), None) + if match is None: + failures, diagnostics = fallback(text) + failures.append("Unproven native262 client platform sequence; exact independent-control-bound messages and stacks required") + return failures, diagnostics + accepted = {index for index, _ in selected} + diagnostics, remaining = [], [] + for index, block in enumerate(blocks): + if index in accepted: + diagnostics.append(dict(classification="INDEPENDENTLY_PROVEN_NATIVE_CLIENT_PLATFORM_DIAGNOSTIC", + fixtureSha256=FIXTURE_SHA, hostControlReceiptSha256=HOST_CONTROL_SHA, + appender=match, rawBlock=block)) + else: + remaining.extend(block) + failures, other = fallback("\n".join(remaining)) + diagnostics.extend(other) + return failures, diagnostics diff --git a/test-rig/migration/native262_endurance.py b/test-rig/migration/native262_endurance.py new file mode 100644 index 00000000..0e896422 --- /dev/null +++ b/test-rig/migration/native262_endurance.py @@ -0,0 +1,307 @@ +"""Measured two-world Forge65 endurance, new-world proof and full cold restart.""" + +import argparse +from collections import Counter +from datetime import datetime +import json +from pathlib import Path +import shutil +import time + +import endurance +from matrix import WORLD, digest, fingerprint, write_json +from native117 import output_path, require_disjoint +from native117_actor_io import bind +from native117_endurance import (NativeEnduranceRig, FRESH_FIXTURE_SEED, POI_POSITION, + configure_new_fresh_fixture) +from native117_broad import read_json +import native12111_endurance as previous +import native262 as native +from native262_layout import read_level, read_worldgen, shared_file +from native262_upgrade import SCOPE as MIGRATION_SCOPE, comparison, copy_source + + +MODES = ("fresh262", "retained-upgrade-native262") +RECEIPT = "native262-endurance-results.json" +EnduranceHold = previous.EnduranceHold +check_hold = previous.check_hold +stop_at_checkpoint = previous.stop_at_checkpoint +measure_batch = bind(previous.measure_batch, native=native) + + +class ThroughputFailure(AssertionError): + pass + + +def interval_metrics(before, after, label): + if type(before.get("worldTime")) is not int or type(after.get("worldTime")) is not int: + raise AssertionError("Missing native integer interval world time") + seconds = (datetime.fromisoformat(after["recordedAtUtc"]) - datetime.fromisoformat(before["recordedAtUtc"])).total_seconds() + ticks = after["worldTime"] - before["worldTime"] + if seconds <= 0 or ticks < 0: + raise AssertionError("Nonmonotonic native interval clocks") + if ticks < 18 * seconds: + raise ThroughputFailure(f"RESOURCE_THROUGHPUT_GATE {label}: {ticks / seconds:.3f} TPS below 18; not a gameplay diagnosis") + return dict(label=label, gameTicks=ticks, elapsedSeconds=seconds, ticksPerSecond=ticks / seconds) + + +class Rig(previous.Rig): + server_type = native.Server + checkpoint = bind(previous.Rig.checkpoint, native=native) + batch = bind(previous.Rig.batch, measure_batch=measure_batch) + + def boot(self, phase): + native.verify_runtime(self.runtime, self.code_inventory, self.expected_jars) + NativeEnduranceRig.boot(self, phase) + + +def world_info(world): + level, envelope = read_level(world), read_worldgen(world) + settings = envelope["data"] + import nbtlib + if type(settings.get("seed")) is not nbtlib.Long or type(settings.get("generate_structures")) is not nbtlib.Byte: + raise AssertionError("Native worldgen seed/structure flag is missing or wrongly typed") + return dict(RandomSeed=int(settings["seed"]), MapFeatures=int(settings["generate_structures"]), + dataVersion=int(level["DataVersion"]), fullWorldgenSnbt=envelope.snbt()) + + +def verify_fresh_fixture_proof(runtime, proof): + endurance.require(proof.get("existing_world_absent") is True and proof.get("status") in ("UNVERIFIED", "PASS"), + "Missing independent new-world isolation proof") + for name, expected in proof["files"].items(): + endurance.require(digest(runtime / name) == expected, "Fresh isolation evidence changed: " + name) + properties = dict(line.split("=", 1) for line in + (runtime / "fresh-fixture-proof/server-before-first-creation.properties").read_text().splitlines() + if line and not line.startswith("#") and "=" in line) + endurance.require(properties.get("generate-structures") == "false" + and properties.get("level-seed") == str(FRESH_FIXTURE_SEED), "Wrong fresh creation settings") + actual = world_info(runtime / "fresh-fixture-proof/world") + endurance.require(actual["MapFeatures"] == 0 and actual["RandomSeed"] == FRESH_FIXTURE_SEED, + "Actual native external worldgen differs from configured fresh world") + endurance.require(actual == proof["actual_world_info"], "Native worldgen/unknown NBT proof changed") + rows = read_json(runtime / "migration-fresh-isolation-before-build.json") + endurance.require(len(rows) == 1 and tuple(rows[0][key] for key in ("x", "y", "z")) == POI_POSITION + and rows[0]["actualName"] == "minecraft:air" and rows[0] == proof["critical_block"], + "Missing exact original composter AIR control") + return actual + + +def prove_fresh_fixture_before_build(rig, configuration): + runtime = rig.runtime + write_json(runtime / "migration-fixture.json", [dict(id="poi-target", x=POI_POSITION[0], y=POI_POSITION[1], z=POI_POSITION[2])]) + rig.server.snapshot("fresh-isolation-before-build") + rig.command("save-all flush", "Saved the game") + root = runtime / "fresh-fixture-proof/world" + root.mkdir() + shutil.copy2(runtime / WORLD / "level.dat", root / "level.dat") + destination = shared_file(root, "minecraft:world_gen_settings") + destination.parent.mkdir(parents=True) + shutil.copy2(shared_file(runtime / WORLD, "minecraft:world_gen_settings"), destination) + files = ["fresh-fixture-proof/server-before-first-creation.properties", "fresh-fixture-proof/world/level.dat", + "fresh-fixture-proof/world/data/minecraft/world_gen_settings.dat", "migration-fresh-isolation-before-build.json"] + proof = dict(configuration, status="UNVERIFIED", actual_world_info=world_info(root), + critical_block=read_json(runtime / files[-1])[0], files={name: digest(runtime / name) for name in files}, + scope="NEW native4903 world before Works build; complete external SavedData preserved; no source edits") + path = runtime / "fresh-fixture-proof.json" + write_json(path, proof) + verify_fresh_fixture_proof(runtime, proof) + proof["status"] = "PASS" + write_json(path, proof) + return proof + + +def require_retained_receipt(prior, profile_sha, inventory_sha): + if (prior.get("status") != "PASS" or prior.get("scope") != MIGRATION_SCOPE or prior.get("route") != "original110" + or prior.get("minecraft") != native.MINECRAFT or prior.get("forge") != native.FORGE + or prior.get("profileSha256") != profile_sha + or inventory_sha not in prior.get("inputHashes", {}).values() + or not all(prior.get(key) is True for key in ("sourceUnchanged", "referenceUnchanged", "seedUnchanged", "inputsUnchanged"))): + raise AssertionError("Endurance requires the full passing original110 migration under the same candidate") + for phase in ("converted", "restarted"): + observation = prior["phases"][phase] + if (observation.get("status") != "PASS" or observation.get("serialized", {}).get("status") != "PASS" + or observation.get("witnesses") != 14 or observation["serialized"].get("witnesses") != 14 + or observation["serialized"].get("nativeDataVersion") != 4903): + raise AssertionError("Missing genuine stopped native migration capture") + if not {"destination", "steamCrusher", "electricFurnace", "readiness"} <= prior["phases"].get("operation", {}).keys(): + raise AssertionError("Hook-only or non-operating migration cannot supply retained endurance") + + +def retained_input(args): + if args.retained is None: + if args.seconds >= 1800: + raise ValueError("Qualified endurance requires the fresh original-world migration first") + return None + if args.retained_receipt is None or args.retained_receipt_sha256 is None: + raise ValueError("Retained runtime needs its explicit passing migration receipt and SHA-256") + native.validate_hash(args.retained_receipt, args.retained_receipt_sha256) + prior = read_json(args.retained_receipt) + require_retained_receipt(prior, digest(args.profile), digest(args.inventory)) + if (Path(prior["runtime"]).resolve() != args.retained + or prior.get("inputHashes", {}).get(str(args.inventory)) != digest(args.inventory) + or digest(args.retained / "migration-restarted.json") != prior["phases"]["restarted"]["snapshotSha256"]): + raise AssertionError("Retained world does not match the actual passing pinned native262 migration") + return fingerprint(args.retained) + + +def run(args): + endurance.require(args.seconds >= 60 and args.seconds % 60 == 0, "Use whole one-minute batches") + root = output_path(args.output) + target = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + java, version = native.java25(args.java) + for port in (args.port, args.port + 1): + native.require_free_port(port) + for protected in (args.profile, args.seed, args.inventory, java, *(item[1] for item in target[2].values())): + require_disjoint(root, protected) + if args.retained: + require_disjoint(root, args.retained) + source_before = retained_input(args) + from native262_endurance_replay import Budget, verify_seal + sealed = verify_seal(args.seal, args.seal_sha256, Budget()) + if sealed["purpose"] != "NATIVE65_ENDURANCE_PRE_RUN_SEAL": + raise ValueError("Producer requires a genuine pre-run seal, never retrospective") + if (sealed["files"].get(str(args.profile)) != digest(args.profile) + or sealed.get("retainedReceipt") != (str(args.retained_receipt) if args.retained_receipt else None)): + raise ValueError("Producer inputs differ from pre-run seal") + seed_before = fingerprint(args.seed) + 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, inputSealSha256=args.seal_sha256) + receipt = root / RECEIPT + write_json(receipt, report) + rigs = [] + try: + cases = tuple(zip(MODES, (None, args.retained))) if args.retained else ((MODES[0], 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} + previous_intervals = {rig.name: rig.documents["W-01"].copy() for rig in rigs} + intervals = {rig.name: [] for rig in rigs} + started, cycles = time.monotonic(), 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) + intervals[rig.name].append(interval_metrics(previous_intervals[rig.name], rig.documents["W-01"], label)) + previous_intervals[rig.name] = rig.documents["W-01"].copy() + 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") + previous_intervals[rig.name] = rig.documents["W-01"].copy() + rig.batch("after-restart") + rig.structure() + rig.seed() + deadline = time.monotonic() + 60 + while time.monotonic() < deadline: + check_hold(root) + time.sleep(min(1, max(0, deadline - time.monotonic()))) + for rig in rigs: + check_hold(root) + statuses = stop_at_checkpoint(rig) + resumed_rate = interval_metrics(previous_intervals[rig.name], rig.documents["W-01"], "restart-resumed") + 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", intervalMeasurements=intervals[rig.name], + minimumIntervalTicksPerSecond=min(row["ticksPerSecond"] for row in intervals[rig.name]), + resumedRate=resumed_rate) + report["status"] = "PASS" if args.seconds >= 1800 else "SMOKE_PASS_NOT_ENDURANCE" + except EnduranceHold as hold: + report.update(status="HELD", holdReason=str(hold)) + except ThroughputFailure as failure: + report.update(status="FAIL", failure=repr(failure), failureCategory="RESOURCE_THROUGHPUT_NOT_GAMEPLAY_DIAGNOSIS") + raise + except BaseException as failure: + report.update(status="FAIL", failure=repr(failure)) + raise + finally: + for rig in rigs: + try: + rig.shutdown() + native.verify_runtime(rig.runtime, rig.code_inventory, rig.expected_jars) + except Exception as failure: + report.update(status="FAIL", shutdown_or_audit_error=repr(failure)) + report["seedUnchanged"] = fingerprint(args.seed) == seed_before + if args.retained: + report["retained_unchanged"] = fingerprint(args.retained) == source_before + if not report["seedUnchanged"] or (args.retained and not report["retained_unchanged"]): + report["status"] = "FAIL" + try: + verify_seal(args.seal, args.seal_sha256, Budget()) + except Exception as failure: + report.update(status="FAIL", sealAuditError=repr(failure)) + write_json(receipt, report) + print(report["status"] + ": " + str(receipt), flush=True) + return 2 if report["status"] == "HELD" else int(report["status"] == "FAIL") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("profile", "seed", "inventory", "output", "java", "seal"): + parser.add_argument("--" + name, required=True, type=lambda value: Path(value).resolve()) + for name in ("inventory-sha256", "seal-sha256"): + parser.add_argument("--" + name, 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", type=lambda value: Path(value).resolve(strict=True)) + parser.add_argument("--retained-receipt-sha256") + parser.add_argument("--port", type=int, required=True) + return run(parser.parse_args()) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native262_endurance_replay.py b/test-rig/migration/native262_endurance_replay.py new file mode 100644 index 00000000..ffcdf1ca --- /dev/null +++ b/test-rig/migration/native262_endurance_replay.py @@ -0,0 +1,342 @@ +"""Genuine preseal, exact stopped freeze and independent Forge65 endurance replay.""" + +import argparse +from contextlib import ExitStack +from datetime import datetime, timezone +import json +from pathlib import Path +import sys + +import nbtlib + +import audit_native1201_endurance as checkpoint_audit +import endurance +from matrix import WORLD +from native117_actor_io import bind +from native117_entity_storage import _decompress, _nbt, _payloads +from native117_machine_saved import state +import native12111_endurance_replay as previous +import native262 as native +import native262_platform_diagnostics as platform_diagnostics +from native262_endurance import MODES, RECEIPT, interval_metrics, measure_batch, verify_fresh_fixture_proof, require_retained_receipt +from native262_layout import DATA_VERSION, region_file, read_worldgen + + +Budget = previous.Budget +require, pin, regular = previous.require, previous.pin, previous.regular +write_new, disjoint, tree = previous.write_new, previous.disjoint, previous.tree +stopped = previous.stopped +MAX_JSON = previous.MAX_JSON +row_nbt, typed_nbt_or_none = previous.row_nbt, previous.typed_nbt_or_none +snapshot = bind(previous.snapshot, native=native) +terrain = bind(previous.terrain, DATA_VERSION=DATA_VERSION) +complete = bind(previous.complete, native=native, MODES=MODES) +feed_audit = bind(previous.feed_audit, native=native) +PRE_RUN = "NATIVE65_ENDURANCE_PRE_RUN_SEAL" +RETROSPECTIVE = "NATIVE65_ENDURANCE_RETROSPECTIVE_SEAL" +FREEZE = "NATIVE65_STOPPED_ENDURANCE_FREEZE" + + +def oracle_paths(): + # Warm the typed-array path before inventorying its imported native dependencies. + nbtlib.IntArray([3, 2, 1]).snbt() + paths = set(previous.oracle_paths()) + paths.update(Path(path).resolve() for path in (sys.executable, getattr(sys, "_base_executable", sys.executable))) + for module in list(sys.modules.values()): + path = getattr(module, "__file__", None) + if path and getattr(module, "__name__", "").startswith("numpy"): + paths.add(Path(path).resolve()) + import numpy + runtime_root = Path(numpy.__file__).resolve().parent.parent + paths.update(path.resolve() for path in (runtime_root / "numpy.libs").glob("*.dll")) + custody = runtime_root / "native262-python-runtime-custody.json" + if custody.is_file(): + paths.add(custody) + return sorted(paths) + + +class Forge65: + MINECRAFT, FORGE = native.MINECRAFT, native.FORGE + verify_runtime = staticmethod(native.audit_staged) + parse_snapshot = staticmethod(snapshot) + + @staticmethod + def require_native_boot(path): + native.require_clean_log(path) + native.require_complete_boot(path.read_text(encoding="utf-8"), stop_sent=True, returncode=0) + + @staticmethod + def verify_batch(runtime, label, documents, definitions): + measured = measure_batch({key: documents[key] for key in ("S-04", "E-03", "W-04", "W-03")}, definitions) + recorded = native.read_json(runtime / f"semantic-batch-{label}.json") + require(measured["status"] == "PASS" and measured == recorded, + f"{runtime.name}/{label}: independent batch measurements disagree") + + +def engine_inputs(path, expected, budget): + pin(budget.read(path), expected) + proof = budget.json(path) + require(proof.get("status") == "STRICT_CLASSIFIER_AND_ORDERED_COLDSTOP_READY" + and proof.get("noJvmFlagsAdded") is True and proof.get("debugNotSuppressed") is True + and proof.get("coreSha256") == budget.digest(Path(native.__file__)) + and proof.get("classifierSha256") == budget.digest(Path(platform_diagnostics.__file__)), + "Engine proof does not describe the actual current reviewed core/classifier") + require(platform_diagnostics.COMMON_JAR_SHA in proof["proofFiles"].values() + and platform_diagnostics.SOURCES_JAR_SHA in proof["proofFiles"].values(), + "Primary Netty source and native binary proof are missing") + require({platform_diagnostics.HOST_CONTROL_SHA, platform_diagnostics.HOST_FIXTURE_SHA, + platform_diagnostics.HOST_FILE_FIXTURE_SHA, platform_diagnostics.NETTY_FIXTURE_SHA, + platform_diagnostics.OSHI_JAR_SHA} <= set(proof["proofFiles"].values()), + "Independent native host control, exact appender fixtures or OSHI binary proof missing") + files = {Path(row["path"]): row["sha256"] for row in proof["sourceClosure"]} + files.update({Path(name): checksum for name, checksum in proof["proofFiles"].items()}) + for name, checksum in files.items(): + pin(budget.read(name), checksum) + return files + + +def seal(profile, profile_sha256, specification, inventory, inventory_sha256, java, budget, *, + retrospective=False, retained_receipt=None, retained_receipt_sha256=None, + engine_receipt=None, engine_receipt_sha256=None): + pin(budget.read(profile), profile_sha256) + pin(budget.read(inventory), inventory_sha256) + document = budget.json(profile) + require(document["minecraftVersion"] == native.MINECRAFT and document["forge"]["version"] == native.FORGE_VERSION, + "Wrong selected native runtime profile") + mods = document["mods"] + require(len(mods) == 6 and {row["id"] for row in mods} == set(native.VERSIONS), "Seal requires exactly six native inputs") + for row in mods: + require(Path(row["path"]).name == row["file"] and row["version"] == native.VERSIONS[row["id"]], + "Artifact identity/version differs") + pin(budget.read(row["path"]), row["sha256"]) + executable, version = native.java25(Path(java)) + native.verify_compile_pin(next(Path(row["path"]) for row in mods if row["id"] == "advantageworkstest"), + next(row["sha256"] for row in mods if row["id"] == "basepower")) + require(engine_receipt is not None and engine_receipt_sha256 is not None, + "Preseal must bind the reviewed platform classifier and primary-source proof") + proof_files = engine_inputs(engine_receipt, engine_receipt_sha256, budget) + retained_files = [] + if retained_receipt is not None: + pin(budget.read(retained_receipt), retained_receipt_sha256) + prior = budget.json(retained_receipt) + require_retained_receipt(prior, profile_sha256.upper(), inventory_sha256.upper()) + retained_files.append(Path(retained_receipt)) + files = [Path(profile), Path(specification), Path(inventory), executable, Path(engine_receipt), + *(Path(row["path"]) for row in mods), *retained_files, *proof_files, *oracle_paths()] + return dict(format=1, purpose=RETROSPECTIVE if retrospective else PRE_RUN, + createdAtUtc=datetime.now(timezone.utc).isoformat(), profile=str(Path(profile).resolve()), + specification=str(Path(specification).resolve()), inventory=str(Path(inventory).resolve()), + java=str(executable), javaVersion=version, + engineReceipt=str(Path(engine_receipt).resolve()), engineReceiptSha256=engine_receipt_sha256.upper(), + retainedReceipt=str(Path(retained_receipt).resolve()) if retained_receipt else None, + files={str(path.resolve()): budget.digest(path) for path in files}, + oracles=[str(path) for path in oracle_paths()]) + + +def verify_seal(path, expected, budget): + pin(budget.read(path), expected) + document = budget.json(path) + require(document.get("format") == 1 and document.get("purpose") in (PRE_RUN, RETROSPECTIVE), "Wrong input seal") + require(document["oracles"] == [str(path) for path in oracle_paths()], "Full transitive oracle module coverage changed") + required = set(document["oracles"]) | {document[key] for key in ("profile", "specification", "inventory", "java", "engineReceipt")} + if document.get("retainedReceipt"): + required.add(document["retainedReceipt"]) + require(required <= document["files"].keys(), "Seal lists an oracle/input without binding its bytes") + require(document["files"][document["engineReceipt"]] == document["engineReceiptSha256"], "Engine receipt pin differs") + proof_files = engine_inputs(Path(document["engineReceipt"]), document["engineReceiptSha256"], budget) + require(all(document["files"].get(str(path.resolve())) == checksum for path, checksum in proof_files.items()), + "Seal omits reviewed engine/source proof bytes") + for name, checksum in document["files"].items(): + pin(budget.read(name), checksum) + return document + + +def custody(sealed, run, budget): + setup = min(datetime.fromisoformat(budget.json( + run / mode / "advantage-works-results/W-01-checkpoint-setup.json")["recordedAtUtc"]) for mode in MODES) + created = datetime.fromisoformat(sealed["createdAtUtc"]) + retrospective = sealed["purpose"] == RETROSPECTIVE + require(retrospective or created < setup, "Seal was not made before setup; retrospective cannot qualify") + return dict(preRunCustody=not retrospective, sealCreatedAtUtc=sealed["createdAtUtc"], earliestSetupAtUtc=setup.isoformat(), + coverageGap="No pre-run oracle custody proof; seal is retrospective" if retrospective else None) + + +def freeze(run, seal_path, seal_sha256, receipt_sha256, output, budget): + run, output = Path(run).resolve(), Path(output).resolve() + sealed = verify_seal(seal_path, seal_sha256, budget) + disjoint(output, run, seal_path, *(Path(name) for name in sealed["files"])) + pin(budget.read(run / RECEIPT), receipt_sha256) + complete(budget.json(run / RECEIPT)) + provenance = custody(sealed, run, budget) + output.mkdir() + result = dict(status="FAIL", format=1, purpose=FREEZE, sealSha256=seal_sha256.upper(), + inputReceiptSha256=receipt_sha256.upper(), source=str(run), custody=provenance) + try: + with ExitStack() as stack: + for mode in MODES: + stack.enter_context(stopped(run / mode, budget)) + files = tree(run, budget) + for name, checksum in files.items(): + data = budget.read(run / name) + pin(data, checksum) + destination = output / "run" / name + destination.parent.mkdir(parents=True, exist_ok=True) + with destination.open("xb") as stream: + stream.write(data) + require(tree(run, budget) == files and tree(output / "run", budget) == files, "Stopped source/frozen copy changed") + verify_seal(seal_path, seal_sha256, budget) + with (output / "seal.json").open("xb") as output_seal: + output_seal.write(budget.read(seal_path)) + result.update(status="PASS", files=files) + except BaseException as failure: + result["failure"] = repr(failure) + raise + finally: + write_new(output / "freeze.json", result) + return result + + +def raw_final(runtime, budget): + world = runtime / WORLD + root = _nbt(_decompress(budget.read(world / "level.dat", MAX_JSON), 1)) + version = root["Data"].get("DataVersion") + require(type(version) is nbtlib.Int and int(version) == DATA_VERSION, "Wrong stopped native level DataVersion") + # Absence/unknown fields stay in the actual separate SavedData, never synthesized from level.dat. + read_worldgen(world) + grouped, count = {}, 0 + for station in endurance.CHECKED: + document = budget.json(runtime / "advantage-works-results" / f"{station}-checkpoint-restart-resumed.json") + snapshot(document) + dimension = document.get("dimension", "minecraft:overworld") + for row in document["snapshot"]: + x, _, z = row["pos"] + grouped.setdefault((dimension, x // 16, z // 16), []).append(row) + count += 1 + regions, differences = {}, [] + for (dimension, cx, cz), rows in grouped.items(): + budget.check() + path = region_file(world, dimension, cx, cz) + if path not in regions: + regions[path] = dict(_payloads(budget.read(path), cx // 32, cz // 32)) + require((cx, cz) in regions[path], "Missing stopped terrain chunk") + sections, tiles = terrain(regions[path][cx, cz], (cx, cz)) + for row in rows: + x, y, z = pos = tuple(row["pos"]) + palette = sections.get(y // 16) + actual = palette[((y & 15) << 8) | ((z & 15) << 4) | (x & 15)] if palette else {"Name": "minecraft:air"} + actual = dict(Name=actual["Name"], Properties=actual.get("Properties", {})) + if actual != state(row) or typed_nbt_or_none(row_nbt(row)) != typed_nbt_or_none(tiles.get(pos)): + differences.append(dict(pos=list(pos), dimension=dimension, checkpoint=row, + savedState=actual, savedNbt=typed_nbt_or_none(tiles.get(pos)))) + require(not differences, "Stopped raw state/component mismatch: " + json.dumps(differences[:3])) + return dict(status="PASS", dataVersion=DATA_VERSION, comparedRows=count, comparedChunks=len(grouped)) + + +def interval_audit(runtime, report, budget): + station = runtime / "advantage-works-results" + before = budget.json(station / "W-01-checkpoint-setup.json") + measured = [] + for index in range(1, 31): + label = f"cycle-{index:03}" + after = budget.json(station / f"W-01-checkpoint-{label}.json") + measured.append(interval_metrics(before, after, label)) + before = after + resumed = interval_metrics(budget.json(station / "W-01-checkpoint-after-restart.json"), + budget.json(station / "W-01-checkpoint-restart-resumed.json"), "restart-resumed") + require(measured == report.get("intervalMeasurements") and resumed == report.get("resumedRate") + and min(row["ticksPerSecond"] for row in measured) == report.get("minimumIntervalTicksPerSecond"), + "Recorded per-interval TPS differs from native checkpoint clocks") + return dict(status="PASS", measuredIntervals=len(measured), + minimumTicksPerSecond=min(row["ticksPerSecond"] for row in measured), resumed=resumed) + + +def replay(frozen, freeze_sha256, seal_sha256, budget): + frozen = Path(frozen).resolve() + pin(budget.read(frozen / "freeze.json"), freeze_sha256) + manifest = budget.json(frozen / "freeze.json") + require(manifest.get("status") == "PASS" and manifest.get("format") == 1 + and manifest.get("purpose") == FREEZE and manifest.get("sealSha256") == seal_sha256.upper(), + "Incomplete or different frozen evidence") + sealed = verify_seal(frozen / "seal.json", seal_sha256, budget) + run = frozen / "run" + require(tree(run, budget) == manifest["files"], "Frozen file inventory changed") + pin(budget.read(run / RECEIPT), manifest["inputReceiptSha256"]) + report = budget.json(run / RECEIPT) + complete(report) + provenance = custody(sealed, run, budget) + require(manifest.get("custody") == provenance, "Frozen custody attestation changed") + require(report["profileSha256"] == sealed["files"][sealed["profile"]] + and report["stationDefinitionSha256"] == sealed["files"][sealed["specification"]] + and report["inventorySha256"] == sealed["files"][sealed["inventory"]] + and report["javaSha256"] == sealed["files"][sealed["java"]] and report["javaVersion"] == sealed["javaVersion"] + and report["inputSealSha256"].upper() == seal_sha256.upper() + and report["jars"] == {row["id"]: row["sha256"].upper() for row in budget.json(sealed["profile"])["mods"]}, + "Producer differs from pre-run seal") + verify_fresh_fixture_proof(run / MODES[0], report["fresh_fixture_proof"]) + + class BoundedForge65(Forge65): + @staticmethod + def verify_batch(runtime, label, documents, definitions): + budget.check() + Forge65.verify_batch(runtime, label, documents, definitions) + + replay_checkpoints = bind(checkpoint_audit.replay, read_json=budget.json, digest=budget.digest) + measured = replay_checkpoints(run, Path(sealed["profile"]), Path(sealed["specification"]), adapter=BoundedForge65) + raw, feeds, rates = {}, {}, {} + for mode in MODES: + inventory = budget.json(run / mode / native.INVENTORY) + require(inventory == budget.json(sealed["inventory"]), "Runtime code inventory differs from sealed seed") + require(not list((run / mode).glob("native*-launch-environment-*.json")), "Diagnostic launch cannot qualify") + raw[mode], feeds[mode] = raw_final(run / mode, budget), feed_audit(run / mode, budget) + rates[mode] = interval_audit(run / mode, report["profiles"][mode], budget) + require(tree(run, budget) == manifest["files"], "Frozen files changed during replay") + verify_seal(frozen / "seal.json", seal_sha256, budget) + return dict(status="PASS" if provenance["preRunCustody"] else "PASS_RETROSPECTIVE_REPLAY_NOT_PRE_RUN_CUSTODY", + qualificationAllowed=provenance["preRunCustody"], custody=provenance, + scope="Independent stopped native65 raw and every-batch replay; not client/player-conversion acceptance", + inputFreezeSha256=freeze_sha256.upper(), inputSealSha256=seal_sha256.upper(), + inputReceiptSha256=manifest["inputReceiptSha256"], checkpointReplay=measured, stoppedRaw=raw, nativeFeeds=feeds, + intervalThroughput=rates) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + make_seal = sub.add_parser("seal") + for name in ("profile", "specification", "inventory", "java", "output"): + make_seal.add_argument("--" + name, required=True, type=Path) + make_seal.add_argument("--engine-receipt", required=True, type=Path) + for name in ("profile-sha256", "inventory-sha256"): + make_seal.add_argument("--" + name, required=True) + make_seal.add_argument("--engine-receipt-sha256", required=True) + make_seal.add_argument("--retained-receipt", type=Path) + make_seal.add_argument("--retained-receipt-sha256") + make_seal.add_argument("--retrospective", action="store_true") + make_freeze = sub.add_parser("freeze") + for name in ("run", "seal", "output"): + make_freeze.add_argument("--" + name, required=True, type=Path) + for name in ("seal-sha256", "receipt-sha256"): + make_freeze.add_argument("--" + name, required=True) + make_replay = sub.add_parser("replay") + for name in ("frozen", "output"): + make_replay.add_argument("--" + name, required=True, type=Path) + for name in ("freeze-sha256", "seal-sha256"): + make_replay.add_argument("--" + name, required=True) + args = parser.parse_args() + budget = Budget() + if args.command == "seal": + disjoint(args.output, args.profile, args.specification, args.inventory, args.java) + result = seal(args.profile, args.profile_sha256, args.specification, args.inventory, args.inventory_sha256, + args.java, budget, retrospective=args.retrospective, retained_receipt=args.retained_receipt, + retained_receipt_sha256=args.retained_receipt_sha256, engine_receipt=args.engine_receipt, + engine_receipt_sha256=args.engine_receipt_sha256) + write_new(args.output, result) + elif args.command == "freeze": + freeze(args.run, args.seal, args.seal_sha256, args.receipt_sha256, args.output, budget) + else: + disjoint(args.output, args.frozen) + write_new(args.output, replay(args.frozen, args.freeze_sha256, args.seal_sha256, budget)) + + +if __name__ == "__main__": + main() diff --git a/test-rig/migration/native262_engine_proof.py b/test-rig/migration/native262_engine_proof.py new file mode 100644 index 00000000..48eed174 --- /dev/null +++ b/test-rig/migration/native262_engine_proof.py @@ -0,0 +1,80 @@ +"""Bind independent native platform controls and actual ordered server cold stops.""" + +import argparse +from datetime import datetime, timezone +from pathlib import Path +import shutil +from zipfile import ZipFile + +from matrix import digest, write_json +import native262 as native +import native262_platform_diagnostics as diagnostics +import native262_endurance_replay as replay + + +def run(args): + native.validate_hash(args.migration, args.migration_sha256) + prior = native.read_json(args.migration) + if prior.get("status") != "PASS" or prior.get("scope") != "MACHINE_MIGRATION_STOPPED_STORAGE_AND_PLAYER_AUTHORITY": + raise AssertionError("Platform proof requires the completed actual migration and cold restart") + native.validate_hash(args.host / "receipt.json", diagnostics.HOST_CONTROL_SHA) + host = native.read_json(args.host / "receipt.json") + if (host.get("minecraftVersion") != "26.2" or host.get("targetDataVersion") != 4903 + or not all(host.get(key) is True for key in ("noPower", "noHarness", "noWorld", "noServer", "noTicks", + "currentProcessMissing", "inheritedJvmEnvironmentAbsent"))): + raise AssertionError("Host control lacks independent native runtime proof") + netty = (args.netty / "runtime.log").read_text(encoding="utf-8") + if "NETTY_JAVA25_CONTROL_PASS noPower=true noHarness=true noMinecraft=true namedModule=io.netty.common" not in netty: + raise AssertionError("New native Netty binary was not independently exercised") + native.validate_hash(args.netty_sources, diagnostics.SOURCES_JAR_SHA) + native.validate_hash(args.netty_binary, diagnostics.COMMON_JAR_SHA) + native.validate_hash(args.oshi_binary, diagnostics.OSHI_JAR_SHA) + with ZipFile(args.sdk_sources) as source: + import hashlib + if hashlib.sha256(source.read("net/minecraft/SystemReport.java")).hexdigest().upper() != diagnostics.SYSTEM_REPORT_SOURCE_SHA: + raise AssertionError("Native SystemReport primary source differs") + runtime = Path(prior["runtime"]) + proof_files = [args.migration, args.host / "receipt.json", args.host / "native-system-report.txt", args.host_log, + args.netty / "runtime.log", args.netty / "command.json", args.netty_sources, args.netty_binary, + args.oshi_binary, args.sdk_sources, diagnostics.HOST_FIXTURE, diagnostics.HOST_FILE_FIXTURE, + diagnostics.NETTY_FIXTURE, Path(__file__).resolve(), + native.RIG / "platform-controls/NativeSystemReportPlatformControl.java", + native.RIG / "platform-controls/NettyJava25PlatformControl.java", args.focused_tests] + for label in ("converted", "restarted"): + if prior["phases"][label]["status"] != "PASS": + raise AssertionError("Missing stopped native migration phase") + for prefix in ("console", "latest", "debug"): + path = runtime / f"{prefix}-{label}.log" + native.require_clean_log(path) + proof_files.append(path) + native.require_complete_boot((runtime / f"console-{label}.log").read_text(encoding="utf-8"), + stop_sent=True, returncode=0) + proof_files.append(runtime / f"native65-process-{label}.json") + if not args.focused_tests.read_text(encoding="utf-8").rstrip().endswith("OK"): + raise AssertionError("Focused positive and negative classifier checks did not pass") + args.output.mkdir(parents=True, exist_ok=False) + closure = [] + for index, path in enumerate(replay.oracle_paths()): + frozen = args.output / "sources" / f"{index:03}-{path.name}" + frozen.parent.mkdir(exist_ok=True) + shutil.copy2(path, frozen) + if digest(path) != digest(frozen): + raise AssertionError("Platform source closure copy changed") + closure.append(dict(path=str(path), sha256=digest(path), frozen=str(frozen))) + result = dict(status="STRICT_CLASSIFIER_AND_ORDERED_COLDSTOP_READY", qualificationAllowed=False, + minecraft="26.2", forge="65.1.0", createdUtc=datetime.now(timezone.utc).isoformat(), + debugNotSuppressed=True, noJvmFlagsAdded=True, rawFailedEvidenceUntouched=True, + sourceClosure=closure, proofFiles={str(path): digest(path) for path in proof_files}, + coreSha256=digest(Path(native.__file__)), classifierSha256=digest(Path(diagnostics.__file__)), + scope="Native host/capability diagnostics and owned cold-stop readiness only; not endurance proof") + write_json(args.output / "receipt.json", result) + return result + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + for key in ("migration", "host", "host-log", "netty", "netty-sources", "netty-binary", "oshi-binary", + "sdk-sources", "focused-tests", "output"): + parser.add_argument("--" + key, type=lambda value: Path(value).resolve(), required=True) + parser.add_argument("--migration-sha256", required=True) + run(parser.parse_args()) diff --git a/test-rig/migration/native262_interop.py b/test-rig/migration/native262_interop.py new file mode 100644 index 00000000..251bc41d --- /dev/null +++ b/test-rig/migration/native262_interop.py @@ -0,0 +1,87 @@ +"""Native26 capability, locked automation, portable tank and survival oil-bucket checks.""" + +from pathlib import Path +import traceback + +from matrix import digest, write_json +from native117_interop import require_checks, require_probe_order +from native117_readiness import await_ready +import native117_interop as contracts +import native12111 as previous +import native262 as native +from native262_resources import arguments, preflight + + +RECEIPT = "native262-interop-results.json" + + +def validate_observation(document, phase, readiness): + require_probe_order(document, readiness) + require_checks(document, phase) + previous.require_shared_crushing(document) + + +def run(args): + if not 1024 <= args.port < 65535: + raise ValueError("Use an unprivileged localhost port") + inputs = preflight(args, [Path(__file__), Path(previous.__file__), Path(contracts.__file__)]) + if args.preflight_only: + return dict(status="READY_NOT_RUN", qualificationAllowed=False, **inputs.receipt()) + runtime = args.output.resolve() + expected = native.stage(runtime, *inputs.target, args.port) + (runtime / "advantage-native-oil-bucket.allow").touch() + report = dict(status="RUNNING", scope="PACKAGED_NATIVE_INTERFACES_AND_CONTAINER_RESTART", + qualificationStatus="EXPLORATORY_UNQUALIFIED", phases={}, profileSha256=digest(args.profile), + inventorySha256=digest(args.inventory), jars=expected, **inputs.receipt()) + receipt = runtime / RECEIPT + write_json(receipt, report) + server = None + try: + for phase in ("exercise", "restored"): + native.verify_runtime(runtime, inputs.target[1], expected) + server = native.Server(runtime, inputs.java, native.LAUNCHER, phase) + server.send("forceload add 1023 1023 1100 1030") + ready = await_ready(server, runtime, (63, 63, 68, 64), f"interop-{phase}", minecraft=native.MINECRAFT) + server.send(f"advinterop {phase}") + server.wait(f"ADVINTEROP COMPLETE {phase}", 120) + document = native.read_json(runtime / "advantage-works-results" / f"native-interop-{phase}.json") + report["phases"][phase] = dict(readiness=ready, observation=document) + validate_observation(document, phase, ready) + if phase == "exercise": + server.send("advoilbuckets") + server.wait("ADVOILBUCKETS COMPLETE", 120) + buckets = native.read_json(runtime / "advantage-works-results/native-oil-buckets.json") + report["phases"][phase]["oilBuckets"] = buckets + previous.require_oil_buckets(buckets, ready) + server.send("save-all flush") + server.wait("Saved the game", 30) + server.close() + server = None + 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: + if server is not None: + server.close() + native.verify_runtime(runtime, inputs.target[1], expected) + report["inputsUnchanged"] = inputs.unchanged() + except BaseException as failure: + report.update(status="FAIL", shutdownOrCustodyError=repr(failure)) + raise + finally: + write_json(receipt, report) + return report + + +def main(): + args = arguments(__doc__).parse_args() + result = run(args) + print(result["status"], flush=True) + return int(result["status"] not in ("PASS", "READY_NOT_RUN")) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native262_layout.py b/test-rig/migration/native262_layout.py new file mode 100644 index 00000000..dd4f6b22 --- /dev/null +++ b/test-rig/migration/native262_layout.py @@ -0,0 +1,128 @@ +"""Explicit 26.2 target paths and lossless stopped-save readers, never source fallbacks.""" + +import copy +import gzip +import io +from pathlib import Path +import re +import uuid + +import nbtlib + + +DATA_VERSION = 4903 +VANILLA_DIMENSIONS = ("minecraft:overworld", "minecraft:the_nether", "minecraft:the_end") +STORAGE_KINDS = ("region", "entities", "poi") +MAX_NBT_BYTES = 64 * 1024 * 1024 + + +def resource_parts(key): + if not isinstance(key, str) or not re.fullmatch(r"[a-z0-9_.-]+:[a-z0-9_./-]+", key): + raise ValueError("Expected an explicit namespaced target resource key") + namespace, path = key.split(":") + if namespace in (".", "..") or any(part in ("", ".", "..") for part in path.split("/")): + raise ValueError("Unsafe target resource path") + return namespace, path + + +def dimension_directory(world, key): + namespace, path = resource_parts(key) + return Path(world) / "dimensions" / namespace / path + + +def region_file(world, dimension, chunk_x, chunk_z, kind="region"): + if type(chunk_x) is not int or type(chunk_z) is not int or kind not in STORAGE_KINDS: + raise ValueError("Expected integer chunk coordinates and native region/entities/poi storage") + return dimension_directory(world, dimension) / kind / f"r.{chunk_x // 32}.{chunk_z // 32}.mca" + + +def shared_file(world, key): + namespace, path = resource_parts(key) + return Path(world) / "data" / namespace / (path + ".dat") + + +def player_identity(value): + if isinstance(value, uuid.UUID): + return value + if not isinstance(value, str): + raise ValueError("Expected an explicit canonical player UUID") + try: + identity = uuid.UUID(value) + except ValueError as error: + raise ValueError("Expected an explicit canonical player UUID") from error + if str(identity) != value: + raise ValueError("Player UUID must be canonical, not a filename alias") + return identity + + +def player_file(world, identity): + return Path(world) / "players/data" / (str(player_identity(identity)) + ".dat") + + +def _load(path): + path = Path(path) + for part in (path, *path.parents): + if part.is_symlink() or getattr(part, "is_junction", lambda: False)(): + raise ValueError("Aliased target save path") + if path.stat().st_nlink != 1 or path.stat().st_size > MAX_NBT_BYTES: + raise ValueError("Aliased or oversized target save file") + raw = path.read_bytes() + if raw.startswith(b"\x1f\x8b"): + with gzip.GzipFile(fileobj=io.BytesIO(raw)) as stream: + raw = stream.read(MAX_NBT_BYTES + 1) + if len(raw) > MAX_NBT_BYTES: + raise ValueError("Expanded target save exceeds the NBT budget") + stream = io.BytesIO(raw) + result = nbtlib.File.parse(stream) + if stream.read(1): + raise ValueError("Trailing data in target NBT file") + return result + + +def _version(tag): + value = tag.get("DataVersion") + if type(value) is not nbtlib.Int or int(value) != DATA_VERSION: + raise ValueError("Expected native 26.2 DataVersion 4903, without source coercion") + + +def read_level(world): + root = _load(Path(world) / "level.dat") + data = root.get("Data") + if not isinstance(data, nbtlib.Compound): + raise ValueError("Target level.dat lacks its native Data compound") + _version(data) + return copy.deepcopy(data) + + +def read_worldgen(world): + read_level(world) + root = _load(shared_file(world, "minecraft:world_gen_settings")) + _version(root) + if not isinstance(root.get("data"), nbtlib.Compound): + raise ValueError("Target worldgen SavedData lacks its native data compound") + return copy.deepcopy(root) + + +def uuid_tag(value): + if type(value) is not nbtlib.IntArray or len(value) != 4: + raise ValueError("Native player authority must be a four-int UUID") + number = 0 + for word in value: + number = (number << 32) | (int(word) & 0xffffffff) + return uuid.UUID(int=number) + + +def singleplayer_uuid(world): + data = read_level(world) + if "Player" in data: + raise ValueError("Embedded Player is historical authority, not native 26 player storage") + return uuid_tag(data["singleplayer_uuid"]) if "singleplayer_uuid" in data else None + + +def read_player(world, identity): + identity = player_identity(identity) + data = _load(player_file(world, identity)) + _version(data) + if uuid_tag(data.get("UUID")) != identity: + raise ValueError("Saved native player UUID differs from its authoritative path") + return copy.deepcopy(data) diff --git a/test-rig/migration/native262_legacy_alternatives.py b/test-rig/migration/native262_legacy_alternatives.py new file mode 100644 index 00000000..0e58ee17 --- /dev/null +++ b/test-rig/migration/native262_legacy_alternatives.py @@ -0,0 +1,119 @@ +"""Version the legacy no-duplicate contract for visible, disabled native26 alternatives. + +Historical drivers and failed receipts remain unchanged. Only three literal +absence assertions change; original OS1 payloads and all sampling/restart gates +are still checked independently. +""" + +import copy +from pathlib import Path + +from matrix import digest, write_json +from native117_actor_io import bind +import native262 as native +import native262_retained_worldgen as retained +import native262_strict_migration as strict +import native262_worldgen as worldgen +from native262_resources import arguments, require + + +SPEC = native.RIG / "profiles/native262/legacy-visible-alternatives.json" +RESULT = "native262-legacy-alternative-worldgen-results.json" + + +def contract_spec(): + value = native.read_json(SPEC) + require(value["protocol"] == "NATIVE26_LEGACY_VISIBLE_DISABLED_ALTERNATIVES_V1" + and len(value["replacesOnly"]) == 3 + and value["replacement"] == dict(present=True, field="enabled", expectedBoolean=False), + "Unexpected versioned legacy alternative contract") + return value + + +def campaign_spec(name): + require(name == "legacy-import", "This contract is only for the legacy import route") + spec = copy.deepcopy(worldgen.campaign_spec(name)) + replacements = {row["id"]: row for row in contract_spec()["replacesOnly"]} + require(len(replacements) == 3, "Duplicate alternative contract IDs") + for phase in ("first", "restart"): + seen = set() + for assertion in spec[phase + "ProfileAssertions"]: + if assertion["id"] not in replacements: + continue + require(assertion["id"] not in seen, "Duplicated historical alternative assertion") + target = replacements[assertion["id"]] + require(assertion == {key: target[key] for key in ("id", "section", "entry")} | {"present": False}, + "Historical absence assertion changed; review rather than replace it blindly") + assertion.update(contract_spec()["replacement"]) + seen.add(assertion["id"]) + require(seen == replacements.keys(), "Missing or duplicated historical alternative assertions") + return spec + + +def validate_profile(document, assertions): + rows = worldgen.validate_profile(document, assertions) + for target in contract_spec()["replacesOnly"]: + rule = document[target["section"]][target["entry"]] + require(rule.get("enabled") is False and rule.get("block") == target["block"], + "Modern alternative must be visible but disabled: " + target["entry"]) + require(rule.get("retrogen", False) is False, "Import must not enable retrogen") + dimensions = rule.get("dimensions") + require(isinstance(dimensions, dict) and "minecraft:overworld" in dimensions, + "Alternative is not a complete editable placement rule") + for placement in dimensions.values(): + require(isinstance(placement, dict) + and all(key in placement for key in ("enabled", "min_y", "max_y", "frequency", "host_blocks", "host_tags")) + and placement["min_y"] <= placement["max_y"] and placement["frequency"] > 0 + and isinstance(placement["host_blocks"], list) and placement["host_blocks"], + "Disabled alternative lost its actual editable geometry/hosts") + if target["section"] == "ores": + require(all(key in row for row in dimensions.values() for key in + ("min_quantity", "max_quantity", "pattern", "placement_channel")), + "Typed ore alternative is incomplete") + else: + require(all(key in row for row in dimensions.values() for key in + ("min_radius", "max_radius", "min_vertical_radius", "max_vertical_radius")), + "Fluid alternative is incomplete") + spec = campaign_spec("legacy-import") + expected = dict(ores={}, fluid_deposits={}) + for source, destination in spec["configFixtures"]: + old = destination.stem + current = {"poweradvantage": "basepower", "electricadvantage": "baseelectric"}[old] + projected = retained.os1_profile(native.read_json(source), current, old) + expected["ores"].update(projected["ores"]) + outputs = {row["block"] for row in contract_spec()["replacesOnly"]} + audit = retained.audit_transition(expected, document, outputs) + require(audit["status"] == "PASS", "Imported OS1 settings changed: " + str(audit["issues"])) + return rows + [dict(id="source-os1-payload-and-no-active-duplicates", status="PASS", contract=audit)] + + +class LegacyServer(worldgen.WorldgenServer): + def validate_native_log(self, path): + super().validate_native_log(path) + strict.clean_log(path) + + +def preflight(args, sources): + return worldgen.preflight(args, [*sources, SPEC, Path(worldgen.__file__), Path(retained.__file__), Path(strict.__file__)]) + + +def run(args): + execute = bind(worldgen.run, campaign_spec=campaign_spec, validate_profile=validate_profile, + WorldgenServer=LegacyServer, preflight=preflight, RESULT=RESULT, __file__=__file__) + result = execute(args) + result["expectationContract"] = dict(path=str(SPEC), sha256=digest(SPEC), **contract_spec()) + result["historicalDriverSha256"] = digest(Path(worldgen.__file__)) + if not args.preflight_only: + write_json(Path(args.output) / RESULT, result) + return result + + +def main(): + parser = arguments(__doc__) + parser.add_argument("--campaign", choices=("legacy-import",), default="legacy-import") + parser.add_argument("--sample-timeout", type=int, default=1800) + print(run(parser.parse_args())["status"], flush=True) + + +if __name__ == "__main__": + main() diff --git a/test-rig/migration/native262_machines.py b/test-rig/migration/native262_machines.py new file mode 100644 index 00000000..fac5ad72 --- /dev/null +++ b/test-rig/migration/native262_machines.py @@ -0,0 +1,101 @@ +"""Native64 harvest and lifecycle transport; retain historical machine oracles.""" + +import traceback +from types import SimpleNamespace + +import lifecycle +from matrix import digest, write_json +from native117_actor_io import bind +from native117_identity import fixture_ids +from native117_lifecycle import NativeLifecycleRun, authorize_lifecycle, exercise +import native1201_machines +import native262 as native +from native262_networks import (MAX_TICKET_DISTANCE, Server, arguments, compatibility_view, + native_boot, native_inventory_item, prepare_inputs, validate_snapshot) + + +class LifecycleRun(NativeLifecycleRun): + minecraft = native.MINECRAFT + max_ticket_distance = MAX_TICKET_DISTANCE + require_boot = staticmethod(native_boot) + + def __init__(self, *args): + super().__init__(*args) + self.pids = [] + + def open_server(self, label): + server = Server(self.runtime, self.java, self.launcher, label) + self.pids.append(server.process.pid) + return server + + 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 _raw_probe(self, operation, label): + return validate_snapshot(super()._raw_probe(operation, label)) + + def probe(self, operation, label): + return compatibility_view(super().probe(operation, label)) + + +harvest = bind(native1201_machines.harvest, native1201=SimpleNamespace( + Server=Server, MINECRAFT=native.MINECRAFT, LAUNCHER=native.LAUNCHER)) + + +def run(args): + inputs = prepare_inputs(args) + if args.preflight_only: + return dict(status="READY_NOT_RUN", qualificationAllowed=False, **inputs.receipt()) + runtime = inputs.output + receipt = runtime / f"native262-{args.campaign}-results.json" + report = dict(status="RUNNING", campaign=args.campaign, scope="FINITE_HARVEST_AND_PHYSICAL_MACHINE_LIFECYCLE", + **inputs.receipt()) + + def progress(value): + report["observations"] = value + write_json(receipt, report) + + runner, staged = None, False + try: + native.stage(runtime, *inputs.target, args.port) + staged = True + write_json(receipt, report) + if args.campaign == "harvest": + result = harvest(runtime, inputs.java, native.LAUNCHER, {}, progress) + else: + definition = fixture_ids(lifecycle.fixtures()) + fixture = runtime / "lifecycle-fixture.json" + write_json(fixture, definition) + report["fixtureSha256"] = digest(fixture) + authorize_lifecycle(runtime) + runner = LifecycleRun(runtime, inputs.java, native.LAUNCHER, definition) + result = exercise(runner, "fresh262", progress) + report.update(status=result["status"], observations=result) + except BaseException as failure: + report.update(status="FAIL", failure=repr(failure), traceback=traceback.format_exc()) + finally: + try: + if runner is not None: + runner.close() + report["processIds"] = runner.pids + report["processesClosed"] = runner.server is None + inputs.verify(staged) + report.update(seedUnchanged=True, inputsUnchanged=True, oraclesUnchanged=True) + except BaseException as failure: + report.update(status="FAIL", custodyError=repr(failure), custodyTraceback=traceback.format_exc()) + if runtime.is_dir(): + write_json(receipt, report) + return report + + +def main(argv=None): + parser = arguments(__doc__) + parser.add_argument("--campaign", choices=("harvest", "lifecycle"), required=True) + report = run(parser.parse_args(argv)) + print(report["status"], flush=True) + return int(report["status"] not in ("PASS", "READY_NOT_RUN")) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native262_networks.py b/test-rig/migration/native262_networks.py new file mode 100644 index 00000000..397da2b2 --- /dev/null +++ b/test-rig/migration/native262_networks.py @@ -0,0 +1,294 @@ +"""Native64 transport for unchanged physical-unload, bridge and extended-drill oracles.""" + +import argparse +from dataclasses import dataclass +from pathlib import Path +import sys +import subprocess +import traceback + +import nbtlib + +from matrix import digest, fingerprint, write_json +from native117 import output_path, require_disjoint +from native117_actor_io import bind +import native117_extended +from native117_geometry import FORCED_LEVEL, FULL_LEVEL +from native117_holder_networks import bridge_fixtures, extended_fixtures, linear_fixtures +from native117_lifecycle import authorize_lifecycle +from native117_partial import exercise_bridge, exercise_partial +from native117_ticket_networks import NativeTicketExtendedRun +import native262 as native + + +# Native ChunkLevel derives 33 + GENERATION_PYRAMID's radius. Every live probe +# must confirm radius 11, forced level 31 and the resulting 13-chunk holder halo. +MAX_TICKET_DISTANCE = 11 +HOLDER_RADIUS = FULL_LEVEL + MAX_TICKET_DISTANCE - FORCED_LEVEL +CAMPAIGNS = { + "partial": (linear_fixtures, exercise_partial), + "bridge": (bridge_fixtures, exercise_bridge), + "extended": (extended_fixtures, native117_extended.exercise), +} + + +def capped_command(command): + command = list(command) + if (not command or "-Xmx2G" not in command or not any(part.startswith("@") for part in command) + or any(part.startswith("-XX:ActiveProcessorCount=") for part in command)): + raise ValueError("Review a changed native finite-server launch before adding its CPU cap") + return [command[0], "-XX:ActiveProcessorCount=4", *command[1:]] + + +class _NativeProcess: + def __init__(self, owner): + self.owner = owner + + def __getattr__(self, name): + return getattr(subprocess, name) + + def Popen(self, command, *args, **kwargs): + command = capped_command(command) + environment = kwargs.get("env") + if not isinstance(environment, dict): + raise ValueError("Finite native launch needs the core's explicit environment") + names = ("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS", "CLASSPATH") + if any(name in environment for name in names): + raise ValueError("Finite native launch may not inherit JVM options/classpath") + path = Path(kwargs["cwd"]) / f"native262-finite-launch-{self.owner.native_label}.json" + if path.exists(): + raise FileExistsError(path) + record = dict(status="PLANNED", command=command, inheritedOptions={name: None for name in names}, + explicitProcessorCap=4, explicitMaxHeap="2G") + write_json(path, record) + process = None + try: + process = subprocess.Popen(command, *args, **kwargs) + record.update(status="REAL_PROCESS_STARTED", pid=process.pid) + write_json(path, record) + return process + except BaseException: + if process is not None and process.poll() is None: + process.kill() + process.wait(timeout=30) + raise + + +class _CappedLaunch(native.NativeLaunch): + def __init__(self, *args, **kwargs): + bind(native.NativeLaunch.__init__, subprocess=_NativeProcess(self))(self, *args, **kwargs) + + +class Server(native.Server, _CappedLaunch): + """Keep the sealed core transport; add a recorded command-line cap without global mutation.""" + + +def native_boot(path): + native.require_clean_log(path) + text = Path(path).read_text(encoding="utf-8") + for marker in (f"Starting minecraft server version {native.MINECRAFT}", + f"Forge mod loading, version {native.FORGE}, for MC {native.MINECRAFT} with MCP {native.MCP}"): + if marker not in text: + raise AssertionError("Missing native65 finite-campaign boot proof: " + marker) + + +def native_inventory_item(item): + identity, separator, payload = item.partition("{") + if not separator: + return item + if "[" in identity: + raise ValueError("Ambiguous legacy and native fixture item components") + custom = nbtlib.parse_nbt(separator + payload) + if not isinstance(custom, nbtlib.Compound): + raise AssertionError("Historical fixture item data must be a compound") + return identity + "[minecraft:custom_data=" + custom.snbt(compact=True) + "]" + + +def compatibility_view(value): + """Derived legacy accessors only; never rewrite native probe files or input objects.""" + 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") + items = compound.get("Items", ()) + if "Items" in compound and not isinstance(items, nbtlib.List): + raise AssertionError("Native inventory must be a typed list") + for stack in 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 26.2 inventory stack: {stack}") + components = stack.get("components", nbtlib.Compound()) + if not isinstance(components, nbtlib.Compound): + raise AssertionError("Native item components must be a compound") + stack["Count"] = stack["count"] + custom = 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 + + +def validate_snapshot(snapshot): + if ((snapshot.get("minecraft"), snapshot.get("forge"), snapshot.get("station")) + != (native.MINECRAFT, native.FORGE, "lifecycle") + or type(snapshot.get("worldTime")) is not int or snapshot["worldTime"] < 0): + raise AssertionError("Historical or incomplete lifecycle evidence is not native65 proof") + return snapshot + + +def oracle_closure(): + directory = Path(__file__).resolve().parent + return {Path(module.__file__).resolve(): digest(Path(module.__file__)) for module in tuple(sys.modules.values()) + if getattr(module, "__file__", None) and Path(module.__file__).suffix == ".py" + and Path(module.__file__).resolve().parent == directory + and not Path(module.__file__).name.startswith("test_")} + + +@dataclass +class Inputs: + target: tuple + java: Path + java_version: str + output: Path + hashes: dict + seed_before: dict + + def verify(self, staged): + if staged: + native.verify_runtime(self.output, self.target[1], + {path.name: sha for _, path, sha in self.target[2].values()}) + if fingerprint(self.target[0]) != self.seed_before: + raise AssertionError("Protected server seed changed") + for path, expected in self.hashes.items(): + native.validate_hash(path, expected) + if set(oracle_closure()) - set(self.hashes): + raise AssertionError("An unsealed migration oracle was imported during the run") + + def receipt(self): + return dict(qualificationStatus=native.EXPLORATORY, minecraft=native.MINECRAFT, + forge=native.FORGE, mcp=native.MCP, javaVersion=self.java_version, + inputHashes={str(path): sha for path, sha in self.hashes.items()}, + jars={key: value[2] for key, value in self.target[2].items()}, + pythonVersion=sys.version, pythonExecutable=str(Path(sys.executable).resolve()), + inheritedJvmOptions={name: None for name in ("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS")}, + explicitProcessorCap=4, explicitMaxHeap="2G", + legacyOracleView="Derived aliases only; original native probe files are unchanged") + + +def prepare_inputs(args): + if any(name in native.os.environ for name in ("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS", "CLASSPATH")): + raise ValueError("Finite native campaigns require absent JVM option/classpath variables") + native.native_launch_environment() + native.validate_hash(args.profile, args.profile_sha256) + target = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + if set(target[2]) != set(native.VERSIONS): + raise ValueError("Require the complete six-jar native65 family") + java, version = native.java25(args.java) + native.require_free_port(args.port) + output = output_path(Path(args.output)) + sources = oracle_closure() + sources.update({Path(path).resolve(): digest(path) for path in ( + args.profile, args.inventory, java, Path(sys.executable), *(value[1] for value in target[2].values()))}) + for path in [target[0], *sources]: + require_disjoint(output, Path(path)) + return Inputs(target, java, version, output, sources, fingerprint(target[0])) + + +def arguments(description=__doc__): + parser = argparse.ArgumentParser(description=description) + for name in ("profile", "seed", "inventory", "output", "java"): + parser.add_argument("--" + name, type=lambda value: Path(value).resolve(), required=True) + parser.add_argument("--profile-sha256", required=True) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--port", type=int, required=True) + parser.add_argument("--preflight-only", action="store_true") + return parser + + +class PartialRun(NativeTicketExtendedRun): + minecraft = native.MINECRAFT + max_ticket_distance = MAX_TICKET_DISTANCE + require_boot = staticmethod(native_boot) + + def __init__(self, *args): + super().__init__(*args) + self.pids = [] + + def open_server(self, label): + server = Server(self.runtime, self.java, self.launcher, label) + self.pids.append(server.process.pid) + return server + + 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 _raw_probe(self, operation, label): + return validate_snapshot(super()._raw_probe(operation, label)) + + def probe(self, operation, label): + return compatibility_view(super().probe(operation, label)) + + +def run(args): + inputs = prepare_inputs(args) + if args.preflight_only: + return dict(status="READY_NOT_RUN", qualificationAllowed=False, **inputs.receipt()) + runtime = inputs.output + receipt = runtime / f"native262-{args.campaign}-results.json" + report = dict(status="RUNNING", campaign=args.campaign, scope="FINITE_PHYSICAL_UNLOAD_NETWORKS_AND_DRILLS", + **inputs.receipt()) + + def progress(value): + report["observations"] = value + write_json(receipt, report) + + runner, staged = None, False + try: + native.stage(runtime, *inputs.target, args.port) + staged = True + write_json(receipt, report) + layout, exercise = CAMPAIGNS[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, inputs.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()) + finally: + try: + if runner is not None: + runner.close() + report["processIds"] = runner.pids + report["processesClosed"] = runner.server is None + inputs.verify(staged) + report.update(seedUnchanged=True, inputsUnchanged=True, oraclesUnchanged=True) + except BaseException as failure: + report.update(status="FAIL", custodyError=repr(failure), custodyTraceback=traceback.format_exc()) + if runtime.is_dir(): + write_json(receipt, report) + return report + + +def main(argv=None): + parser = arguments() + parser.add_argument("--campaign", choices=tuple(CAMPAIGNS), required=True) + report = run(parser.parse_args(argv)) + print(report["status"], flush=True) + return int(report["status"] not in ("PASS", "READY_NOT_RUN")) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native262_oil.py b/test-rig/migration/native262_oil.py new file mode 100644 index 00000000..07da774d --- /dev/null +++ b/test-rig/migration/native262_oil.py @@ -0,0 +1,178 @@ +"""Native64 oil transport and separately classified actual-world renderer evidence.""" + +from pathlib import Path +import shutil + +import lifecycle +from matrix import digest, write_json +from native117 import output_path +from native117_lifecycle import authorize_lifecycle +from native117_oil import NativeOilRun, construction_commands, exercise, fixtures, require_document +import native262 as native +import native262_layout as layout +from native262_resources import arguments, artifact, pixel_pair, preflight, require + + +RESULT = "native262-oil-results.json" +OILS = ("crude_oil", "refined_oil") + + +def native_boot(path): + native.require_clean_log(path) + text = path.read_text(encoding="utf-8") + require(f"Starting minecraft server version {native.MINECRAFT}" in text + and f"Forge mod loading, version {native.FORGE}, for MC {native.MINECRAFT} with MCP {native.MCP}" in text, + "Missing native65 oil startup proof") + + +def validate_snapshot(snapshot, definition, *, initial_air=False): + require_document(snapshot, definition) + require((snapshot["minecraft"], snapshot["forge"], snapshot["station"]) == (native.MINECRAFT, native.FORGE, "lifecycle") + and type(snapshot["worldTime"]) is int and snapshot["worldTime"] >= 0, + "Historical oil snapshot is not target evidence") + for lane in definition: + for row in snapshot["cases"][lane["id"]]: + require(row["loaded"] is True, "Oil witness is not actually loaded") + if initial_air: + require(set(row) == {"pos", "loaded", "block", "state"} + and row["block"] == "minecraft:air" and row["state"] == "Block{minecraft:air}", + "Initial oil witness is not untouched loaded air") + if initial_air: + continue + for role in ("drain", "output"): + data = lifecycle.parse(lifecycle.at(snapshot, lane["id"], lane[role])) + tank = data["Tank"] + require(isinstance(tank, dict), "Missing native tank data") + if set(tank) == {"Empty"}: + require(type(tank["Empty"]) is layout.nbtlib.Byte and int(tank["Empty"]) == 1, + "Unknown native empty-tank encoding") + else: + require(set(tank) == {"FluidName", "Amount"} and tank["FluidName"] == lane["fluid"] + and type(tank["Amount"]) is layout.nbtlib.Int and tank["Amount"] >= 0, + "Unknown/ambiguous native oil tank data") + return snapshot + + +class OilRun(NativeOilRun): + expected_launcher = native.LAUNCHER + max_ticket_distance = 11 + require_boot = staticmethod(native_boot) + + def __init__(self, *args): + super().__init__(*args) + self.pids = [] + + def open_server(self, label): + native.audit_staged(self.runtime) + server = native.Server(self.runtime, self.java, self.launcher, label) + self.pids.append(server.process.pid) + require(len(self.pids) == len(set(self.pids)), "Oil restart reused a process") + return server + + def probe(self, operation, label): + initial_air = (operation, label) == ("load", "oil-loaded") and self.boots == ["oil-first-fill"] \ + and not self.construction_sealed and self.pending_construction == construction_commands(self.definition) + return validate_snapshot(super().probe(operation, label), self.definition, initial_air=initial_air) + + +def validate_in_world_render(document, root, profile_sha256): + """Requires future real-world producer data; the existing worldless probe cannot satisfy it.""" + require((document["protocol"], document["minecraft"], document["forge"]) + == ("NATIVE65_IN_WORLD_OIL_ATLAS_V1", native.MINECRAFT, native.FORGE), "Wrong in-world oil protocol") + require(document["profileSha256"] == profile_sha256 and document["worldLoaded"] is True + and document["dimension"] == "minecraft:overworld" and document["syntheticWorld"] is False, + "Oil rendering must use the actual pinned loaded world") + level = layout._load(artifact(root, document["stoppedLevel"]))["Data"] + require(type(level["DataVersion"]) is layout.nbtlib.Int and int(level["DataVersion"]) == layout.DATA_VERSION + and str(level["Version"]["Name"]) == native.MINECRAFT, "In-world oil used a historical or fake stopped level") + phases = document["phases"] + require(len(phases) == 2 and [row["phase"] for row in phases] == ["initial", "after-resource-reload"], + "Actual atlas reload is required") + initial = {} + for index, phase in enumerate(phases): + require(type(phase["atlasGeneration"]) is int and phase["atlasGeneration"] >= 0, + "Missing atlas generation") + if index: + require(phase["reloadCompleted"] is True and phase["atlasGeneration"] > phases[0]["atlasGeneration"], + "Atlas was not actually reloaded") + cases = phase["cases"] + require(len(cases) == 4 and {(row["oil"], row["flowing"]) for row in cases} + == {(oil, flowing) for oil in OILS for flowing in (True, False)}, "Incomplete in-world oil cases") + for row in cases: + key = row["oil"], row["flowing"] + require(type(row["flowing"]) is bool and row["worldBlock"] == "basepower:" + row["oil"] + and row["modelLayer"] == "TRANSLUCENT" and row["missingModel"] is False, + "Wrong actual oil block/model/layer") + expected = "basepower:blocks/" + row["oil"] + ("_flow" if row["flowing"] else "_still") + require(row["modelSprite"] == row["atlasSprite"] == expected and row["modelLookup"] == "IClientFluidTypeExtensions.getModel", + "World rendering did not select the current native oil model") + require(all(type(row[field]) is int and row[field] != 0 for field in ("modelSpriteIdentity", "atlasSpriteIdentity", "modelIdentity")) + and row["modelSpriteIdentity"] == row["atlasSpriteIdentity"], + "Missing real model/sprite identity") + require(isinstance(row["pos"], list) and len(row["pos"]) == 3 and all(type(value) is int for value in row["pos"]) + and type(row["worldTime"]) is int and row["worldTime"] >= 0, "Missing actual-world position/time") + require(type(row["vertices"]) is int and row["vertices"] >= 4, "Blank in-world fluid geometry") + pixel_pair(root, row["pixels"]) + if index: + old = initial[key] + require(row["modelSpriteIdentity"] != old["modelSpriteIdentity"] and row["modelIdentity"] != old["modelIdentity"], + "Oil model retained a stale pre-reload atlas sprite") + else: + initial[key] = row + controls = document["negativeControls"] + require(set(controls) == {"missing-model", "wrong-sprite", "stale-atlas"}, "Missing actual-world oil negative controls") + for row in controls.values(): + pixel_pair(root, row, matches=False) + return dict(status="PASS", scope="ACTUAL_WORLD_OIL_AND_ATLAS_RELOAD", cases=8, negativeControls=3) + + +def run(args): + require(type(args.seconds) is int and 2 <= args.seconds <= 120, "Bound oil observation intervals") + inputs = preflight(args, [Path(__file__), Path(lifecycle.__file__), Path(exercise.__code__.co_filename)]) + native.require_free_port(args.port) + if args.preflight_only: + return dict(status="READY_NOT_RUN", qualificationAllowed=False, **inputs.receipt()) + runtime = output_path(args.output) + expected = native.stage(runtime, *inputs.target, args.port) + shutil.copy2(Path(__file__), runtime / "native262-oil-source.py") + pins = {"jars": inputs.receipt()["target_jars"]} + definition = fixtures(pins) + require(len(definition) == 4, "Only the four approved Power oil lanes are pinned") + write_json(runtime / "lifecycle-fixture.json", definition) + authorize_lifecycle(runtime) + report = dict(status="RUNNING", scope="BUCKET_PIPE_TRANSPORT_NOT_WORLD_RENDERING", **inputs.receipt(), + inWorldRendering="NOT_RUN_SEPARATE_CLIENT_WITNESS_REQUIRED") + runner = OilRun(runtime, inputs.java, native.LAUNCHER, definition, pins, inputs.seed_before) + + def progress(value): + report.update(value) + write_json(runtime / RESULT, report) + + try: + exercise(runner, args.seconds, progress) + require(len(runner.pids) == 3, "Need three distinct native oil processes") + report.update(status="PASS", processIds=runner.pids) + except BaseException as failure: + report.update(status="FAIL", error=repr(failure)) + raise + finally: + try: + runner.close() + native.verify_runtime(runtime, inputs.target[1], expected) + report["sourceUnchanged"] = inputs.unchanged() + except BaseException as failure: + report.update(status="FAIL", custodyError=repr(failure)) + raise + finally: + write_json(runtime / RESULT, report) + return report + + +def main(): + parser = arguments(__doc__) + parser.add_argument("--seconds", type=int, default=8) + print(run(parser.parse_args())["status"], flush=True) + + +if __name__ == "__main__": + main() diff --git a/test-rig/migration/native262_operations.py b/test-rig/migration/native262_operations.py new file mode 100644 index 00000000..74920e4e --- /dev/null +++ b/test-rig/migration/native262_operations.py @@ -0,0 +1,192 @@ +"""Run the unchanged 132 finite-job oracles against explicit Forge65 storage.""" + +import argparse +from pathlib import Path +import traceback +from zipfile import ZipFile + +import nbtlib + +from matrix import WORLD, digest, fingerprint, write_json +from native117 import output_path, require_disjoint +from native117_entity_storage import _decompress, _nbt +from native117_machine_operations import validate_operation as frozen_validate_operation +import native117_machine_saved as saved +import native12111_operations as previous +import native262 as native +from native262_layout import DATA_VERSION, region_file +from transformer_order import read_region + + +def validate_operation(catalog, document, group): + result = frozen_validate_operation(catalog, document, group) + if group == "all" and result["cases"] != 132: + raise AssertionError("Native262 full operations requires all 132 finite jobs") + return result + + +def modern_chunk(payload): + if len(payload) < 6 or int.from_bytes(payload[:4], "big") != len(payload) - 4: + raise ValueError("Invalid native terrain payload length") + root = _nbt(_decompress(payload[5:], payload[4])) + if type(root.get("DataVersion")) is not nbtlib.Int or int(root["DataVersion"]) != DATA_VERSION or "Level" in root: + raise ValueError("Machine save must be native 26.2 terrain") + if (type(root.get("xPos")) is not nbtlib.Int or type(root.get("zPos")) is not nbtlib.Int + or not isinstance(root.get("sections"), nbtlib.List) + or not isinstance(root.get("block_entities"), nbtlib.List)): + raise ValueError("Missing typed native chunk fields") + sections = [] + for section in root["sections"]: + view = nbtlib.Compound({"Y": section["Y"]}) + if "block_states" in section: + states = section["block_states"] + view["Palette"] = states["palette"] + if "data" in states: + view["BlockStates"] = states["data"] + sections.append(view) + return nbtlib.Compound({"xPos": root["xPos"], "zPos": root["zPos"], + "Sections": nbtlib.List[nbtlib.Compound](sections), "TileEntities": root["block_entities"]}) + + +def audit_saved(runtime, fixture, evidence): + """Same typed state/NBT comparator, new explicit dimension storage; retain raw payloads.""" + runtime, evidence = Path(runtime).resolve(), Path(evidence).resolve() + world = runtime / WORLD + require_disjoint(evidence, world) + evidence.mkdir(parents=True, exist_ok=False) + report = dict(status="FAIL", cases=len(fixture["cases"]), files={}, failures=[], + scope="Stopped Forge65 terrain state and complete typed machine NBT; no source writes") + write_json(evidence / "snapshot.json", fixture) + regions, chunks = {}, {} + try: + for case in fixture["cases"]: + dimension = case.get("dimension", "minecraft:overworld") + grouped = {} + for row in case["positions"]: + grouped.setdefault((row["x"] // 16, row["z"] // 16), []).append(row) + for (cx, cz), rows in grouped.items(): + path = region_file(world, dimension, cx, cz) + if path not in regions: + report["files"][str(path)] = digest(path) + regions[path] = read_region(path)[1] + key = dimension, cx, cz + if key not in chunks: + payload = regions[path][(cx & 31) + (cz & 31) * 32] + with (evidence / f"chunk-{len(chunks):05}.bin").open("xb") as output: + output.write(payload) + chunks[key] = modern_chunk(payload) + if (int(chunks[key]["xPos"]), int(chunks[key]["zPos"])) != (cx, cz): + raise AssertionError("Saved chunk coordinate differs from its region slot") + differences = saved.compare_chunk(rows, chunks[key]) + if differences: + report["failures"].append(dict(id=case["id"], dimension=dimension, differences=differences)) + if any(digest(Path(path)) != sha for path, sha in report["files"].items()): + raise AssertionError("Terrain changed during stopped audit") + report["status"] = "FAIL" if report["failures"] else "PASS" + except BaseException as failure: + report["error"] = repr(failure) + raise + finally: + write_json(evidence / "receipt.json", report) + return report + + +def native_load_sources(): + sources = native.RIG.parent / f".gradle/mavenizer/repo/net/minecraftforge/forge/{native.FORGE_VERSION}/forge-{native.FORGE_VERSION}-sources.jar" + expected = {"net/minecraft/world/entity/Entity.java": + "D0451E3BDF7F730064C80071391EA14E1B7919030D12B00E845B748236F29C8F", + "net/minecraft/world/entity/LivingEntity.java": + "3A402471B7B183E317A07512887827DDCB7EED524BD37008691FD37B9CA6063C"} + # Both load methods are source-bound before reusing the existing exact expected-only + # clamp/rebase rules. Observed full NBT and the frozen equality oracle are unchanged. + import hashlib + with ZipFile(sources) as jar: + for path, sha in expected.items(): + if hashlib.sha256(jar.read(path)).hexdigest().upper() != sha: + raise AssertionError("Review selected native actor load source: " + path) + return expected + + +def validate_restart(fixture, inspected): + source_hashes = native_load_sources() + result = previous.validate_restart(fixture, inspected) + result["comparison"] = result["comparison"].replace("1.21.11 explosive", "26.2 source-verified explosive") + result["nativeLoadSourceSha256"] = source_hashes + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("profile", "seed", "inventory", "java", "output"): + parser.add_argument("--" + name, required=True, type=lambda value: Path(value).resolve()) + parser.add_argument("--inventory-sha256", required=True) + parser.add_argument("--port", required=True, type=int) + parser.add_argument("--group", choices=("all", "pickup", "power", "steam", "electric"), default="all") + args = parser.parse_args() + target = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + java, version = native.java25(args.java) + runtime = output_path(args.output) + for path in (args.profile, args.seed, args.inventory, java, *(entry[1] for entry in target[2].values())): + require_disjoint(runtime, path) + seed_before = fingerprint(args.seed) + expected = native.stage(runtime, *target, args.port) + for marker in ("advantage-machine-operation.allow", "migration-operate.allow"): + (runtime / marker).touch() + report = dict(status="RUNNING", minecraft=native.MINECRAFT, forge=native.FORGE, group=args.group, + profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), jars=expected, + javaSha256=digest(java), javaVersion=version, phases={}) + receipt = runtime / "native262-machine-operation-results.json" + write_json(receipt, report) + server = None + try: + server = native.Server(runtime, java, native.LAUNCHER, "operate") + for command in ("gamerule doMobSpawning false", "gamerule randomTickSpeed 0", + "gamerule doWeatherCycle false", "weather clear"): + server.send(command) + server.send("advmachines run " + args.group + " measured") + server.wait("ADVMACHINES COMPLETE measured", 1500) + catalog = native.read_json(runtime / "advantage-works-results/machine-catalog-measured.json") + document = native.read_json(runtime / "advantage-works-results/machine-operation-measured.json") + report["phases"]["operation"] = validate_operation(catalog, document, args.group) + server.send("save-all flush") + server.wait("Saved the game", 30) + server.close() + server = None + fixture = native.read_json(runtime / "machine-operation-fixture.json") + fixture_sha = report["fixtureSha256"] = digest(runtime / "machine-operation-fixture.json") + report["phases"]["saved-before-restart"] = audit_saved(runtime, fixture, runtime / "raw-before-restart") + write_json(receipt, report) + server = native.Server(runtime, java, native.LAUNCHER, "restart") + server.send("advmachines inspect restarted") + server.wait("ADVMACHINES INSPECTED restarted", 360) + observed = native.read_json(runtime / "advantage-works-results/machine-inspection-restarted.json") + report["phases"]["restart"] = validate_restart(fixture, observed) + server.send("save-all flush") + server.wait("Saved the game", 30) + server.close() + server = None + report["phases"]["saved-after-restart"] = audit_saved(runtime, observed, runtime / "raw-after-restart") + if digest(runtime / "machine-operation-fixture.json") != fixture_sha: + raise AssertionError("Operating fixture changed across restart") + report["status"] = "PASS" if all(value["status"] == "PASS" for value in report["phases"].values()) else "FAIL" + except BaseException as failure: + report.update(status="FAIL", failure=repr(failure), traceback=traceback.format_exc()) + raise + finally: + try: + if server is not None: + server.close() + native.verify_runtime(runtime, target[1], expected) + report["seedUnchanged"] = fingerprint(args.seed) == seed_before + if not report["seedUnchanged"]: + raise AssertionError("Protected server seed changed") + except BaseException as failure: + report.update(status="FAIL", custodyError=repr(failure)) + raise + finally: + write_json(receipt, report) + return int(report["status"] != "PASS") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native262_platform_diagnostics.py b/test-rig/migration/native262_platform_diagnostics.py new file mode 100644 index 00000000..8b5dae61 --- /dev/null +++ b/test-rig/migration/native262_platform_diagnostics.py @@ -0,0 +1,113 @@ +"""Exact, independently reproduced Forge 65 host diagnostics; no exception wildcard.""" + +import hashlib +import json +from pathlib import Path +import re + +COMMON_JAR_SHA = "78206AA7F6D197CAA926291408C01889B6B910CA0F74017D3FCBDACCF9562959" +SOURCES_JAR_SHA = "DE720A128534BA1072B354C863D671AB1999900A8FDDAADC4A68EE74C2E33FD6" +HOST_FIXTURE = Path(__file__).with_name("fixtures") / "native262-server-host-perflib.json" +HOST_FIXTURE_SHA = "894104959BCED7C967A1916B1AEFAE74A02C42648F3A492E95901E1987F98D2B" +HOST_FILE_FIXTURE = HOST_FIXTURE.with_name("native262-file-host-perflib.json") +HOST_FILE_FIXTURE_SHA = "7F59E0B7AA327AFA437F7503EBBA4586C5C33D6177A2E0F6775EDB69A8601840" +HOST_CONTROL_SHA = "9D2BCB6B5780D2DCD56CF2618DD7CB6F49038E3C6D1D2C94E2D6E3DE57575B94" +SYSTEM_REPORT_SOURCE_SHA = "C663E1109AD7C2D1869A74FA07FD74A4D95D8C131F57533A4CB0471988C14CC5" +OSHI_JAR_SHA = "B29BA9BCE041E6EA822203D5B5A692660C87551241510EBC76EEDFFE684844C7" +NETTY_FIXTURE = HOST_FIXTURE.with_name("native262-server-netty-java25.json") +NETTY_FIXTURE_SHA = "D22CD0A0B883C88BE66B42E3AD37EF914DFB8BEBF5CEC17893A09799F47770E3" +HEADER = re.compile(r"^\[[^\]]+\] \[([^\]]+)/([A-Z]+)\] \[([^\]]+)\]: (.*)$") +ANSI = re.compile(r"\x1b\[[0-9;]*m") + + +def host_fixture(file_appender=False): + path, checksum = (HOST_FILE_FIXTURE, HOST_FILE_FIXTURE_SHA) if file_appender else (HOST_FIXTURE, HOST_FIXTURE_SHA) + raw = path.read_bytes() + if hashlib.sha256(raw).hexdigest().upper() != checksum: + raise AssertionError("Reviewed native host-diagnostic fixture changed") + return json.loads(raw)["blocks"] + + +def netty_fixture(): + raw = NETTY_FIXTURE.read_bytes() + if hashlib.sha256(raw).hexdigest().upper() != NETTY_FIXTURE_SHA: + raise AssertionError("Reviewed native Netty fixture changed") + return json.loads(raw)["blocks"] + + +def classify_netty(text, fallback): + blocks = [] + for line in text.splitlines(): + if HEADER.fullmatch(line) or not blocks: + blocks.append([line]) + else: + blocks[-1].append(line) + selected = [] + names = {row["logger"] for row in netty_fixture()} + for index, block in enumerate(blocks): + header = HEADER.fullmatch(block[0]) + if header and header[3] in names: + selected.append((index, dict(thread=header[1], level=header[2], logger=header[3], message=header[4], + body=[line for line in block[1:] if line.strip() and line.strip() != ">"]))) + if not selected: + return fallback(text) + if ([row for _, row in selected] != netty_fixture() + or [index for index, _ in selected] != list(range(selected[0][0], selected[0][0] + 3))): + failures, diagnostics = fallback(text) + failures.append("Unproven native262 Netty sequence; exact DEBUG probes and successful Java25 cleaner required") + return failures, diagnostics + accepted = {index for index, _ in selected[:2]} + failures, diagnostics = [], [] + for index, block in enumerate(blocks): + if index in accepted: + diagnostics.append(dict(classification="SOURCE_AND_BINARY_PROVEN_NATIVE_NETTY_JAVA25_FALLBACK", + dependencySha256=COMMON_JAR_SHA, sourceSha256=SOURCES_JAR_SHA, + fixtureSha256=NETTY_FIXTURE_SHA, rawBlock=block)) + else: + errors, other = fallback("\n".join(block)) + failures.extend(errors) + diagnostics.extend(other) + return failures, diagnostics + + +def classify(text, fallback): + blocks = [] + for raw in text.splitlines(): + line = ANSI.sub("", raw) + if HEADER.fullmatch(line) or not blocks: + blocks.append([line]) + else: + blocks[-1].append(line) + selected = [] + for index, block in enumerate(blocks): + header = HEADER.fullmatch(block[0]) + if header and (header[3] in ("os.dr.wi.re.HkeyPerformanceDataUtil/", "oshi.driver.windows.registry.HkeyPerformanceDataUtil/") + and header[2] != "DEBUG" or + header[3] in ("minecraft/SystemReport", "net.minecraft.SystemReport/") + and header[4].startswith("Failed to get system info for Process ")): + selected.append((index, dict(thread=header[1], level=header[2], logger=header[3], message=header[4], + body=[line for line in block[1:] if line.strip() and line.strip() != ">"]))) + if not selected: + return classify_netty(text, fallback) + file_appender = selected[0][1]["logger"] == "oshi.driver.windows.registry.HkeyPerformanceDataUtil/" + if [row for _, row in selected] != host_fixture(file_appender): + failures, diagnostics = fallback(text) + failures.append("Unproven native262 host-diagnostic sequence; complete exact primary-source-bound stacks required") + return failures, diagnostics + accepted = {index for index, _ in selected} + failures, diagnostics = [], [] + remaining = [] + for index, block in enumerate(blocks): + if index in accepted: + diagnostics.append(dict(classification="INDEPENDENTLY_REPRODUCED_NATIVE_WINDOWS_PERFLIB_FAILURE", + scope="Host process statistics unavailable; not a gameplay assertion", + controlReceiptSha256=HOST_CONTROL_SHA, + fixtureSha256=HOST_FILE_FIXTURE_SHA if file_appender else HOST_FIXTURE_SHA, + primarySourceSha256=SYSTEM_REPORT_SOURCE_SHA, dependencySha256=OSHI_JAR_SHA, + rawBlock=block)) + else: + remaining.extend(block) + errors, other = classify_netty("\n".join(remaining), fallback) + failures.extend(errors) + diagnostics.extend(other) + return failures, diagnostics diff --git a/test-rig/migration/native262_resources.py b/test-rig/migration/native262_resources.py new file mode 100644 index 00000000..37c14024 --- /dev/null +++ b/test-rig/migration/native262_resources.py @@ -0,0 +1,259 @@ +"""Native64 recipe/resource adapters; historical engines and assertions stay unchanged. + +Unit/preflight success is not packaged-game or graphical qualification. +""" + +import argparse +from dataclasses import dataclass +from pathlib import Path +import re +import shutil +import types + +import nbtlib + +from matrix import WORLD, digest, fingerprint, write_json +from native117 import output_path, require_disjoint +import native117_recipes as recipes +import native262 as native +import native262_layout as layout + + +FAMILY = frozenset(native.VERSIONS) +MODES = ("NORMAL", "TECH_PROGRESSION", "APOCALYPTIC") +PACK_METADATA = {"min_format": [107, 1], "max_format": [107, 1], + "description": "Disposable loot-override witness"} +RESULT = "native262-resources-results.json" + + +def require(condition, message): + if not condition: + raise AssertionError(message) + + +def bind(function, **bindings): + """Copy an existing assertion/campaign with explicit globals, never monkey-patch it.""" + result = types.FunctionType(function.__code__, dict(function.__globals__, **bindings), + function.__name__, function.__defaults__, function.__closure__) + result.__kwdefaults__ = function.__kwdefaults__ + return result + + +def artifact(root, record): + root = Path(root).resolve(strict=True) + relative = Path(record["file"]) + require(not relative.is_absolute() and ".." not in relative.parts, "Unsafe retained artifact path") + path = root / relative + for part in (path, *path.parents): + require(not part.is_symlink() and not getattr(part, "is_junction", lambda: False)(), "Aliased retained artifact") + path = path.resolve(strict=True) + require(path.is_relative_to(root) and path.is_file() and path.stat().st_nlink == 1, + "Retained artifact must be an independent in-root file") + native.validate_hash(path, record["sha256"]) + require(path.stat().st_size > 0, "Empty retained artifact") + return path + + +def pixel_pair(root, record, *, matches=True): + width, height = record["width"], record["height"] + require(type(width) is int and type(height) is int and 1 <= width <= 4096 and 1 <= height <= 4096, + "Invalid raw pixel dimensions") + actual, reference = artifact(root, record["actualRgba"]), artifact(root, record["referenceRgba"]) + require(actual != reference, "Pixel reference must be independently retained") + left, right = actual.read_bytes(), reference.read_bytes() + require(len(left) == len(right) == width * height * 4, "Truncated RGBA extraction") + require(any(left[3::4]) and len(set(left)) > 1, "Blank rendered pixels") + different = sum(left[index:index + 4] != right[index:index + 4] for index in range(0, len(left), 4)) + require((different == 0) is matches, "Independent rendered pixel comparison failed") + return different + + +@dataclass +class Inputs: + target: tuple + java: Path + java_version: str + hashes: dict + seed_before: dict + + @property + def mods(self): + return self.target[2] + + def unchanged(self): + require(fingerprint(self.target[0]) == self.seed_before, "Protected SDK seed changed") + for path, expected in self.hashes.items(): + native.validate_hash(Path(path), expected) + return True + + def receipt(self): + return dict(minecraft=native.MINECRAFT, forge=native.FORGE, mcp=native.MCP, + javaVersion=self.java_version, inputs=dict(self.hashes), + target_jars=[dict(id=key, version=version, path=str(path), sha256=sha) + for key, (version, path, sha) in sorted(self.mods.items())]) + + +def preflight(args, helpers=()): + require((native.MINECRAFT, native.FORGE, native.MCP, native.JAVA_VERSION) + == ("26.2", "65.1.0", "20260616.103818", "25.0.3+9"), "Wrong core SDK contract") + native.validate_hash(args.profile, args.profile_sha256) + target = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + require(set(target[2]) == FAMILY, "Require the exact six-jar native65 family") + java, version = native.java25(args.java) + root = output_path(args.output) + sources = [args.profile, args.inventory, java, Path(__file__), Path(native.__file__), Path(layout.__file__), + *helpers, *(value[1] for value in target[2].values())] + for path in [target[0], *sources]: + require_disjoint(root, Path(path).resolve(strict=True)) + return Inputs(target, java, version, {str(Path(path).resolve()): digest(Path(path)) for path in sources}, + fingerprint(target[0])) + + +def verify_saved_level(runtime, entry, minecraft="26.2", forge="65.1.0"): + require((minecraft, forge) == (native.MINECRAFT, native.FORGE), "Historical saved-level reader requested") + world = Path(runtime) / WORLD + data = layout.read_level(world) + require(str(data["Version"]["Name"]) == native.MINECRAFT, "Wrong stopped native level version") + root = layout._load(world / "level.dat") + fml = root["fml"] + versions = {} + for row in fml["LoadingModList"]: + key, value = str(row["ModId"]), str(row["ModVersion"]) + require(key not in versions, "Duplicate stopped mod identity") + versions[key] = value + expected = {row["id"]: row["version"] for row in entry["jars"]} + require(len(expected) == len(entry["jars"]) and set(expected) == FAMILY, + "Stopped recipe proof needs all six exact jars") + expected.update(minecraft=native.MINECRAFT, forge=native.FORGE) + require(versions == expected, "Stopped native mod identities differ from frozen family") + registries = fml["Registries"] + for key in ("minecraft:item", "minecraft:block"): + require(isinstance(registries[key]["ids"], nbtlib.List) and len(registries[key]["ids"]) > 0, + "Missing native stopped registry: " + key) + return dict(path=str(world / "level.dat"), sha256=digest(world / "level.dat"), + mods=versions, dataVersion=layout.DATA_VERSION) + + +class RecipeServer(recipes.RecipePruning, native.Server): + errors = staticmethod(lambda text: native.log_findings(text)[0]) + + @staticmethod + def unexpected_line(line): + loading_failure = re.search(r"\[[^\]]+/(?:ERROR|FATAL)\]", line) and any(marker in line for marker in ( + "Caught exception during event", "Failed to complete lifecycle event", "Failed to start the minecraft server")) + return bool(loading_failure) or native.Server.unexpected_line(line) + + +class Engine: + version = (native.MINECRAFT, native.FORGE) + pack_metadata = PACK_METADATA + require_clean_log = staticmethod(native.require_clean_log) + loot_directory = "loot_table" + server = native.Server + recipe_server = RecipeServer + result_name = RESULT + + @staticmethod + def stage(runtime, target, port, campaign): + require(campaign in ("unlock", "progression"), "Unknown native recipe campaign") + native.stage(runtime, *target, port) + (runtime / ("advantage-recipes.allow" if campaign == "unlock" else "advantage-progression.allow")).touch() + if campaign == "progression": + for marker in ("advantage-works.allow", "migration-operate.allow"): + (runtime / marker).touch() + return native.LAUNCHER + + +unlock_campaign = bind(recipes.unlock_campaign, verify_level=verify_saved_level) +progression_campaign = bind(recipes.progression_campaign, verify_level=verify_saved_level) + + +def verify_packet_exports(runtime): + rows = [] + for mode in ("normal", "tech", "apocalyptic"): + packet = runtime / f"recipes-{mode}-sweep.bin" + manifest_file = packet.with_suffix(".json") + manifest = native.read_json(manifest_file) + require(manifest["bookPacketType"] == "ClientboundRecipeBookAddPacket" + and manifest["tagsPacketType"] == "ClientboundUpdateTagsPacket" + and type(manifest["itemTagCount"]) is int and manifest["itemTagCount"] > 0, + "Missing native recipe display/item-tag packet protocol") + name = manifest["tagsPacketFile"] + require(isinstance(name, str) and Path(name).name == name and name.startswith("tags-") + and name.endswith(".bin"), "Unsafe native tag packet path") + tags = runtime / name + native.validate_hash(tags, manifest["tagsPacketSha256"]) + require(packet.stat().st_size > 0 and tags.stat().st_size > 0, "Empty native recipe/tag packet") + rows.append(dict(mode=mode, packet=str(packet), sha256=digest(packet), + manifestSha256=digest(manifest_file), tags=str(tags), tagsSha256=digest(tags))) + return rows + + +def run(args): + require(args.campaign in ("all", "unlock", "progression"), "Unknown native recipe campaign") + inputs = preflight(args, [Path(recipes.__file__)]) + native.require_free_port(args.port) + root = output_path(args.output) + if args.preflight_only: + return dict(status="READY_NOT_RUN", qualificationAllowed=False, **inputs.receipt()) + definition, sources = recipes.recipe_fixture([value[1] for value in inputs.mods.values()], + component_predicates=True, singular_directories=True) + root.mkdir(parents=True, exist_ok=False) + shutil.copy2(Path(__file__), root / "native262-resources-source.py") + report = dict(status="RUNNING", scope="PACKAGED_RECIPE_SERVER_NOT_GRAPHICAL_ACCEPTANCE", + profileSha256=digest(args.profile), inventorySha256=digest(args.inventory), + **inputs.receipt(), recipe_inventory=recipes.MOD_COUNTS, unlock_boots=[], progression_cases=[]) + entry = {"jars": report["target_jars"]} + expected = {path.name: sha for _, path, sha in inputs.mods.values()} + write_json(root / "packaged-recipe-sources.json", sources) + try: + if args.campaign in ("all", "unlock"): + unlock_campaign(root, inputs.target, entry, inputs.java, args.port, definition, report, engine=Engine) + require(len(report["unlock_boots"]) == 5 and all(row["status"] == "PASS" for row in report["unlock_boots"]), + "Incomplete native65 unlock boots") + report["packet_exports"] = verify_packet_exports(root / "unlock") + if args.campaign in ("all", "progression"): + progression_campaign(root, inputs.target, entry, inputs.java, args.port, report, engine=Engine) + required = {(mode, rubber, phase) for mode in MODES for rubber in (True, False) + for phase in ("initial", "reload", "restart")} + actual = {(row["mode"], row["rubber"], row["phase"]) for row in report["progression_cases"]} + require(len(report["progression_cases"]) == 18 and actual == required + and all(row["status"] == "PASS" for row in report["progression_cases"]), + "Incomplete native65 three-mode progression matrix") + report["status"] = "PASS" + except BaseException as failure: + report.update(status="FAIL", error=repr(failure)) + raise + finally: + try: + for campaign in ("unlock", "progression"): + if (root / campaign).exists(): + native.verify_runtime(root / campaign, inputs.target[1], expected) + report["sourceUnchanged"] = inputs.unchanged() + except BaseException as failure: + report.update(status="FAIL", custodyError=repr(failure)) + raise + finally: + write_json(root / RESULT, report) + return report + + +def arguments(description=__doc__): + parser = argparse.ArgumentParser(description=description) + for key in ("profile", "seed", "inventory", "java", "output"): + parser.add_argument("--" + key, type=Path, required=True) + for key in ("profile-sha256", "inventory-sha256"): + parser.add_argument("--" + key, required=True) + parser.add_argument("--port", type=int, required=True) + parser.add_argument("--preflight-only", action="store_true") + return parser + + +def main(): + parser = arguments() + parser.add_argument("--campaign", choices=("all", "unlock", "progression"), default="all") + print(run(parser.parse_args())["status"], flush=True) + + +if __name__ == "__main__": + main() diff --git a/test-rig/migration/native262_retained_worldgen.py b/test-rig/migration/native262_retained_worldgen.py new file mode 100644 index 00000000..3e7c41a2 --- /dev/null +++ b/test-rig/migration/native262_retained_worldgen.py @@ -0,0 +1,279 @@ +"""Preserve sealed retained rules, then sample genuinely new native65 normal chunks. + +This module is deliberately outside the sealed actor/core/endurance import graph. +Positive fallback block counts never override a failed saved-rule contract. +""" + +import copy +from pathlib import Path +import re +import shutil + +from matrix import digest, fingerprint, write_json +from native117 import output_path, require_disjoint +import native262 as native +import native262_layout as layout +import native262_worldgen as worldgen +from native262_resources import arguments, preflight, require + + +MOD_ALIASES = {"electricadvantage": "baseelectric", "poweradvantage": "basepower", "steamadvantage": "basesteam"} +SECTIONS = ("ores", "fluid_deposits") +CONTROLS = ("manage_vanilla_ores", "suppress_all_ore_features", "place_fluid_deposits") +RESULT = "native262-retained-worldgen-results.json" + + +def output_id(value): + if value == "minecraft:quartz_ore": + return "minecraft:nether_quartz_ore" + if isinstance(value, str) and ":" in value: + namespace, suffix = value.split(":", 1) + return MOD_ALIASES.get(namespace, namespace) + ":" + suffix + return value + + +def expected_rule(value): + result = copy.deepcopy(value) + for key in ("block", "deep_output"): + if key in result: + result[key] = output_id(result[key]) + for row in result.get("outputs", []): + if isinstance(row, dict) and "block" in row: + row["block"] = output_id(row["block"]) + return result + + +def differences(expected, actual, path=""): + if isinstance(expected, dict): + if not isinstance(actual, dict): + return [dict(path=path, expected=expected, actual=actual)] + result = [] + for key, value in expected.items(): + if key not in actual: + result.append(dict(path=path + "/" + key, expected=value, missing=True)) + else: + result.extend(differences(value, actual[key], path + "/" + key)) + return result + if type(expected) is not type(actual) or expected != actual: + return [dict(path=path, expected=expected, actual=actual)] + return [] + + +def active(rule): + return rule.get("enabled", True) and any(row.get("enabled", True) and row.get("frequency", 1) > 0 for section in + ("dimensions", "dimension_selectors") for row in rule.get(section, {}).values()) + + +def audit_transition(source, destination, registry): + issues = [] + for key in (*CONTROLS, "history", "migration_history", "user_notes"): + if key in source: + issues.extend(dict(kind="SOURCE_AUTHORITY", **row) for row in + differences({key: source[key]}, destination)) + for section in SECTIONS: + original, actual = source.get(section, {}), destination.get(section, {}) + expected_outputs = set() + for key, original_rule in original.items(): + if not isinstance(original_rule, dict): + continue + expected = expected_rule(original_rule) + expected_output = expected.get("block", output_id(key)) + if expected_output.startswith(("baseelectric:", "basepower:")): + expected_outputs.add(expected_output) + if key not in actual: + issues.append(dict(kind="MISSING_OR_RENAMED_RULE", section=section, rule=key)) + continue + row = actual[key] + issues.extend(dict(kind="RULE_PAYLOAD_CHANGED", section=section, rule=key, **delta) + for delta in differences(expected, row)) + actual_output = row.get("block", key) + if actual_output not in registry: + issues.append(dict(kind="UNKNOWN_OUTPUT", section=section, rule=key, output=actual_output)) + for key, row in actual.items(): + if key not in original and isinstance(row, dict) and active(row) and row.get("block", key) in expected_outputs: + issues.append(dict(kind="NEW_ACTIVE_DEFAULT", section=section, rule=key, output=row.get("block", key))) + return dict(status="FAIL" if issues else "PASS", issues=issues, + ruleKeyPolicy="EXACT_SOURCE_KEYS_NO_SILENT_RENAME", positiveBlockCountsCannotWaiveFailure=True) + + +def os1_profile(document, current, legacy): + """Independent projection of the frozen OS1 data contract, not target observations.""" + require(isinstance(document.get("dimensions"), list), "Expected sealed OS1 dimensions") + result = dict(ores={}, fluid_deposits={}) + ordinal = 0 + clamp = lambda value, low, high: max(low, min(high, value)) + for dimension in document["dimensions"]: + for ore in dimension.get("ores", []): + block = ore["blockID"] + require(block.startswith(legacy + ":") or block.startswith(current + ":"), "Unexpected OS1 provider owner") + block = current + block[block.index(":"):] + size, variation = int(ore.get("size", 8)), max(0, int(ore.get("variation", 0))) + minimum = clamp(int(ore.get("minHeight", 0)), 0, 255) + maximum = clamp(int(ore.get("maxHeight", 256)), 0, 256) - 1 + require(maximum >= minimum, "Invalid source OS1 bounds") + placement = dict(enabled=True, min_y=minimum, max_y=maximum, + frequency=max(0.0, float(ore.get("frequency", 0.5))), pattern="default", height_distribution="uniform", + spread=8, vertical_spread=clamp(variation, 1, 64), node_size=clamp(size, 1, 32), + host_blocks=["minecraft:stone"], host_tags=["forge:stone"], + host_families=["sedimentary", "metamorphic", "igneous_intrusive", "igneous_volcanic"], + biome_ids=[value for value in ore.get("biomes", []) if ":" in value], excluded_biome_ids=[], + biome_dictionary=[value.upper() for value in ore.get("biomes", []) if re.fullmatch(r"[A-Za-z_]+", value)], + excluded_biome_dictionary=[], geomes={}) + if variation: + placement.update(min_quantity=clamp(size - variation, 1, 64), max_quantity=clamp(size + variation - 1, 1, 64)) + else: + placement["quantity"] = clamp(size, 1, 64) + rule = dict(block=block, enabled=True, native_generation=False, suppress_vanilla=False, retrogen=False) + dimension_id = dimension["dimension"] + if dimension_id == "+": + rule["dimension_selectors"] = {"orespawn:all_except_nether_end": placement} + else: + name = {0: "minecraft:overworld", -1: "minecraft:the_nether", 1: "minecraft:the_end"}.get(dimension_id) + require(name is not None, "Unreviewed OS1 dimension") + rule["dimensions"] = {name: placement} + safe = re.sub(r"[^a-z0-9_]+", "_", block.lower()).strip("_") + result["ores"][current + ":legacy/" + safe + "_" + str(ordinal)] = rule + ordinal += 1 + require(ordinal > 0, "Empty OS1 source oracle") + return result + + +def require_new_world(world): + world = Path(world) + require(not any(world.rglob("*.mca")) and not (world / "level.dat").exists(), + "New-chunk proof cannot contain preexisting save or region data") + + +def source_expectation(source): + source = Path(source) + for relative in ("AdvantageWorksMigration/serverconfig/orespawn-worldgen.json", "config/orespawn-worldgen.json"): + path = source / relative + if path.is_file(): + return native.read_json(path), dict(kind="RETAINED_PROFILE", path=str(path), sha256=digest(path)) + result, files = dict(ores={}, fluid_deposits={}), [] + for current, legacy in (("baseelectric", "electricadvantage"), ("basepower", "poweradvantage")): + path = source / "config/orespawn" / (legacy + ".json") + expected = os1_profile(native.read_json(path), current, legacy) + result["ores"].update(expected["ores"]) + files.append(dict(path=str(path), sha256=digest(path))) + return result, dict(kind="RAW_OS1_SOURCE_NO_TARGET_DERIVED_ORACLE", files=files) + + +def registered_blocks(world): + layout.read_level(world) + root = layout._load(Path(world) / "level.dat") + rows = root["fml"]["Registries"]["minecraft:block"]["ids"] + values = {str(row["K"]) for row in rows} + require(len(values) == len(rows) and values, "Incomplete or duplicate actual native block registry") + return values + + +def run(args): + native.validate_hash(args.source_seal, args.source_seal_sha256) + seal = native.read_json(args.source_seal) + matches = [row for row in seal["routes"] if row["name"] == args.route] + require(len(matches) == 1, "Expected an exact approved source route") + route = matches[0] + source = Path(route["source"]).resolve(strict=True) + require(fingerprint(source) == route["sourceFingerprint"], "Sealed source fixture changed") + expected_profile, oracle = source_expectation(source) + helpers = [Path(__file__), args.source_seal, Path(worldgen.__file__), + *(path for path in (source / "config").rglob("*") if path.is_file())] + inputs = preflight(args, helpers) + require_disjoint(Path(args.output).resolve(), source) + require(1 <= args.radius <= 8 and 300 <= args.sample_timeout <= 3600, "Bound native sampling") + native.require_free_port(args.port) + if args.preflight_only: + return dict(status="READY_NOT_RUN", oracle=oracle, sourceRoute=args.route, **inputs.receipt()) + runtime = output_path(args.output) + expected_jars = native.stage(runtime, *inputs.target, args.port) + for path in (source / "config").rglob("*"): + if not path.is_file() or path.name == "fml.toml": + continue + target = runtime / "config" / path.relative_to(source / "config") + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, target) + world = runtime / worldgen.WORLD + old_world_profile = source / "AdvantageWorksMigration/serverconfig/orespawn-worldgen.json" + if old_world_profile.is_file(): + target = world / "serverconfig/orespawn-worldgen.json" + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(old_world_profile, target) + require_new_world(world) + spec = worldgen.campaign_spec("advantage") + spec["worldgenSampleRadius"] = args.radius + enabled = {output_id(row.get("block", key)) for section in SECTIONS for key, row in expected_profile.get(section, {}).items() + if active(row) and output_id(row.get("block", key)).startswith(("basepower:", "baseelectric:"))} + spec["worldgenExpectations"] = dict(minimumCounts={key: 1 for key in enabled}, maximumCounts={}, zeroBiomeViolations=False) + worldgen.configure_normal_world(runtime, spec, args.port) + report = dict(status="RUNNING", sourceRoute=args.route, oracle=oracle, profileSha256=digest(args.profile), + sourceSealSha256=digest(args.source_seal), sourceFingerprintBefore=route["sourceFingerprint"], + newWorldBeforeFirstBoot=True, sourceScope="RELEASED" if args.route.startswith("released") else "OTHER_EXPLICIT_SOURCE_ROUTE", + ruleKeyPolicy="EXACT_SOURCE_KEYS_PENDING_PARENT_HELPER_CONTRACT", phases={}, **inputs.receipt()) + write_json(runtime / "source-profile-oracle.json", expected_profile) + server, first, settings = None, None, None + pids = [] + try: + for phase in ("initial", "restart"): + native.verify_runtime(runtime, inputs.target[1], expected_jars) + server = worldgen.WorldgenServer(runtime, inputs.java, native.LAUNCHER, "retained-worldgen-" + phase) + pids.append(server.process.pid) + require(len(pids) == len(set(pids)), "Restart reused a process") + server.send("gamerule minecraft:spawn_mobs false") + server.send("save-all flush") + server.wait("Saved the game", 30) + profile = native.read_json(world / "serverconfig/orespawn-worldgen.json") + contract = audit_transition(expected_profile, profile, registered_blocks(world)) + write_json(runtime / ("profile-" + phase + ".json"), profile) + server.send("advworks sample-worldgen " + str(args.radius)) + server.wait("Worldgen sample wrote ", args.sample_timeout) + sample = native.read_json(runtime / "advantage-works-results/worldgen-sample.json") + write_json(runtime / ("sample-" + phase + ".json"), sample) + worldgen.validate_sample(sample, spec, inputs.mods) + server.send("save-all flush") + server.wait("Saved the game", 30) + server.close() + server = None + saved = worldgen.normal_world(world) + if first is None: + first, settings = sample, saved + else: + worldgen.validate_restart(first, sample, spec) + require(saved == settings, "Native generator SavedData changed on restart") + require(any(layout.dimension_directory(world, "minecraft:overworld").rglob("*.mca")), "No actual new native regions saved") + report["phases"][phase] = dict(status=contract["status"], contract=contract, sampleSha256=digest(runtime / ("sample-" + phase + ".json")), pid=pids[-1]) + write_json(runtime / RESULT, report) + report["status"] = "PASS" if all(row["status"] == "PASS" for row in report["phases"].values()) else "FAIL" + except BaseException as failure: + report.update(status="FAIL", error=repr(failure)) + raise + finally: + try: + if server is not None: + server.close() + native.verify_runtime(runtime, inputs.target[1], expected_jars) + report["inputsUnchanged"] = inputs.unchanged() + require(fingerprint(source) == route["sourceFingerprint"], "Protected source fixture changed") + report["sourceUnchanged"] = True + except BaseException as failure: + report.update(status="FAIL", custodyError=repr(failure)) + raise + finally: + write_json(runtime / RESULT, report) + return report + + +def main(): + parser = arguments(__doc__) + parser.add_argument("--source-seal", type=Path, required=True) + parser.add_argument("--source-seal-sha256", required=True) + parser.add_argument("--route", required=True) + parser.add_argument("--radius", type=int, default=3) + parser.add_argument("--sample-timeout", type=int, default=1800) + result = run(parser.parse_args()) + print(result["status"], flush=True) + raise SystemExit(1 if result["status"] == "FAIL" else 0) + + +if __name__ == "__main__": + main() diff --git a/test-rig/migration/native262_stopped_actor_reference.py b/test-rig/migration/native262_stopped_actor_reference.py new file mode 100644 index 00000000..699491b7 --- /dev/null +++ b/test-rig/migration/native262_stopped_actor_reference.py @@ -0,0 +1,163 @@ +"""Seal an actor reference from stopped source disk only, before any target load.""" + +import argparse +import copy +import io +from pathlib import Path +import uuid + +import nbtlib +import actors +from matrix import WORLD, digest, fingerprint, plain, write_json +from native117 import require_disjoint +import native117_entity_storage as storage +from native117_format import uuid_longs +import native262 as native + +VERSION = 3955 +SCOPE = "SOURCE_STOPPED_DISK_ONLY_NATIVE1211_ACTOR_REFERENCE" +WITNESSES = {"cart": "minecraft:chest_minecart", "dropped-musket": "minecraft:item", + "dropped-oil": "minecraft:item", "owned-wolf": "minecraft:wolf", "hydroturbine": "baseelectric:hydroturbine"} + + +def read_nbt(path): + return storage._nbt(storage._decompress(path.read_bytes(), 1)) + + +def identity(words): + if not isinstance(words, nbtlib.IntArray): + raise AssertionError("Source player requires a native UUID int array") + most, least = uuid_longs(list(map(int, words))) + return str(uuid.UUID(int=((most & ((1 << 64) - 1)) << 64) | (least & ((1 << 64) - 1)))) + + +def encoded(tag): + output = io.BytesIO() + nbtlib.File(copy.deepcopy(tag)).write(output) + return output.getvalue() + + +def disk_state(source): + # No serializer snapshot, stopped-event report, or target runtime is read here. + import native262_actors as driver + definition = native.read_json(source / "actor-fixture.json") + world = source / WORLD + level = read_nbt(world / "level.dat") + if type(level["Data"]["DataVersion"]) is not nbtlib.Int or int(level["Data"]["DataVersion"]) != VERSION: + raise AssertionError("Only an actual stopped native1.21.1 source world is supported") + player_relative = Path("playerdata") / (definition["playerUuid"] + ".dat") + player = read_nbt(world / player_relative) + if (type(player["DataVersion"]) is not nbtlib.Int or int(player["DataVersion"]) != VERSION + or identity(player["UUID"]) != definition["playerUuid"]): + raise AssertionError("Wrong source player identity or native version") + registry = actors.enchantments(source) + player_state = driver.player_semantic(plain(player.unpack()), actors.PLAYER_KEYS, registry) + result = dict(file=player_state, adapter=copy.deepcopy(player_state), entities={}, hostAuthority=None) + if "Player" in level["Data"]: + host = level["Data"]["Player"] + if identity(host["UUID"]) != definition["playerUuid"]: + raise AssertionError("Embedded source host is not the actor witness player") + result["hostAuthority"] = definition["playerUuid"] + result["embedded"] = driver.player_semantic(plain(host.unpack()), actors.PLAYER_KEYS, registry) + assets = {"raw/level.dat": (world / "level.dat").read_bytes(), "raw/player.dat": (world / player_relative).read_bytes(), + "raw/actor-fixture.json": (source / "actor-fixture.json").read_bytes()} + metadata, seen = {}, set() + directory = world / "entities" + for region in sorted(directory.iterdir()): + if not region.is_file(): + raise AssertionError("Unexpected directory in source entity storage") + match = storage.REGION_NAME.fullmatch(region.name) + if match is None: + raise AssertionError("Unreviewed source entity storage file: " + region.name) + raw = region.read_bytes() + assets["raw/entities/" + region.name] = raw + for coordinates, payload in storage._payloads(raw, int(match[1]), int(match[2])): + for root in storage._entity_chunk(payload, coordinates, VERSION): + if storage._chunk(root) != coordinates: + raise AssertionError("Source entity position does not match its physical region slot") + for identifier, entity, parent in storage._tree(root): + if identifier in seen: + raise AssertionError("Duplicate source entity UUID in stopped regions") + seen.add(identifier) + data = entity.get("ForgeData", {}) + if "AdvWorksActor" not in data: + continue + witness = str(data["AdvWorksActor"]) + if (type(data["AdvWorksActor"]) is not nbtlib.String or witness not in WITNESSES + or witness in result["entities"] or str(entity["id"]) != WITNESSES[witness]): + raise AssertionError("Missing, duplicate or unexpected source actor witness/type") + if parent is not None or "Passengers" in entity: + raise AssertionError("This bounded stopped actor reference does not model passenger fixtures") + if not definition["minX"] <= float(entity["Pos"][0]) <= definition["maxX"]: + raise AssertionError("Source witness outside the actor fixture bounds") + result["entities"][witness] = driver.historical_semantic(plain(entity.unpack()), actors.ENTITY_KEYS, registry) + asset = "raw/witnesses/" + witness + ".nbt" + assets[asset] = encoded(entity) + metadata[witness] = dict(uuid=identifier, region=region.name, chunk=list(coordinates), rawNbtAsset=asset) + if set(result["entities"]) != set(WITNESSES): + raise AssertionError("Stopped source does not contain the exact five actor witnesses") + return driver.source_components(result, VERSION), metadata, assets + + +def seal(source, fixture_receipt, fixture_sha, output): + source = Path(source).resolve(strict=True) + output = Path(output).resolve() + native.validate_hash(fixture_receipt, fixture_sha) + approved = native.read_json(fixture_receipt) + before = fingerprint(source) + if Path(approved["source"]).resolve() != source or approved["sourceFingerprint"] != before: + raise AssertionError("Stopped source differs from its approved fixture receipt") + require_disjoint(output, source) + require_disjoint(output, fixture_receipt) + output.mkdir(parents=True, exist_ok=False) + state, metadata, assets = disk_state(source) + for name, data in assets.items(): + path = output / name + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("xb") as stream: + stream.write(data) + if fingerprint(source) != before: + raise AssertionError("Source changed while sealing its stopped reference") + document = dict(format=1, scope=SCOPE, source=str(source), sourceDataVersion=VERSION, + sourceFingerprint=before, fixtureReceiptSha256=fixture_sha.upper(), sourceUnchanged=True, + referenceBuilderSha256=digest(Path(__file__)), callerMustStopSourceServer=True, + targetObservationsUsed=False, projection="Unchanged actor oracle keys plus exact complete stack/component payloads", + expected=state, actorBindings=metadata, assets={name: digest(output / name) for name in assets}) + write_json(output / "reference.json", document) + return output / "reference.json" + + +def validate(path, checksum, source, fixture_sha): + path, source = Path(path).resolve(strict=True), Path(source).resolve(strict=True) + native.validate_hash(path, checksum) + document = native.read_json(path) + if (document.get("format") != 1 or document.get("scope") != SCOPE or document.get("source") != str(source) + or document.get("sourceDataVersion") != VERSION or document.get("sourceUnchanged") is not True + or document.get("targetObservationsUsed") is not False or document.get("fixtureReceiptSha256") != fixture_sha.upper() + or document.get("referenceBuilderSha256") != digest(Path(__file__)) + or document.get("sourceFingerprint") != fingerprint(source)): + raise AssertionError("Unbound or changed stopped source reference") + expected, bindings, assets = disk_state(source) + if document["expected"] != expected or document["actorBindings"] != bindings or set(document["assets"]) != set(assets): + raise AssertionError("Stopped golden does not derive exactly from source disk") + expected_files = {"reference.json", *assets} + actual_files = {item.relative_to(path.parent).as_posix() for item in path.parent.rglob("*") if item.is_file()} + if actual_files != expected_files: + raise AssertionError("Unexpected/missing reference asset (target evidence is forbidden)") + for name, raw in assets.items(): + artifact = path.parent / name + native.validate_hash(artifact, document["assets"][name]) + if artifact.read_bytes() != raw: + raise AssertionError("Stopped raw reference asset differs from source bytes") + return copy.deepcopy(expected) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + for name in ("source", "fixture-receipt", "output"): + parser.add_argument("--" + name, type=lambda value: Path(value).resolve(), required=True) + parser.add_argument("--fixture-receipt-sha256", required=True) + args = parser.parse_args() + reference = seal(args.source, args.fixture_receipt, args.fixture_receipt_sha256, args.output) + print(reference) + print(digest(reference)) diff --git a/test-rig/migration/native262_strict_migration.py b/test-rig/migration/native262_strict_migration.py new file mode 100644 index 00000000..0dada7fc --- /dev/null +++ b/test-rig/migration/native262_strict_migration.py @@ -0,0 +1,151 @@ +"""Stopped-boot retained-profile and zero-invalid-output gates; old drivers stay unchanged.""" + +import copy +from datetime import datetime, timezone +from pathlib import Path +from types import SimpleNamespace +import traceback + +from matrix import WORLD, digest, fingerprint, write_json +from native117 import output_path +from native117_actor_io import bind +import native262 as native +import native262_upgrade as upgrade +import native262_actor_transformer_order as actor_order +import native262_retained_worldgen as retained + + +REJECT = ("Ignoring invalid OreSpawn-managed ore", "Ignoring OreSpawn-managed ore", + "Ignoring invalid fluid deposit", "Ignoring invalid dimension", + "Ignoring invalid placement rule for OreSpawn-managed ore", + "Ignoring unknown dimension selector", "Ignoring invalid selector rule") + + +def clean_log(path): + diagnostics = native.require_clean_log(path) + lines = Path(path).read_text(encoding="utf-8", errors="strict").splitlines() + failures = [line for line in lines if any(value in line for value in REJECT)] + if failures: + raise AssertionError("Ignored/invalid worldgen outputs are forbidden: " + repr(failures[:8])) + return diagnostics + + +def contract(expected, actual, registry): + result = retained.audit_transition(expected, actual, registry) + outputs = {retained.output_id(row.get("block", key)) for section in retained.SECTIONS + for key, row in expected.get(section, {}).items() if isinstance(row, dict)} + for section in retained.SECTIONS: + for key, rule in actual.get(section, {}).items(): + if not isinstance(rule, dict): + raise AssertionError("Malformed actual ore/deposit rule: " + key) + if key not in expected.get(section, {}) and retained.active(rule) and rule.get("block", key) in outputs: + result["issues"].append(dict(kind="CROSS_SECTION_NEW_ACTIVE_DEFAULT", section=section, rule=key)) + for field, output in [("block", rule.get("block", key)), ("deep_output", rule.get("deep_output")), + *(("outputs", row.get("block")) for row in rule.get("outputs", []))]: + if output is not None and output not in registry: + result["issues"].append(dict(kind="UNKNOWN_ACTUAL_OUTPUT", section=section, rule=key, field=field, output=output)) + result["status"] = "FAIL" if result["issues"] else "PASS" + return result + + +def source_profiles(source): + source = Path(source) + expected, fact = retained.source_expectation(source) + global_file = source / "config/orespawn-worldgen.json" + return dict(world=expected, global_profile=upgrade.read_json(global_file) if global_file.is_file() else None, + oracle=fact, sourceFingerprint=fingerprint(source)) + + +def server_type(base, source): + expectations = source_profiles(source) + + class StrictServer(base): + def close(self): + if getattr(self, "strict_profile_audited", False): + return super().close() + self.strict_profile_audited = True + failure = None + try: + super().close() + except BaseException as error: + failure = error + root = Path(self.native_runtime) + output = root / "strict-profile-audits" / self.native_label + output.mkdir(parents=True, exist_ok=False) + report = dict(status="FAIL", oracle=expectations["oracle"], ignoredOutputWarningCount=None, + driverFailure=repr(failure) if failure else None, profiles={}) + try: + if self.process.poll() is None: + raise AssertionError("Never inspect profile/registry state of an active server") + clean_log(root / ("console-" + self.native_label + ".log")) + report["ignoredOutputWarningCount"] = 0 + world = root / WORLD + registry = retained.registered_blocks(world) + cases = [("world", world / "serverconfig/orespawn-worldgen.json", expectations["world"])] + if expectations["global_profile"] is not None: + cases.append(("global", root / "config/orespawn-worldgen.json", expectations["global_profile"])) + for label, path, expected in cases: + actual = upgrade.read_json(path) + checked = contract(expected, actual, registry) + write_json(output / (label + ".json"), actual) + report["profiles"][label] = dict(file=str(path), sha256=digest(path), contract=checked) + if any(row["contract"]["status"] != "PASS" for row in report["profiles"].values()): + raise AssertionError("Retained global/world profile differs from its own sealed source: " + str(output)) + if fingerprint(Path(source)) != expectations["sourceFingerprint"]: + raise AssertionError("Protected source changed") + if failure is None: + report["status"] = "PASS" + except BaseException as audit: + report.update(error=repr(audit), traceback=traceback.format_exc()) + if failure is None: + failure = audit + else: + failure.add_note("Strict profile/output audit also failed: " + repr(audit)) + finally: + write_json(output / "receipt.json", report) + if failure is not None: + raise failure + + return StrictServer + + +def machine(args, seal): + source, reference, receipt, _, _, _ = upgrade.read_source(args) + native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + native.java25(args.java) + root = output_path(args.output) + files = {**upgrade.oracle_closure(), Path(args.profile): digest(args.profile), + Path(args.inventory): digest(args.inventory), receipt: digest(receipt)} + files.update({Path(row["path"]): row["sha256"] for row in upgrade.read_json(args.profile)["mods"]}) + if Path(seal).exists(): + raise AssertionError("Never replace a pre-run seal") + write_json(Path(seal), dict(scope="GENUINE_PRERUN_STRICT_MACHINE_MIGRATION", setupNotStarted=True, + createdUtc=datetime.now(timezone.utc).isoformat(), files={str(p): h for p, h in files.items()}, + source=str(source), sourceFingerprint=fingerprint(source), reference=str(reference), + referenceFingerprint=fingerprint(reference), seedFingerprint=fingerprint(Path(args.seed)), + oracle=source_profiles(source)["oracle"], arguments={k: str(v) if isinstance(v, Path) else v for k, v in vars(args).items()})) + return upgrade.run(args, server_type=server_type(native.Server, source)) + + +def actors(args): + def campaign(values, *, server_type: type, stage_function, hook_only=False): + return actor_order.run_actor_campaign(values, server_type=globals()["server_type"](server_type, values.fixture), + stage_function=stage_function, hook_only=hook_only) + return bind(actor_order.run, run_actor_campaign=campaign)(args) + + +def main(): + import sys + mode = sys.argv.pop(1) + if mode == "machine": + parser = upgrade.parser() + parser.add_argument("--pre-run-seal", type=Path, required=True) + args = parser.parse_args() + return machine(args, args.pre_run_seal) + if mode == "actors": + return actors(actor_order.parser().parse_args()) + raise ValueError("Expected machine or actors") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native262_transformer_order.py b/test-rig/migration/native262_transformer_order.py new file mode 100644 index 00000000..3de3851b --- /dev/null +++ b/test-rig/migration/native262_transformer_order.py @@ -0,0 +1,481 @@ +"""Pinned Forge65 two-order process proof. Instrumented, never production qualification.""" + +import argparse +import copy +import importlib.util +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tomllib +from types import SimpleNamespace +import traceback +import xml.etree.ElementTree as ET +from zipfile import ZipFile + +from matrix import digest, fingerprint, write_json +from native117 import output_path, require_disjoint +from native117_actor_io import bind +import native12111_order_probe as prior +import native262_upgrade as upgrade + +SCOPE = prior.SCOPE +MOD_ID, VERSION = prior.MOD_ID, "1.0.0.2602001" +MOD_IDS, MODES, VECTORS = upgrade.MOD_IDS, prior.MODES, prior.VECTORS +RIG = Path(__file__).resolve().parents[1] +TEMPLATES = RIG / "order-probe" +TAG = "Lnet/minecraft/nbt/CompoundTag;" +UPGRADE = "(" + TAG + "I" + TAG + "I)" + TAG +SELECTED = "net/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess" +RAW_READ = "(Ljava/nio/file/Path;)" + TAG +TARGETS = ("net/minecraft/world/level/chunk/storage/SimpleRegionStorage", SELECTED, + "net/minecraft/server/level/WorldGenRegion") +GUARD_CONTROLS = ("jar-hash", "class-hash", "config-hash", "missing-config", "duplicate-config", + "not-required", "config-priority", "explicit-class-priority", "missing-mixin-annotation", "mixin-version") +WOVEN_CONTROLS = ("body", "constant-body", "duplicate-handler", "priority", "early-return", "missing-head", + "missing-return", "bypass-head", "worldgen-body", "worldgen-true", "worldgen-extra-cast") +TEMPLATE_HASHES = { + "src/zone/moddev/orderprobe/ProbeGuard.java": "AF284EC4E6826701085BECC159A1984095DD3429CEA85D2BA3160E4399BAD53B", + "src/zone/moddev/orderprobe/OrderProbeAudit.java": "46AE94FB298E1683C1B71B149A307AF42BEBECAE875EE8658F7F104E6509C6C3", + "src/zone/moddev/orderprobe/OrderProbePlugin.java": "21DEABC1DBECCAFE49CBD67F13B44B10426BD4F0A874F1438CA06AD4ECE33FE5", + "src/zone/moddev/orderprobe/OrderProbeMod.java": "54E37CE043BD3DB401E9D8ED383A112340B3DD21A8729716AC1929800CD68582", + "test/WovenNegativeControl.java": "08C5840485E84F58B1D1C698F508F7A6F6A8A74DFA181CE73553B5877FD139FA", + "test/run_guard_controls.py": "307CCF9BEBDD58E242A7B4D961D372803E16315B0D2F8286DBCCEC3A7A0DDDC3", + "test/run_woven_controls.py": "A6892AAADBF027953569D71C9EC9FCA33BB539CFBB47BD2C563CA63D03A26F81", + "resources/META-INF/mods.toml": "01A57052CC5BD5CEA7458BBD75ADFEE6C0F7E01B73DD431420BE570F5658A954", + "resources/advantageorderprobe.mixins.json": "1652355A2C5397D6F34CC7F07C8355D85F830876E107D32625A98F459E7644DC", +} + + +def replace_once(text, old, new): + if text.count(old) != 1: + raise AssertionError("Unreviewed order-probe template drift: " + old[:90]) + return text.replace(old, new, 1) + + +LOCK_AUDIT = ''' MethodNode selected = method(level, "getUnfixedDataTag", "(Z)Lcom/mojang/serialization/Dynamic;"); + MethodNode nativeSelected = method(exported(nativeClasses, LEVEL), "getUnfixedDataTag", selected.desc); + List selectedCode = instructions(selected); + int lockedAt = -1, rawAt = -1; + List capturedAt = new ArrayList<>(); + for (int i = 0; i < selectedCode.size(); i++) { + if (!(selectedCode.get(i) instanceof MethodInsnNode call)) continue; + if (call.owner.equals(LEVEL) && call.name.equals("checkLock") && call.desc.equals("()V")) { + ProbeGuard.require(lockedAt < 0, "duplicate selected-world lock check"); lockedAt = i; + } + if (call.owner.equals("net/minecraft/world/level/storage/LevelStorageSource") + && call.name.equals("readLevelDataTagRaw") + && call.desc.equals("(Ljava/nio/file/Path;)Lnet/minecraft/nbt/CompoundTag;")) { + ProbeGuard.require(rawAt < 0, "duplicate selected-world raw read"); rawAt = i; + } + if (worldHeads.containsKey(call.name)) capturedAt.add(i); + ProbeGuard.require(!call.name.equals("isCancelled"), "selected-world capture became cancellable"); + } + ProbeGuard.require(lockedAt >= 0 && capturedAt.size() == 2 && lockedAt < capturedAt.getFirst() + && capturedAt.getLast() < rawAt, "capture is not after session lock and before raw read"); + for (int site : capturedAt) { + ProbeGuard.require(dominates(selected, lockedAt, site), "selected-world lock can be bypassed"); + ProbeGuard.require(dominates(selected, site, rawAt), "selected-world capture can be bypassed"); + } + ProbeGuard.require(dominates(selected, capturedAt.getFirst(), capturedAt.getLast()), "capture order can be bypassed"); + ProbeGuard.require(selected.tryCatchBlocks.stream().map(row -> row.type).toList().equals( + nativeSelected.tryCatchBlocks.stream().map(row -> row.type).toList()), "selected-world exception coverage changed"); + List levelOrder = sequence(selected, worldHeads);''' + + +def retarget(relative, text): + if relative.endswith("OrderProbeAudit.java"): + text = replace_once(text, '"net/minecraft/world/level/storage/LevelStorageSource";', '"' + SELECTED + '";') + text = replace_once(text, '+ target.name.substring(target.name.lastIndexOf(\'/\') + 1) + "Mixin";', + '+ (target.name.equals(LEVEL) ? (mod.equals("basepower") ? "SelectedWorldStorageMixin" : "LevelStorageSourceMixin")\n' + ' : target.name.substring(target.name.lastIndexOf(\'/\') + 1) + "Mixin");') + text = replace_once(text, '"(" + TAG + "I" + TAG + ")" + TAG', '"(" + TAG + "I" + TAG + "I)" + TAG') + text = replace_once(text, ' List levelOrder = sequence(method(level, "readLevelDataTagFixed", null), worldHeads);', LOCK_AUDIT) + text = replace_once(text, ' result.addProperty("exceptionEdgesChecked", true);', + ' result.addProperty("exceptionEdgesChecked", true);\n' + ' result.addProperty("minecraft", "26.2"); result.addProperty("forge", "65.1.0");\n' + ' result.addProperty("dataVersion", 4903); result.addProperty("upgradeDescriptor", upgrade.desc);\n' + ' result.addProperty("selectedWorldAfterLock", true);\n' + ' result.addProperty("selectedWorldTarget", LEVEL);\n' + ' result.addProperty("rawReadDescriptor", "(Ljava/nio/file/Path;)Lnet/minecraft/nbt/CompoundTag;");') + elif relative.endswith("OrderProbePlugin.java"): + text = replace_once(text, "native61-order-plugin-", "native65-order-plugin-") + text = replace_once(text, "NATIVE61_ORDER_PROBE_PRIORITIES", "NATIVE65_ORDER_PROBE_PRIORITIES") + elif relative.endswith("WovenNegativeControl.java"): + text = replace_once(text, "(Lnet/minecraft/nbt/CompoundTag;ILnet/minecraft/nbt/CompoundTag;)Lnet/minecraft/nbt/CompoundTag;", UPGRADE) + elif relative.endswith(".py"): + text = replace_once(text, "native12111 as native", "native262 as native") + text = replace_once(text, "native12111_order_probe", "native262_transformer_order") + text = replace_once(text, '"--release", "21"', '"--release", "25"') + elif relative.endswith("mods.toml"): + for old, new in (('[61,)', '[65,)'), ('1.0.0.121111', VERSION), ('Native61', 'Native65'), + ('[61.1.0]', '[65.1.0]'), ('[1.21.11]', '[26.2]')): + text = replace_once(text, old, new) + return text + + +def prepare_sources(output): + payloads = {} + for relative, checksum in TEMPLATE_HASHES.items(): + path = TEMPLATES / relative + upgrade.validate_hash(path, checksum) + payloads[relative] = retarget(relative, path.read_text(encoding="utf-8")) + # Fail before creating any output if a template is stale. + output.mkdir(parents=True, exist_ok=False) + for relative, text in payloads.items(): + path = output / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8", newline="\n") + return fingerprint(output) + + +def builder(): + spec = importlib.util.spec_from_file_location("native262_pinned_probe_builder", TEMPLATES / "build.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def verify_files(hashes): + for path, checksum in hashes.items(): + upgrade.validate_hash(Path(path), checksum) + + +def verify_probe_package(path): + required = ("META-INF/mods.toml", "advantageorderprobe.mixins.json", "order-probe-pins.json", + *("zone/moddev/orderprobe/" + name + ".class" for name in + ("OrderProbeMod", "OrderProbeAudit", "OrderProbePlugin", "ProbeGuard"))) + with ZipFile(path) as archive: + for name in required: + if archive.namelist().count(name) != 1: + raise AssertionError("Missing/duplicate order sidecar entry: " + name) + metadata = tomllib.loads(archive.read("META-INF/mods.toml").decode("utf-8")) + if [(row.get("modId"), row.get("version")) for row in metadata.get("mods", [])] != [(MOD_ID, VERSION)]: + raise AssertionError("Order sidecar must declare exactly its own current mod identity") + + +def build_sidecar(profile, seed, java, output): + native = upgrade.engine() + java, _ = native.java25(java) + baseline = builder() + classes = copy.deepcopy(baseline.CLASSES) + classes["basepower"].insert(2, "SelectedWorldStorageMixin") + pins = bind(baseline.base_pins, native=native, CLASSES=classes, RIG=RIG)(profile, seed) + classpath_file = RIG.parent / ".classpath" + classpath = [Path(row.attrib["path"]).resolve(strict=True) + for row in ET.parse(classpath_file).getroot().findall("classpathentry") if row.attrib.get("kind") == "lib"] + if not classpath or any(not path.is_file() for path in classpath): + raise AssertionError("Missing sealed javac dependencies") + engine = Path(seed) / "libraries/net/minecraftforge/forge/26.2-65.1.0/forge-26.2-65.1.0-server.jar" + engine_sha = digest(engine) + output = output_path(output) + for path in (profile, seed, java, classpath_file, TEMPLATES, Path(__file__), *classpath): + require_disjoint(output, path) + output.mkdir(parents=True, exist_ok=False) + source_tree = output / "source-tree" + sources = prepare_sources(source_tree) + compiled = output / "classes" + compiled.mkdir() + cp_hashes = {str(path): digest(path) for path in classpath} + command = [str(java.with_name("javac.exe" if os.name == "nt" else "javac")), "-proc:none", "--release", "25", + "-cp", os.pathsep.join(map(str, classpath)), "-d", str(compiled), *map(str, sorted((source_tree / "src").rglob("*.java")))] + completed = subprocess.run(command, env=native.native_launch_environment(), capture_output=True, text=True, timeout=120) + (output / "javac.log").write_text(completed.stdout + completed.stderr, encoding="utf-8") + if completed.returncode: + raise AssertionError("Order probe compilation failed: " + str(output / "javac.log")) + native_classes = output / "native-classes" + with ZipFile(engine) as original: + for name in TARGETS: + path = native_classes / (name + ".class") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(baseline.unique_entry(original, name + ".class")) + variants = {} + for mode in MODES: + directory = output / mode + directory.mkdir() + selected = copy.deepcopy(pins) + selected.update(mode=mode, priorities={"basepower.mixins.json": 900 if mode == "power-low" else 1100, + "basemetals.mixins.json": 1100 if mode == "power-low" else 900}) + raw = (json.dumps(selected, indent=2, sort_keys=True) + "\n").encode() + plan = directory / "order-probe-pins.json" + plan.write_bytes(raw) + files = {path.relative_to(compiled).as_posix(): path.read_bytes() for path in compiled.rglob("*.class")} + files.update({path.relative_to(source_tree / "resources").as_posix(): path.read_bytes() + for path in (source_tree / "resources").rglob("*") if path.is_file()}) + files["order-probe-pins.json"] = raw + files["META-INF/MANIFEST.MF"] = ("Manifest-Version: 1.0\r\nMixinConfigs: advantageorderprobe.mixins.json\r\n" + "Automatic-Module-Name: advantageorderprobe\r\n\r\n").encode() + jar = directory / ("AdvantageOrderProbe-" + VERSION + ".jar") + baseline.archive(jar, files) + verify_probe_package(jar) + variants[mode] = dict(jar=str(jar), sha256=digest(jar), pins=str(plan), pinsSha256=digest(plan)) + verify_files(cp_hashes) + if fingerprint(source_tree) != sources or digest(engine) != engine_sha: + raise AssertionError("Probe build inputs changed") + result = dict(scope=SCOPE, qualificationAllowed=False, minecraft=upgrade.MINECRAFT, forge=upgrade.FORGE, + profile=str(profile), profileSha256=digest(profile), javaSha256=digest(java), sourceTree=str(source_tree), + sourceFingerprint=sources, templateHashes=TEMPLATE_HASHES, builderSha256=digest(TEMPLATES / "build.py"), + adapterSha256=digest(Path(__file__)), classpathFile=str(classpath_file), classpathFileSha256=digest(classpath_file), + classpath=cp_hashes, nativeEngine=str(engine), nativeEngineSha256=engine_sha, + nativeClasses=str(native_classes), nativeClassHashes=fingerprint(native_classes), variants=variants) + write_json(output / "build-receipt.json", result) + return result + + +def verify_woven(proof): + required = dict(status="PASS", scope=SCOPE, qualificationAllowed=False, minecraft=upgrade.MINECRAFT, + forge=upgrade.FORGE, dataVersion=4903, upgradeDescriptor=UPGRADE, + selectedWorldAfterLock=True, selectedWorldTarget=SELECTED, rawReadDescriptor=RAW_READ, + allHandlerBodiesMatchPinnedProduction=True, nativeReturnsCovered=2, exceptionEdgesChecked=True) + if any(type(proof.get(key)) is not type(value) or proof[key] != value for key, value in required.items()): + raise AssertionError("Missing Forge65 locked-world / four-argument woven proof") + prior.compare_orders(proof, {**proof, **{name: ([list(reversed(pair)) for pair in proof[name]] if name == "regionReturns" + else list(reversed(proof[name]))) for name in VECTORS}}) + return proof + + +def compare_orders(first, second): + verify_woven(first) + verify_woven(second) + return prior.compare_orders(first, second) + + +verify_plugin = prior.verify_plugin + + +def diagnostic_manifest(runtime, label): + path = Path(runtime) / ("order-export-" + label + ".json") + document = dict(format=1, purpose="NATIVE65_MIXIN_EXPORT_DIAGNOSTIC", runtime=str(Path(runtime).resolve()), label=label, + properties={"mixin.debug.export": True, "mixin.debug.export.decompile": False}) + with path.open("x", encoding="utf-8") as output: + json.dump(document, output, sort_keys=True, indent=2) + output.write("\n") + return path, digest(path) + + +def archive_exports(runtime, folder): + runtime, folder = Path(runtime).resolve(strict=True), Path(folder).resolve(strict=True) + folder.relative_to(runtime) + source = runtime / ".mixin.out" + source.resolve(strict=True).relative_to(runtime) + for target in TARGETS: + upgrade.regular_bytes(source / "class" / (target + ".class")) + for path in source.rglob("*"): + if path.is_symlink() or getattr(path, "is_junction", lambda: False)(): + raise AssertionError("Aliased Mixin export input") + if path.is_file(): + upgrade.regular_bytes(path) + shutil.copytree(source / "class", folder / "export") + source.rename(folder / "mixin-output") + return fingerprint(folder / "mixin-output") + + +def audit_command(java, build, variant, runtime, exports, output): + proof = bind(prior.audit_command, native=upgrade.engine())(java, build, variant, runtime, exports, output) + return verify_woven(proof) + + +def instrumented_stage(variant): + def stage(runtime, seed, inventory, mods, port): + native = upgrade.engine() + upgrade.validate_hash(variant["jar"], variant["sha256"]) + expected = native.stage(runtime, seed, inventory, mods, port) + jar = Path(variant["jar"]) + if jar.name in expected: + raise AssertionError("Order sidecar collides with a production input") + shutil.copy2(jar, Path(runtime) / "mods" / jar.name) + expected[jar.name] = variant["sha256"].upper() + path = Path(runtime) / "input-jars.json" + inputs = upgrade.read_json(path) + inputs.append(dict(file=jar.name, source=str(jar), sha256=variant["sha256"].upper(), + scope=SCOPE, qualificationAllowed=False)) + write_json(path, inputs) + native.verify_runtime(runtime, inventory, expected) + return expected + return stage + + +def controls(java, build_path, build, output, *, runtime=None, exports=None): + woven = runtime is not None + script = Path(build["sourceTree"]) / "test" / ("run_woven_controls.py" if woven else "run_guard_controls.py") + command = [sys.executable, "-B", str(script), "--build-receipt", str(build_path), "--java", str(java), "--output", str(output)] + if woven: + command.extend(["--runtime", str(runtime), "--exports", str(exports)]) + env = upgrade.engine().native_launch_environment() + env["PYTHONPATH"] = os.pathsep.join([str(RIG / "migration"), *sys.path]) + done = subprocess.run(command, env=env, capture_output=True, text=True, timeout=300) + Path(str(output) + ".log").write_text(done.stdout + done.stderr, encoding="utf-8") + if done.returncode: + raise AssertionError("Order mutation controls failed: " + str(output)) + path = Path(output) / ("negative-controls.json" if woven else "guard-controls.json") + receipt = upgrade.read_json(path) + expected = {"unmodified" if woven else "baseline": "PASS", **dict.fromkeys(WOVEN_CONTROLS if woven else GUARD_CONTROLS, "REJECTED")} + if (receipt.get("status") != "PASS" or receipt.get("scope") != SCOPE + or receipt.get("qualificationAllowed") is not False or receipt.get("controls") != expected + or receipt.get("buildReceiptSha256") != digest(build_path)): + raise AssertionError("Missing exact 21 mutation controls / positive baselines") + return dict(file=str(path), sha256=digest(path), **receipt) + + +def validate_build(build, profile, java): + if (build.get("scope") != SCOPE or build.get("qualificationAllowed") is not False + or (build.get("minecraft"), build.get("forge")) != (upgrade.MINECRAFT, upgrade.FORGE) + or build["profileSha256"] != digest(profile) or build["javaSha256"] != digest(java) + or build["templateHashes"] != TEMPLATE_HASHES): + raise AssertionError("Probe is not bound to this exact Forge65 production profile") + paths = {Path(path): sha for path, sha in build["classpath"].items()} + paths.update({TEMPLATES / path: sha for path, sha in TEMPLATE_HASHES.items()}) + paths.update({Path(__file__): build["adapterSha256"], TEMPLATES / "build.py": build["builderSha256"], + Path(build["nativeEngine"]): build["nativeEngineSha256"], + Path(build["classpathFile"]): build["classpathFileSha256"]}) + for variant in build["variants"].values(): + paths.update({Path(variant["jar"]): variant["sha256"], Path(variant["pins"]): variant["pinsSha256"]}) + verify_probe_package(Path(variant["jar"])) + verify_files(paths) + for folder, hashes in ((build["nativeClasses"], build["nativeClassHashes"]), (build["sourceTree"], build["sourceFingerprint"])): + if fingerprint(Path(folder)) != hashes: + raise AssertionError("Pinned native audit or generated source tree changed") + paths.update({Path(folder) / name: sha for name, sha in hashes.items()}) + if set(build["variants"]) != set(MODES): + raise AssertionError("Both pinned order variants are required") + return paths + + +def run(args): + native = upgrade.engine() + upgrade.read_source(args) + upgrade.validate_hash(args.build_receipt, args.build_receipt_sha256) + build = upgrade.read_json(args.build_receipt) + target = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + java, _ = native.java25(args.java) + protected = validate_build(build, args.profile, java) + protected.update({Path(args.build_receipt): digest(args.build_receipt), Path(args.profile): digest(args.profile), + Path(args.inventory): digest(args.inventory), java: digest(java), **upgrade.oracle_closure()}) + protected.update({Path(path): checksum for _, path, checksum in target[2].values()}) + trees = {Path(path): fingerprint(Path(path)) for path in (args.source, args.legacy_reference or args.source, args.seed)} + root = output_path(args.output) + for path in (args.source, args.legacy_reference or args.source, args.profile, args.seed, args.inventory, java, *protected, + *(row[1] for row in target[2].values())): + require_disjoint(root, path) + root.mkdir(parents=True, exist_ok=False) + hook_only = getattr(args, "hook_only", False) + result = dict(status="RUNNING", scope=SCOPE, qualificationAllowed=False, minecraft=upgrade.MINECRAFT, + forge=upgrade.FORGE, profileSha256=digest(args.profile), buildReceiptSha256=digest(args.build_receipt), + inputHashes={str(path): sha for path, sha in protected.items()}, + migrationScope=upgrade.HOOK_SCOPE if hook_only else upgrade.SCOPE, + operationRequested=not hook_only, modes={}) + report = root / "native262-transformer-order-results.json" + write_json(report, result) + try: + result["guardControls"] = controls(java, args.build_receipt, build, root / "guard-controls") + for index, mode in enumerate(MODES): + variant = build["variants"][mode] + pins = upgrade.read_json(variant["pins"]) + observations = [] + result["modes"][mode] = dict(status="RUNNING", observations=observations) + write_json(report, result) + + class OrderServer(native.Server): + def __init__(self, runtime, java, launcher, label): + self.order_archived = False + if (Path(runtime) / ".mixin.out").exists(): + raise AssertionError("Never reuse stale exports from an earlier boot") + manifest, checksum = diagnostic_manifest(runtime, label) + super().__init__(runtime, java, launcher, label, diagnostic_environment=manifest, + diagnostic_environment_sha256=checksum) + + def close(self): + failure = None + try: + super().close() + except BaseException as error: + failure = error + try: + if not self.order_archived: + if self.process.poll() is None: + raise AssertionError("Never archive an active order-probe JVM") + self.order_archived = True + runtime = self.native_runtime + folder = runtime / "order-evidence" / self.native_label + folder.mkdir(parents=True, exist_ok=False) + plugin_file = runtime / ("native65-order-plugin-" + str(self.process.pid) + ".json") + plugin = upgrade.read_json(plugin_file) + verify_plugin(plugin, pins, runtime, self.process.pid) + if plugin["pinsSha256"] != variant["pinsSha256"]: + raise AssertionError("Plugin resource pins changed") + shutil.copy2(plugin_file, folder / "plugin.json") + exports = archive_exports(runtime, folder) + woven = audit_command(java, build, variant, runtime, folder / "export", folder / "audit.json") + launch = upgrade.read_json(runtime / ("native65-launch-environment-" + self.native_label + ".json")) + if launch["status"] != "DIAGNOSTIC_NOT_QUALIFIED" or launch["qualificationAllowed"] is not False: + raise AssertionError("Diagnostic launch was relabelled production qualified") + observations.append(dict(runtime=str(runtime), label=self.native_label, folder=str(folder), audit=woven, + auditSha256=digest(folder / "audit.json"), pluginSha256=digest(plugin_file), + fullExportHashes=exports)) + except BaseException as audit: + if failure is None: + raise + failure.add_note("Order archive/audit also failed: " + repr(audit)) + if failure is not None: + raise failure + + values = SimpleNamespace(**vars(args)) + values.output, values.port = root / mode, args.port + index + expected_boots = 2 if hook_only else 3 + if upgrade.run(values, server_type=OrderServer, stage_function=instrumented_stage(variant), + hook_only=hook_only) or len(observations) != expected_boots: + raise AssertionError("Missing real woven process proofs for the explicitly requested migration scope") + signature = {key: observations[0]["audit"][key] for key in VECTORS} + if any({key: observation["audit"][key] for key in VECTORS} != signature for observation in observations): + raise AssertionError("Actual callback order changed across cold boots") + result["modes"][mode] = dict(status="PASS", observations=observations) + write_json(report, result) + first = result["modes"][MODES[0]]["observations"][0] + result["wovenControls"] = controls(java, args.build_receipt, build, root / "woven-controls", + runtime=Path(first["runtime"]), exports=Path(first["folder"]) / "export") + result["permutationProof"] = compare_orders(first["audit"], result["modes"][MODES[1]]["observations"][0]["audit"]) + result["status"] = "PASS" + except BaseException as failure: + result.update(status="FAIL", error=repr(failure), traceback=traceback.format_exc()) + raise + finally: + try: + validate_build(build, args.profile, java) + result["inputsUnchanged"] = all(digest(path) == sha.upper() for path, sha in protected.items()) + result["protectedTreesUnchanged"] = all(fingerprint(path) == hashes for path, hashes in trees.items()) + if not result["inputsUnchanged"] or not result["protectedTreesUnchanged"]: + raise AssertionError("Pinned order inputs changed") + except Exception as audit: + result.update(status="FAIL", custodyFailure=repr(audit)) + write_json(report, result) + return int(result["status"] != "PASS") + + +def main(): + if len(sys.argv) > 1 and sys.argv[1] == "build": + parser = argparse.ArgumentParser(description="Compile the local diagnostic sidecar without launching Minecraft") + for name in ("profile", "seed", "inventory", "java", "output"): + parser.add_argument("--" + name, type=lambda value: Path(value).resolve(), required=True) + parser.add_argument("--inventory-sha256", required=True) + args = parser.parse_args(sys.argv[2:]) + native = upgrade.engine() + native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + build_sidecar(args.profile, args.seed, args.java, args.output) + return 0 + parser = upgrade.parser() + parser.description = __doc__ + parser.add_argument("--hook-only", action="store_true", help="Explicit non-consuming conversion/restart proof; never operating qualification") + parser.add_argument("--build-receipt", type=lambda value: Path(value).resolve()) + parser.add_argument("--build-receipt-sha256") + args = parser.parse_args() + if not 1024 <= args.port <= 65534: + parser.error("Require two assigned consecutive unprivileged ports") + if args.build_receipt is None or args.build_receipt_sha256 is None: + parser.error("Require the independently pinned sidecar build receipt and SHA") + return run(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native262_upgrade.py b/test-rig/migration/native262_upgrade.py new file mode 100644 index 00000000..0673364c --- /dev/null +++ b/test-rig/migration/native262_upgrade.py @@ -0,0 +1,402 @@ +"""Strict machine migration to Forge65; historical inputs and oracles stay immutable.""" + +import argparse +import copy +import importlib +import json +from pathlib import Path +import re +import shutil +import sys +import traceback + +import nbtlib + +from matrix import WORLD, digest, fingerprint, parse_nbt, plain, write_json +from native117 import output_path, require_disjoint +from native117_actor_io import bind +from native117_entity_storage import _decompress, _entity_chunk, _nbt, _payloads +from native117_format import decode_palette, uuid_longs +import native12111_upgrade as previous +import native262_layout as layout + + +MINECRAFT, FORGE, DATA_VERSION = "26.2", "65.1.0", 4903 +SOURCE_VERSIONS = {"1.13.2": 1631, "1.14.4": 1976, "1.15.2": 2230, "1.16.5": 2586, + "1.17.1": 2730, "1.18.2": 2975, "1.19.4": 3337, + **previous.COMPONENT_SOURCE_VERSIONS, "1.21.11": 4671, "26.1.2": 4790} +LEGACY_ROUTES = ("original110", "released110", "maintained112", "released112") +LEGACY_SOURCE_VERSIONS = dict(original110=512, released110=512, maintained112=1343, released112=1343) +MOD_IDS = frozenset(("basepower", "basesteam", "baseelectric", "basemetals", "orespawn", "advantageworkstest")) +SCOPE = "MACHINE_MIGRATION_STOPPED_STORAGE_AND_PLAYER_AUTHORITY" +HOOK_SCOPE = "INSTRUMENTED_MIGRATION_HOOK_CONVERSION_RESTART_ONLY" +MAX_FILE = 256 * 1024 * 1024 + + +def read_json(path): + def unique(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError("Duplicate receipt key: " + key) + result[key] = value + return result + return json.loads(Path(path).read_text(encoding="utf-8-sig"), object_pairs_hook=unique, + parse_constant=lambda value: (_ for _ in ()).throw(ValueError("Nonfinite JSON: " + value))) + + +def engine(): + module = importlib.import_module("native262") + if (module.MINECRAFT, module.FORGE) != (MINECRAFT, FORGE): + raise AssertionError("Migration requires the handed-off Forge65.1.0 engine") + for name in ("Server", "java25", "read_family_target", "stage", "verify_runtime", "LAUNCHER"): + if not hasattr(module, name): + raise AssertionError("Missing native262 engine contract: " + name) + return module + + +def validate_hash(path, checksum): + if not isinstance(checksum, str) or not re.fullmatch(r"[A-Fa-f0-9]{64}", checksum) or digest(Path(path)) != checksum.upper(): + raise AssertionError("Input checksum differs: " + str(path)) + + +def expected_components(value, *, source_version): + if not isinstance(source_version, str) or source_version not in SOURCE_VERSIONS: + raise AssertionError("Unsupported sealed source version: " + str(source_version)) + if source_version in previous.COMPONENT_SOURCE_VERSIONS: + return previous.expected_components(value, source_version=source_version) + if SOURCE_VERSIONS[source_version] < 3465: + return previous.components(copy.deepcopy(value)) + # 1.21.11 and 26.1.2 already have native counts and unwrapped enchantment maps. Never + # recursively reinterpret unknown component payloads as another item schema. + def walk(node): + if isinstance(node, list): + return [walk(item) for item in node] + if not isinstance(node, dict): + return copy.deepcopy(node) + if "id" not in node or not any(key in node for key in ("Count", "count")): + return {key: walk(item) for key, item in node.items()} + if "Count" in node or type(node.get("count")) is not int or node["count"] < 1: + raise AssertionError("1.21.11 source requires native integer count") + if not isinstance(node["id"], str) or not re.fullmatch(r"[a-z0-9_.-]+:[a-z0-9_./-]+", node["id"]): + raise AssertionError("Invalid source item identity") + patch = node.get("components", {}) + if not isinstance(patch, dict): + raise AssertionError("Source components must be a map") + for name in ("minecraft:enchantments", "minecraft:stored_enchantments"): + if name not in patch: + continue + levels = patch[name] + if not isinstance(levels, dict) or any(not isinstance(key, str) + or not re.fullmatch(r"[a-z0-9_.-]+:[a-z0-9_./-]+", key) + or type(level) is not int or not 1 <= level <= 255 for key, level in levels.items()): + raise AssertionError("Non-native 1.21.11 enchantment map") + return copy.deepcopy(node) + return walk(value) + + +def comparison(before, after, *, modern, source_version="1.20.1"): + return bind(previous.comparison, expected_components=expected_components)( + before, after, modern=modern, source_version=source_version) + + +load_operating_chunks = bind(previous.load_operating_chunks, MINECRAFT=MINECRAFT) + + +def oracle_closure(): + directory = Path(__file__).resolve().parent + return {Path(module.__file__).resolve(): digest(Path(module.__file__)) for module in tuple(sys.modules.values()) + if getattr(module, "__file__", None) and Path(module.__file__).suffix == ".py" + and Path(module.__file__).resolve().parent == directory + and not Path(module.__file__).name.startswith("test_")} + + +def regular_bytes(path): + path = Path(path) + for part in (path, *path.parents): + if part.is_symlink() or getattr(part, "is_junction", lambda: False)(): + raise ValueError("Aliased stopped-save input") + if not path.is_file() or path.stat().st_nlink != 1 or path.stat().st_size > MAX_FILE: + raise ValueError("Missing, aliased or oversized stopped-save input") + return path.read_bytes() + + +def player_authority(world): + identity = layout.singleplayer_uuid(world) + if identity is None: + return dict(status="NO_SINGLEPLAYER_OWNER", playerConversionQualified=False) + path = layout.player_file(world, identity) + tag = layout._load(path) + if layout.uuid_tag(tag.get("UUID")) != identity: + raise AssertionError("Host UUID differs from its authoritative player file") + version = tag.get("DataVersion") + if type(version) is not nbtlib.Int or not 0 < int(version) <= DATA_VERSION: + raise AssertionError("Unproven host-player serialization version") + return dict(status="NATIVE_PLAYER" if int(version) == DATA_VERSION else "RETAINED_UNOPENED_SOURCE_PLAYER", + uuid=str(identity), file=str(path.relative_to(world)), sha256=digest(path), + dataVersion=int(version), playerConversionQualified=False) + + +def capture(runtime, label, rows): + world = Path(runtime) / WORLD + output = Path(runtime) / "native262-save" / label + output.mkdir(parents=True, exist_ok=False) + level, settings = layout.read_level(world), layout.read_worldgen(world) + shutil.copy2(world / "level.dat", output / "level.dat") + shutil.copy2(layout.shared_file(world, "minecraft:world_gen_settings"), output / "world_gen_settings.dat") + chunks, regions = {}, {} + identities = set() + for row in rows: + pos = tuple(row[axis] for axis in ("x", "y", "z")) + if any(type(value) is not int for value in pos) or (row["name"], *pos) in identities: + raise AssertionError("Invalid or duplicate live machine witness") + identities.add((row["name"], *pos)) + x, y, z = pos + cx, cz = key = x // 16, z // 16 + if key not in chunks: + region = cx // 32, cz // 32 + if region not in regions: + path = layout.region_file(world, "minecraft:overworld", cx, cz) + raw = regular_bytes(path) + regions[region] = dict(_payloads(raw, *region)) + (output / path.name).write_bytes(raw) + if key not in regions[region]: + raise AssertionError("Missing stopped native terrain witness") + payload = regions[region][key] + tag = _nbt(_decompress(payload[5:], payload[4])) + if (type(tag.get("DataVersion")) is not nbtlib.Int or int(tag["DataVersion"]) != DATA_VERSION + or "Level" in tag or any(type(tag.get(name)) is not nbtlib.Int for name in ("xPos", "zPos")) + or tuple(int(tag[name]) for name in ("xPos", "zPos")) != key): + raise AssertionError("Saved chunk differs from its native4903 region slot") + (output / f"{cx}_{cz}.chunk").write_bytes(payload) + chunk = plain(tag.unpack()) + sections, tiles = {}, {} + for section in chunk["sections"]: + section_y = section["Y"] + if section_y in sections or any(name in section for name in ("Blocks", "Palette", "BlockStates")): + raise AssertionError("Duplicate section or historical target palette") + states = section.get("block_states") + sections[section_y] = decode_palette(states["palette"], states.get("data", [])) if states else None + for tile in chunk["block_entities"]: + location = tuple(tile[axis] for axis in ("x", "y", "z")) + if location in tiles: + raise AssertionError("Duplicate stopped block entity") + if "keepPacked" in tile: + if tile.pop("keepPacked") != 0: + raise AssertionError("Unreviewed packed block-entity representation") + 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("Stopped block differs from actual live state: " + str(pos)) + if tiles.get(pos) != (plain(parse_nbt(row)) if row.get("nbtBase64") else None): + raise AssertionError("Stopped block entity differs exactly from live NBT: " + str(pos)) + if not identities: + raise AssertionError("Empty migration witnesses cannot qualify") + result = dict(status="PASS", scope=SCOPE, witnesses=len(rows), nativeDataVersion=DATA_VERSION, + worldGenSettings=plain(settings.unpack()), playerAuthority=player_authority(world), + files={path.name: digest(path) for path in output.iterdir()}) + write_json(output / "receipt.json", result) + return result + + +def turbine(world, position): + x, _, z = position + cx, cz = x // 16, z // 16 + path = layout.region_file(world, "minecraft:overworld", cx, cz, "entities") + payloads = dict(_payloads(regular_bytes(path), cx // 32, cz // 32)) + entities = _entity_chunk(payloads[(cx, cz)], (cx, cz), DATA_VERSION) + found = [tag for tag in entities if str(tag["id"]) == "baseelectric:hydroturbine"] + if len(found) != 1: + raise AssertionError("Expected exactly one stopped native turbine") + tag = found[0] + words = tag.get("UUID") + if type(words) is not nbtlib.IntArray or len(words) != 4 or any(key in tag for key in ("UUIDMost", "UUIDLeast")): + raise AssertionError("Turbine requires its native UUID int[4]") + return dict(id=str(tag["id"]), parent=list(map(int, tag["parentPos"])), + position=list(map(float, tag["Pos"])), uuid=list(uuid_longs(words.tolist()))) + + +def copy_source(source, runtime): + before = fingerprint(Path(source) / WORLD) + shutil.copytree(Path(source) / WORLD, Path(runtime) / WORLD) + if fingerprint(Path(runtime) / WORLD) != before: + raise AssertionError("Disposable world differs from its sealed source") + overlay = {} + config = Path(source) / "config" + if config.is_dir(): + for path in sorted(config.rglob("*")): + if not path.is_file(): + continue + relative = path.relative_to(config) + target = Path(runtime) / "config" / relative + if relative.as_posix() == "fml.toml" and target.is_file(): + overlay[str(relative)] = dict(authority="NATIVE_TARGET_LOADER", sourceSha256=digest(path), targetSha256=digest(target)) + continue + if target.exists() and regular_bytes(target) != regular_bytes(path): + raise AssertionError("Unreviewed target/source configuration collision: " + str(relative)) + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, target) + write_json(Path(runtime) / "historical-config-custody.json", overlay) + + +def with_server(runtime, java, label, action, *, server_type=None): + native = engine() + server = (server_type or native.Server)(runtime, java, native.LAUNCHER, label) + try: + action(server) + server.send("tick freeze") + server.wait("The game is frozen", 30) + 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("Shutdown/log audit also failed: " + repr(shutdown)) + raise + else: + server.close() + return read_json(Path(runtime) / f"migration-{label}.json") + + +def read_source(args): + if args.source_route not in (*LEGACY_ROUTES, *SOURCE_VERSIONS): + raise AssertionError("Unsupported explicit source route") + source = Path(args.source).resolve(strict=True) + reference = Path(args.legacy_reference).resolve(strict=True) if args.legacy_reference else source + path = reference / args.source_receipt_name + validate_hash(path, args.receipt_sha256) + prior = read_json(path) + restart = prior["phases"]["restarted"] + version = prior.get("minecraft", "1.20.1" if args.legacy_reference else None) + serialized = restart.get("serialized", {}) + if (not isinstance(version, str) or version not in SOURCE_VERSIONS or type(serialized.get("nativeDataVersion")) is not int + or serialized["nativeDataVersion"] != SOURCE_VERSIONS[version]): + raise AssertionError("Unproven sealed source version/data-version pair") + if prior.get("status") != "PASS" or prior.get("sourceUnchanged") is not True or restart.get("status") != "PASS": + raise AssertionError("Missing passing immutable source and restart proof") + if not args.legacy_reference and args.source_route in SOURCE_VERSIONS and args.source_route != version: + raise AssertionError("Source route differs from the sealed receipt platform") + if args.source_route in LEGACY_ROUTES and not args.legacy_reference: + raise AssertionError("Raw historical routes require their independent reference") + snapshot = reference / "migration-restarted.json" + validate_hash(snapshot, restart["snapshotSha256"]) + if args.legacy_reference: + route = args.source_route + allowed = {**LEGACY_SOURCE_VERSIONS, **{name: number for name, number in SOURCE_VERSIONS.items() if number < 3465}} + if version != "1.20.1" or route not in allowed: + raise AssertionError("Historical bridge requires a sealed 1.20.1 reference and an older source route") + portable = lambda values: {key.replace("\\", "/"): value.upper() for key, value in values.items()} + if source != Path(prior["source"]).resolve() or portable(fingerprint(source)) != portable(prior["sourceFingerprint"]): + raise AssertionError("Original historical source no longer matches its independent reference") + level = layout._load(source / WORLD / "level.dat").get("Data", {}) + if type(level.get("DataVersion")) is not nbtlib.Int or int(level["DataVersion"]) != allowed[route]: + raise AssertionError("Historical route differs from the immutable source level version") + return source, reference, path, prior, version, read_json(snapshot) + + +def run(args, *, server_type=None, stage_function=None, hook_only=False): + if type(hook_only) is not bool: + raise ValueError("Hook-only scope must be explicitly boolean") + source, reference, prior_path, prior, version, old_rows = read_source(args) + native = engine() + staging = stage_function or native.stage + target = native.read_family_target(args.profile, args.seed, args.inventory, args.inventory_sha256) + if set(target[2]) != MOD_IDS: + raise AssertionError("Migration requires exactly six frozen production jars") + java, java_version = native.java25(args.java) + root = output_path(args.output) + for protected in (source, reference, args.seed, args.profile, args.inventory, java, *(row[1] for row in target[2].values())): + require_disjoint(root, protected) + before, seed_before, reference_before = fingerprint(source), fingerprint(args.seed), fingerprint(reference) + protected = {Path(args.profile): digest(args.profile), Path(args.inventory): digest(args.inventory), + prior_path: digest(prior_path), java: digest(java), **oracle_closure()} + protected.update({Path(path): checksum for _, path, checksum in target[2].values()}) + root.mkdir(parents=True, exist_ok=False) + runtime = root / "runtime" + receipt = root / "native262-upgrade-results.json" + result = dict(status="RUNNING", scope=HOOK_SCOPE if hook_only else SCOPE, + minecraft=MINECRAFT, forge=FORGE, runtime=str(runtime), + qualificationStatus="EXPLORATORY_UNQUALIFIED", + route=args.source_route, source=str(source), sourceReceiptSha256=digest(prior_path), + profileSha256=digest(args.profile), javaSha256=digest(java), javaVersion=java_version, + playerConversionQualified=False, inputHashes={str(path): sha for path, sha in protected.items()}, phases={}) + if hook_only: + result.update(qualificationAllowed=False, operationNotRun=dict(status="NOT_RUN", + reason="Explicit non-consuming instrumented hook scope; not operating qualification")) + write_json(receipt, result) + expected = None + try: + expected = staging(runtime, *target, args.port) + copy_source(source, runtime) + fixture = read_json(reference / "migration-fixture.json") + write_json(runtime / "migration-fixture.json", fixture) + positions = {row["name"]: [row[axis] for axis in ("x", "y", "z")] for row in fixture} + saves = [] + for label in ("converted", "restarted"): + rows = with_server(runtime, java, label, lambda server: None, server_type=server_type) + phase = comparison(old_rows, rows, modern=True, source_version=version) + phase["snapshotSha256"] = digest(runtime / f"migration-{label}.json") + result["phases"][label] = phase + write_json(receipt, result) + if phase["status"] != "PASS": + raise AssertionError("Strict source witness mismatch: " + repr(phase["differences"][:5])) + phase["serialized"] = capture(runtime, label, rows) + phase["turbine"] = turbine(runtime / WORLD, positions["hydroGenerator"]) + if phase["turbine"] != prior["phases"]["restarted"]["turbine"]: + raise AssertionError("Turbine UUID, parent or position changed") + saves.append((rows, phase["serialized"]["worldGenSettings"])) + if saves[0][1] != saves[1][1] or comparison(saves[0][0], saves[1][0], modern=False)["status"] != "PASS": + raise AssertionError("Machine NBT or full worldgen settings changed on restart") + if not hook_only: + operation = bind(previous.operate, stage=staging, copy_source=copy_source, + with_server=lambda *a: with_server(*a, server_type=server_type), + verify_runtime=native.verify_runtime, load_operating_chunks=load_operating_chunks) + result["phases"]["operation"] = operation(root / "operation", runtime, target, java, args.port, positions, saves[1][0]) + result["status"] = "PASS" + except BaseException as failure: + result.update(status="FAIL", failure=repr(failure), traceback=traceback.format_exc()) + raise + finally: + try: + if expected is not None: + native.verify_runtime(runtime, target[1], expected) + result["sourceUnchanged"] = fingerprint(source) == before + result["seedUnchanged"] = fingerprint(args.seed) == seed_before + result["referenceUnchanged"] = fingerprint(reference) == reference_before + result["inputsUnchanged"] = all(digest(path) == checksum for path, checksum in protected.items()) + if not all(result[key] for key in ("sourceUnchanged", "seedUnchanged", "referenceUnchanged", "inputsUnchanged")): + raise AssertionError("Protected migration inputs changed") + except Exception as audit: + result.update(status="FAIL", auditError=repr(audit)) + write_json(receipt, result) + return int(result["status"] != "PASS") + + +def parser(): + result = argparse.ArgumentParser(description=__doc__) + for name in ("source", "profile", "seed", "inventory", "java", "output"): + result.add_argument("--" + name, type=lambda value: Path(value).resolve(), required=True) + result.add_argument("--legacy-reference", type=lambda value: Path(value).resolve()) + result.add_argument("--source-route", choices=(*LEGACY_ROUTES, *SOURCE_VERSIONS), required=True) + result.add_argument("--source-receipt-name", required=True) + for name in ("receipt-sha256", "inventory-sha256"): + result.add_argument("--" + name, required=True) + result.add_argument("--port", type=int, required=True) + return result + + +def main(): + cli = parser() + args = cli.parse_args() + if not 1024 <= args.port <= 65535 or (args.source_route in LEGACY_ROUTES and not args.legacy_reference): + cli.error("Require an unprivileged assigned port and an independent reference for raw historical routes") + return run(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native262_worldgen.py b/test-rig/migration/native262_worldgen.py new file mode 100644 index 00000000..b4b46f21 --- /dev/null +++ b/test-rig/migration/native262_worldgen.py @@ -0,0 +1,155 @@ +"""Native64 normal-world sampling with lossless separate WorldGenSettings custody.""" + +import copy +from pathlib import Path +import shutil + +from matrix import digest, write_json +from native117 import output_path +import native12111_worldgen as previous +import native262 as native +import native262_layout as layout +from native262_resources import FAMILY, arguments, bind, preflight, require + + +CAMPAIGNS = ("advantage", "legacy-import") +WORLD = previous.WORLD +RESULT = "native262-worldgen-results.json" +validate_profile = previous.validate_profile +validate_initial_hierarchy = previous.validate_initial_hierarchy +mutated_profile = previous.mutated_profile +check_files = previous.check_files +validate_sample = bind(previous.validate_sample, native=native) +configure_normal_world = previous.configure_normal_world +_campaign_spec = bind(previous.campaign_spec, native=native) + + +def campaign_spec(name): + require(name in CAMPAIGNS, "Worldgen route lacks an approved native65 provider pin: " + name) + return _campaign_spec(name) + + +def select_mods(mods, spec): + require(set(mods) == FAMILY and spec["requiredMods"] <= mods.keys(), + "Worldgen needs all six exact frozen jars without an unpinned provider") + return dict(mods) + + +def normal_world(world): + level = layout.read_level(world) + require(str(level["LevelName"]) == WORLD, "Unexpected native normal-world identity") + require("WorldGenSettings" not in level, "Historical embedded settings are not native65 authority") + saved = layout.read_worldgen(world) + data = saved["data"] + generator = data["dimensions"]["minecraft:overworld"]["generator"] + require(str(generator["type"]) == "minecraft:noise" and str(generator["settings"]) == "minecraft:overworld", + "Must sample actual normal terrain, not the flat machine fixture") + require(type(data["seed"]) is layout.nbtlib.Long, "Missing native world seed") + # Retain the complete native SavedData envelope, not a projected set of known settings. + return copy.deepcopy(saved) + + +def validate_restart(first, restart, spec): + previous.validate_restart(first, restart, spec) + for field in ("counts", "biomeViolations", "biomeBoundaryBlocks"): + require(first[field] == restart[field], "Native64 restart measurements changed; no historical exclusion is inherited") + + +class WorldgenServer(native.Server): + @staticmethod + def unexpected_line(line): + return native.Server.unexpected_line(line) or any(marker in line for marker in ( + "Worldgen sample failed", "Advantage Works command failed")) + + +def run(args): + require(type(args.sample_timeout) is int and 300 <= args.sample_timeout <= 3600, "Bound the sampling deadline") + spec = campaign_spec(args.campaign) + inputs = preflight(args, [Path(__file__), Path(previous.__file__), spec["template"], + *(source for source, _ in spec["configFixtures"])]) + mods = select_mods(inputs.mods, spec) + native.require_free_port(args.port) + if args.preflight_only: + return dict(status="READY_NOT_RUN", qualificationAllowed=False, campaign=args.campaign, **inputs.receipt()) + runtime = output_path(args.output) + expected = native.stage(runtime, *inputs.target, args.port) + shutil.copy2(Path(__file__), runtime / "native262-worldgen-source.py") + configure_normal_world(runtime, spec, args.port) + for source, destination in spec["configFixtures"]: + path = runtime / destination + path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, path) + report = dict(status="RUNNING", campaign=args.campaign, **inputs.receipt(), phases={}) + server, initial, saved_settings = None, None, None + pids = [] + try: + for phase in ("initial", "restart"): + native.verify_runtime(runtime, inputs.target[1], expected) + server = WorldgenServer(runtime, inputs.java, native.LAUNCHER, "worldgen-" + phase) + pids.append(server.process.pid) + require(len(pids) == len(set(pids)), "Worldgen restart reused a process") + server.send("gamerule minecraft:spawn_mobs false") + server.send("advworks sample-worldgen " + str(spec["worldgenSampleRadius"])) + server.wait("Worldgen sample wrote ", args.sample_timeout) + sample_file = runtime / "advantage-works-results/worldgen-sample.json" + sample = native.read_json(sample_file) + shutil.copy2(sample_file, runtime / f"worldgen-{phase}.json") + validate_sample(sample, spec, mods) + server.send("save-all flush") + server.wait("Saved the game", 30) + server.close() + server = None + world = runtime / WORLD + settings = normal_world(world) + raw = layout.shared_file(world, "minecraft:world_gen_settings") + for source, name in ((world / "level.dat", f"level-{phase}.dat"), + (raw, f"world-gen-settings-{phase}.dat")): + shutil.copy2(source, runtime / name) + profile_path = world / "serverconfig/orespawn-worldgen.json" + profile = native.read_json(profile_path) + write_json(runtime / f"orespawn-profile-{phase}.json", profile) + prefix = "first" if phase == "initial" else "restart" + assertions = validate_profile(profile, spec[prefix + "ProfileAssertions"]) + check_files(runtime, spec[prefix + "FileAssertions"]) + for source, destination in spec["configFixtures"]: + require(digest(runtime / destination) == inputs.hashes[str(source)], "Legacy import rewrote its source") + if initial is None: + validate_initial_hierarchy(profile, mods, legacy=args.campaign == "legacy-import") + initial, saved_settings = sample, settings + if spec["profileMutations"]: + write_json(profile_path, mutated_profile(profile, spec["profileMutations"])) + else: + validate_restart(initial, sample, spec) + require(settings == saved_settings, "Native worldgen SavedData changed across restart") + report["phases"][phase] = dict(status="PASS", pid=pids[-1], assertions=assertions, + sampleSha256=digest(runtime / f"worldgen-{phase}.json"), + stoppedLevelSha256=digest(runtime / f"level-{phase}.dat"), + stoppedSettingsSha256=digest(runtime / f"world-gen-settings-{phase}.dat")) + write_json(runtime / RESULT, report) + report["status"] = "PASS" + except BaseException as failure: + report.update(status="FAIL", error=repr(failure)) + raise + finally: + try: + if server is not None: + server.close() + native.verify_runtime(runtime, inputs.target[1], expected) + report["sourceUnchanged"] = inputs.unchanged() + except BaseException as failure: + report.update(status="FAIL", custodyError=repr(failure)) + raise + finally: + write_json(runtime / RESULT, report) + return report + + +def main(): + parser = arguments(__doc__) + parser.add_argument("--campaign", choices=CAMPAIGNS, required=True) + parser.add_argument("--sample-timeout", type=int, default=1800) + print(run(parser.parse_args())["status"], flush=True) + + +if __name__ == "__main__": + main() diff --git a/test-rig/migration/test_native262.py b/test-rig/migration/test_native262.py new file mode 100644 index 00000000..636b3dcd --- /dev/null +++ b/test-rig/migration/test_native262.py @@ -0,0 +1,223 @@ +"""Bounded process/custody negatives plus the actual selected SDK logging source.""" + +import json +import os +from pathlib import Path +import socket +import tempfile +import unittest +from unittest.mock import patch +from zipfile import ZipFile + +import native262 as native + + +SDK = Path(os.environ.get("NATIVE262_TEST_SDK", + "D:/PowerAdvantage/_verification/base-262-port-20261112/server-code-seed-01")) +LOGGING = SDK / f"libraries/net/minecraftforge/fmlloader/{native.FORGE_VERSION}/fmlloader-{native.FORGE_VERSION}.jar" + + +class Native262GuardsTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name).resolve() + + def test_all_inherited_option_channels_fail(self): + for key in ("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS", "CLASSPATH"): + with self.subTest(key=key), patch.dict(os.environ, {key: "injected"}, clear=True): + with self.assertRaises(ValueError): + native.native_launch_environment() + + def test_empty_environment_is_not_given_an_agent(self): + with patch.dict(os.environ, {}, clear=True): + self.assertEqual({}, native.native_launch_environment()) + + def test_wrong_java_hash_fails_before_execution(self): + executable = self.root / "java.exe" + executable.write_bytes(b"wrong Java") + with patch.object(native.subprocess, "run") as run: + with self.assertRaises(AssertionError): + native.java25(executable) + run.assert_not_called() + + def test_hash_pin_is_required_and_not_coerced(self): + file = self.root / "input" + file.write_bytes(b"exact") + for sha in (None, "", "pending", "0" * 63): + with self.subTest(sha=sha), self.assertRaises(ValueError): + native.validate_hash(file, sha) + with self.assertRaises(AssertionError): + native.validate_hash(file, "0" * 64) + native.validate_hash(file, native.digest(file)) + + def test_duplicate_document_fields_fail_closed(self): + path = self.root / "profile.json" + path.write_text('{"mods":[],"mods":["hidden"]}') + with self.assertRaises(ValueError): + native.read_json(path) + + def test_missing_family_mod_does_not_create_runtime(self): + runtime = self.root / "not-created" + for missing in native.VERSIONS: + mods = {key: (version, self.root / (key + ".jar"), "0" * 64) + for key, version in native.VERSIONS.items() if key != missing} + with self.subTest(missing=missing), patch.dict(os.environ, {}, clear=True), self.assertRaises(ValueError): + native.stage(runtime, self.root, {}, mods, 26660) + self.assertFalse(runtime.exists()) + + def test_occupied_port_does_not_get_reclaimed(self): + with socket.socket() as occupied: + occupied.bind(("127.0.0.1", 0)) + occupied.listen() + with self.assertRaises(ValueError): + native.require_free_port(occupied.getsockname()[1]) + self.assertGreater(occupied.fileno(), -1) + + def test_invalid_ports_fail_without_coercion(self): + for port in (True, "26660", 80, 65536, -1): + with self.subTest(port=port), self.assertRaises(ValueError): + native.require_free_port(port) + + def settings(self, value): + (self.root / "server.properties").write_text(value) + + def test_pause_guard_is_explicit(self): + for pause in ("", "pause-when-empty-seconds=0\n", "pause-when-empty-seconds=60\n"): + self.settings("server-ip=127.0.0.1\nserver-port=26660\n" + pause) + with self.subTest(pause=pause), self.assertRaises(ValueError): + native.require_launch_settings(self.root) + + def test_duplicate_settings_fail_before_port_probe(self): + self.settings("server-port=26660\nserver-port=26661\n") + with patch.object(native, "require_free_port") as probe: + with self.assertRaises(ValueError): + native.require_launch_settings(self.root) + probe.assert_not_called() + + def test_good_settings_check_actual_selected_port(self): + self.settings("server-ip=127.0.0.1\nserver-port=26660\npause-when-empty-seconds=-1\n") + with patch.object(native, "require_free_port") as probe: + native.require_launch_settings(self.root) + probe.assert_called_once_with(26660) + + def manifest(self, **changes): + document = dict(format=1, purpose="NATIVE65_MIXIN_EXPORT_DIAGNOSTIC", + runtime=str(self.root), label="order-a", properties={"mixin.debug.export": True}) + document.update(changes) + path = self.root / "diagnostic.json" + path.write_text(json.dumps(document)) + return path + + def test_diagnostic_manifest_is_explicit_and_nonqualifying(self): + path = self.manifest() + with patch.dict(os.environ, {}, clear=True): + environment = native.native_launch_environment(self.root, "order-a", diagnostic_environment=path, + diagnostic_environment_sha256=native.digest(path)) + self.assertEqual(environment, {"JAVA_TOOL_OPTIONS": "-Dmixin.debug.export=true"}) + receipt = native.read_json(self.root / "native65-launch-environment-order-a.json") + self.assertFalse(receipt["qualificationAllowed"]) + self.assertEqual("DIAGNOSTIC_NOT_QUALIFIED", receipt["status"]) + with patch.dict(os.environ, {}, clear=True), self.assertRaises(FileExistsError): + native.native_launch_environment(self.root, "order-a", diagnostic_environment=path, + diagnostic_environment_sha256=native.digest(path)) + + def test_old_purpose_wrong_binding_and_injected_agent_rejected(self): + for change in (dict(purpose="NATIVE61_MIXIN_EXPORT_DIAGNOSTIC"), dict(label="another"), + dict(properties={"mixin.debug.export": True, "javaagent": "hidden"})): + path = self.manifest(**change) + with self.subTest(change=change), patch.dict(os.environ, {}, clear=True), self.assertRaises(ValueError): + native.native_launch_environment(self.root, "order-a", diagnostic_environment=path, + diagnostic_environment_sha256=native.digest(path)) + self.assertFalse((self.root / "native65-launch-environment-order-a.json").exists()) + + def test_diagnostic_tamper_is_preserved_not_resealed(self): + path = self.manifest() + pin = native.digest(path) + path.write_text(path.read_text() + " ") + with patch.dict(os.environ, {}, clear=True), self.assertRaises(AssertionError): + native.native_launch_environment(self.root, "order-a", diagnostic_environment=path, + diagnostic_environment_sha256=pin) + + @staticmethod + def complete_log(): + return "\n".join((f"Forge mod loading, version {native.FORGE}, for MC {native.MINECRAFT} with MCP {native.MCP}", + f"Starting minecraft server version {native.MINECRAFT}", "Done (0.1s)!", + "Stopping server", "All dimensions are saved")) + + def test_real_boot_requires_all_witnesses_in_order(self): + text = self.complete_log() + native.require_complete_boot(text, stop_sent=True, returncode=0) + for line in text.splitlines(): + with self.subTest(line=line), self.assertRaises(AssertionError): + native.require_complete_boot(text.replace(line, ""), stop_sent=True, returncode=0) + with self.assertRaises(AssertionError): + native.require_complete_boot("\n".join(reversed(text.splitlines())), stop_sent=True, returncode=0) + + def test_crash_exit_zero_and_external_termination_are_not_cold_stop(self): + with self.assertRaises(AssertionError): + native.require_complete_boot("Crash report saved", stop_sent=False, returncode=0) + with self.assertRaises(AssertionError): + native.require_complete_boot(self.complete_log(), stop_sent=False, returncode=0) + with self.assertRaises(AssertionError): + native.require_complete_boot(self.complete_log(), stop_sent=True, returncode=1) + + def test_earlier_autosave_and_explicit_flush_cannot_shadow_final_save(self): + text = self.complete_log().replace("Done (", "All dimensions are saved\nDone (") + text = text.replace("Stopping server", "All dimensions are saved\nStopping server") + native.require_complete_boot(text, stop_sent=True, returncode=0) + without_final = text.rsplit("All dimensions are saved", 1)[0] + with self.assertRaisesRegex(AssertionError, "All dimensions are saved"): + native.require_complete_boot(without_final, stop_sent=True, returncode=0) + + def test_stop_before_done_or_save_before_stop_cannot_qualify(self): + misordered = self.complete_log().replace("Done (0.1s)!\nStopping server", "Stopping server\nDone (0.1s)!") + with self.assertRaisesRegex(AssertionError, "Stopping server"): + native.require_complete_boot(misordered, stop_sent=True, returncode=0) + no_final_flush = self.complete_log().replace("Stopping server\nAll dimensions are saved", + "All dimensions are saved\nStopping server") + with self.assertRaisesRegex(AssertionError, "All dimensions are saved"): + native.require_complete_boot(no_final_flush, stop_sent=True, returncode=0) + + def test_formatter_faults_and_debug_runtime_errors_stay_failures(self): + for text in ("Unable to write to stream DebugFile", "java.lang.ExceptionInInitializerError", + "[now] [main/DEBUG] [mod/]: java.lang.IllegalStateException: actual defect", + "[now] [main/ERROR] [io.netty.channel.epoll.Epoll/]: Epoll support is not available", + "Server empty for 60 seconds, pausing"): + with self.subTest(text=text): + self.assertTrue(native.log_findings(text)[0]) + + +class Native262SelectedSdkTest(unittest.TestCase): + def test_selected_sdk_identity_and_compact_headers(self): + inventory = native.inspect_seed(SDK) + self.assertEqual("26.2-65.1.0", inventory["forge"]) + self.assertEqual(96, len(inventory["files"])) + self.assertEqual(native.SHIM_SHA, inventory["files"][native.SHIM]["sha256"]) + + def test_actual_logging_adapter_changes_only_two_renderers(self): + native.validate_hash(LOGGING, native.LOGGING_JAR_SHA) + with ZipFile(LOGGING) as jar: + original = jar.read("log4j2.xml") + changed = native.plain_logging_bytes(original) + self.assertEqual(original.replace(b"%xEx", b"%ex"), changed) + self.assertIn(b'forge.logging.debugFile.level:-debug', changed) + self.assertIn(b'', changed) + self.assertEqual(2, original.count(b"%xEx")) + with self.assertRaises(AssertionError): + native.plain_logging_bytes(original.replace(b'level="all"', b'level="info"')) + + def test_logging_custody_rejects_existing_configuration_tamper(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + native.logging_configuration(root, LOGGING) + native.logging_configuration(root, LOGGING) + config = root / "forge65-log4j2-plain-stacktraces.xml" + config.write_bytes(config.read_bytes() + b"") + with self.assertRaises(AssertionError): + native.logging_configuration(root, LOGGING) + self.assertTrue(config.read_bytes().endswith(b"")) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_actor_transformer_order.py b/test-rig/migration/test_native262_actor_transformer_order.py new file mode 100644 index 00000000..371cac75 --- /dev/null +++ b/test-rig/migration/test_native262_actor_transformer_order.py @@ -0,0 +1,274 @@ +import base64 +import copy +import gzip +import io +import json +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +import nbtlib as nbt + +import native262_actor_transformer_order as target + + +def expected_state(): + stack = dict(id="basesteam:musket", count=1, components={ + "minecraft:damage": 5, "minecraft:enchantments": dict.fromkeys(target.ENCHANTMENTS.values(), 1), + "minecraft:custom_data": {"Nested": {"OwnerNote": "keep-this-verbatim"}, + "MigrationWitness": "steamadvantage:musket"}}) + return dict(entities={"cart": {"Items": [dict(Slot=1, **copy.deepcopy(stack))]}, + "dropped-musket": {"Item": stack}}) + + +def source_level(): + entries = [nbt.Compound(K=nbt.String(value.replace("basesteam:", "steamadvantage:")), V=nbt.Int(key)) + for key, value in target.ENCHANTMENTS.items()] + return nbt.File({"Data": nbt.Compound(DataVersion=nbt.Int(512)), + "FML": nbt.Compound(Registries=nbt.Compound({ + "minecraft:enchantments": nbt.Compound(ids=nbt.List[nbt.Compound](entries))}))}) + + +def source_entity(witness): + stack = nbt.Compound(id=nbt.String("steamadvantage:musket"), Count=nbt.Byte(1), + tag=nbt.Compound(ench=nbt.List[nbt.Compound]([ + nbt.Compound(id=nbt.Short(key), lvl=nbt.Short(1)) for key in (11, 13, 10, 12)]))) + if witness == "cart": + stack["Slot"] = nbt.Byte(1) + return nbt.File({"Items": nbt.List[nbt.Compound]([stack])}) + return nbt.File({"Item": stack}) + + +def row(witness, value=None): + stream = io.BytesIO() + (value if value is not None else source_entity(witness)).write(stream) + return dict(witness=witness, nbtBase64=base64.b64encode(gzip.compress(stream.getvalue())).decode("ascii")) + + +def source_record(): + return dict(entities=[row(witness) for witness in target.WITNESSES]) + + +def context(): + return target.source_context(source_level(), source_record(), expected_state()) + + +class SourceContextTest(unittest.TestCase): + def test_original_numeric_ids_and_saved_names_define_expected_not_target(self): + result = context() + self.assertFalse(result["targetObservationsUsed"]) + self.assertEqual([11, 13, 10, 12], result["witnesses"]["cart"]["numericIds"]) + self.assertEqual(expected_state()["entities"]["dropped-musket"]["Item"], + result["witnesses"]["dropped-musket"]["expectedStack"]) + + def test_native_or_wrongly_typed_source_version_rejects(self): + for version in (nbt.Int(4903), nbt.Short(512), nbt.String("512")): + with self.subTest(version=version): + level = source_level() + level["Data"]["DataVersion"] = version + with self.assertRaisesRegex(AssertionError, "raw original110"): + target.source_context(level, source_record(), expected_state()) + + def test_saved_registry_missing_collision_duplicate_or_typed_id_rejects(self): + for mutation in ("missing", "collision", "duplicate", "type"): + with self.subTest(mutation=mutation): + level = source_level() + entries = level["FML"]["Registries"]["minecraft:enchantments"]["ids"] + if mutation == "missing": entries.pop() + elif mutation == "collision": entries[0]["K"] = nbt.String("minecraft:binding_curse") + elif mutation == "duplicate": entries.append(copy.deepcopy(entries[0])) + else: entries[0]["V"] = nbt.Short(10) + with self.assertRaises(AssertionError): + target.source_context(level, source_record(), expected_state()) + + def test_raw_enchantment_mutations_reject_for_both_entity_witnesses(self): + for witness in target.WITNESSES: + for mutation in ("string-id", "int-id", "level", "duplicate", "missing"): + with self.subTest(witness=witness, mutation=mutation): + value = source_entity(witness) + entries = target.item(value, witness, native_format=False)["tag"]["ench"] + if mutation == "string-id": entries[0]["id"] = nbt.String("basesteam:powderless") + elif mutation == "int-id": entries[0]["id"] = nbt.Int(11) + elif mutation == "level": entries[0]["lvl"] = nbt.Short(2) + elif mutation == "duplicate": entries[0]["id"] = nbt.Short(13) + else: entries.pop() + record = source_record() + record["entities"] = [row(witness, value) if item["witness"] == witness else item + for item in record["entities"]] + with self.assertRaises(AssertionError): + target.source_context(source_level(), record, expected_state()) + + def test_missing_or_duplicate_source_entities_reject(self): + for values in ([], [row("cart"), row("cart"), row("dropped-musket")]): + with self.assertRaisesRegex(AssertionError, "source witness"): + target.source_context(source_level(), dict(entities=values), expected_state()) + + def test_historical_oracle_cannot_derive_curse_substitution_from_target(self): + changed = expected_state() + changed["entities"]["cart"]["Items"][0]["components"]["minecraft:enchantments"] = {"minecraft:binding_curse": 1} + with self.assertRaisesRegex(AssertionError, "Historical oracle"): + target.source_context(source_level(), source_record(), changed) + + def test_preflight_requires_receipt_and_entire_original_fingerprint_before_output(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source = root / "original" + (source / target.WORLD).mkdir(parents=True) + source_level().save(source / target.WORLD / "level.dat", gzipped=True) + target.write_json(source / "actors-source.json", source_record()) + receipt = root / "approved.json" + target.write_json(receipt, dict(source=str(source), sourceFingerprint=target.fingerprint(source))) + args = SimpleNamespace(fixture=source, fixture_receipt=receipt, + fixture_receipt_sha256=target.digest(receipt), output=root / "never-created") + with patch.object(target.actors, "baseline", return_value=(expected_state(), "source")): + self.assertEqual(512, target.preflight(args)["context"]["sourceDataVersion"]) + (source / "unapproved.txt").write_text("drift", encoding="ascii") + with self.assertRaisesRegex(AssertionError, "approved independent receipt"): + target.preflight(args) + self.assertFalse(args.output.exists()) + args.fixture_receipt_sha256 = "0" * 64 + with self.assertRaises(AssertionError): target.preflight(args) + + +class NativeWitnessTest(unittest.TestCase): + def test_all_fourteen_derived_controls_reject_without_altering_baseline(self): + actual = expected_state() + before = copy.deepcopy(actual) + result = target.negative_controls(context(), actual) + self.assertEqual(before, actual) + self.assertEqual(14, list(result["controls"].values()).count("REJECTED")) + self.assertIs(False, result["sourceOrWorldFilesMutated"]) + self.assertIs(False, result["qualificationAllowed"]) + + def test_false_native_types_cannot_equal_expected_numbers(self): + for witness in target.WITNESSES: + for location, value in (("count", True), ("count", 1.0), ("wear", 5.0), + ("level", True), ("level", "1"), ("slot", True)): + if location == "slot" and witness != "cart": continue + with self.subTest(witness=witness, location=location, value=value): + actual = expected_state() + stack = target.item(actual["entities"][witness], witness, native_format=True) + if location == "count": stack["count"] = value + elif location == "wear": stack["components"]["minecraft:damage"] = value + elif location == "slot": stack["Slot"] = value + else: stack["components"]["minecraft:enchantments"]["basesteam:recoil"] = value + with self.assertRaises(AssertionError): target.assert_witnesses(context(), actual) + + def test_missing_duplicate_slot_and_renamed_native_musket_reject(self): + for mutation in ("missing", "duplicate", "slot", "namespace"): + actual = expected_state() + values = actual["entities"]["cart"]["Items"] + if mutation == "missing": values.clear() + elif mutation == "duplicate": values.append(copy.deepcopy(values[0])) + elif mutation == "slot": values[0]["Slot"] = 2 + else: values[0]["id"] = "steamadvantage:musket" + with self.assertRaises(AssertionError): target.assert_witnesses(context(), actual) + + def test_complete_unknown_custom_payload_is_part_of_exact_oracle(self): + actual = expected_state() + del actual["entities"]["dropped-musket"]["Item"]["components"]["minecraft:custom_data"]["Nested"] + with self.assertRaises(AssertionError): target.assert_witnesses(context(), actual) + + def test_stopped_audit_cannot_accept_empty_failed_or_boolean_count(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "receipt.json" + for status, count in (("FAIL", 2), ("PASS", 0), ("PASS", 1), ("PASS", True), ("PASS", "2")): + target.write_json(path, dict(status=status, matchedEntities=count)) + with self.assertRaises(AssertionError): target.raw_receipt(path) + target.write_json(path, dict(status="PASS", matchedEntities=2)) + self.assertEqual(target.digest(path), target.raw_receipt(path)["sha256"]) + + def test_hook_only_is_not_actor_proof(self): + with self.assertRaisesRegex(ValueError, "all three"): + target.run_actor_campaign(SimpleNamespace(), server_type=object, stage_function=None, hook_only=True) + + def test_actor_facade_binding_does_not_mutate_sealed_order_globals(self): + original, modes = target.order.upgrade, target.order.MODES + facade = SimpleNamespace() + campaign = target.bind(target.order.run, upgrade=facade, MODES=tuple(reversed(modes))) + self.assertIs(facade, campaign.__globals__["upgrade"]) + self.assertIs(original, target.order.upgrade) + self.assertEqual(modes, target.order.MODES) + self.assertEqual(21, len(target.order.GUARD_CONTROLS) + len(target.order.WOVEN_CONTROLS)) + self.assertEqual(("converted", "adapter", "restarted", "runtime", "runtime-restart"), target.PHASES) + self.assertEqual(("conversion", "restart", "runtime-restart"), target.STOP_PHASES) + + +class CampaignContractTest(unittest.TestCase): + def exercise(self, root, mutation=None): + source = root / "source" + source.mkdir() + profile = root / "profile.json" + profile.write_text("{}", encoding="ascii") + args = SimpleNamespace(fixture=source, profile=profile, seed=root / "seed", inventory=root / "inventory", + inventory_sha256="0" * 64, java=root / "java.exe", output=root / "campaign", port=26560) + approved = dict(context=context(), sourceFingerprint=target.fingerprint(source)) + + def route(actual_source, runtime, actual_target, values, **hooks): + self.assertEqual(source, actual_source) + self.assertEqual("actors", values.kind) + self.assertEqual(args.profile, values.target_manifest) + self.assertEqual("26.2", hooks["minecraft"]) + self.assertIsNotNone(hooks["stop_audit"]) + runtime.mkdir() + if mutation == "stop": raise RuntimeError("owned stop/raw audit failure") + for label in target.PHASES: + actual = expected_state() + if mutation == "curse" and label == "converted": + actual["entities"]["cart"]["Items"][0]["components"]["minecraft:enchantments"] = {"minecraft:binding_curse": 1} + if mutation != "missing-phase" or label != "runtime-restart": + target.write_json(runtime / ("semantic-" + label + ".json"), actual) + for label in target.STOP_PHASES: + path = runtime / "stopped-entity-evidence" / label / "receipt.json" + path.parent.mkdir(parents=True) + target.write_json(path, dict(status="FAIL" if mutation == "raw" else "PASS", matchedEntities=2)) + return {} if mutation == "empty-checks" else {"conversion": dict(status="PASS"), "runtime-restart": dict(status="PASS")} + + with patch.object(target, "preflight", return_value=approved), \ + patch.object(target.native, "read_family_target", return_value=(args.seed, {}, {})), \ + patch.object(target.native, "java25", return_value=(args.java, "25")), \ + patch.object(target.actors, "run_route", side_effect=route): + if mutation: + with self.assertRaises((AssertionError, RuntimeError)): + target.run_actor_campaign(args, server_type=object, stage_function=None) + else: + self.assertEqual(0, target.run_actor_campaign(args, server_type=object, stage_function=None)) + return target.upgrade.read_json(args.output / "native262-actor-order-campaign.json") + + def test_complete_actor_campaign_requires_all_phases_raw_audits_and_controls(self): + with tempfile.TemporaryDirectory() as directory: + result = self.exercise(Path(directory)) + self.assertEqual("PASS", result["status"]) + self.assertEqual(set(target.PHASES), set(result["witnessChecks"])) + self.assertEqual(set(target.STOP_PHASES), set(result["rawAudits"])) + self.assertEqual(14, list(result["entityControls"]["controls"].values()).count("REJECTED")) + self.assertIs(False, result["qualificationAllowed"]) + modes = dict.fromkeys(target.order.MODES, result) + self.assertTrue(target.all_orders_proven(dict(status="PASS"), modes)) + for mutation in ("missing-mode", "failed-mode", "missing-phase", "failed-raw", "missing-control", "custody"): + changed = copy.deepcopy(modes) + first = changed[target.order.MODES[0]] + if mutation == "missing-mode": del changed[target.order.MODES[0]] + elif mutation == "failed-mode": first["status"] = "FAIL" + elif mutation == "missing-phase": del first["witnessChecks"]["converted"] + elif mutation == "failed-raw": first["rawAudits"]["conversion"]["status"] = "FAIL" + elif mutation == "missing-control": del first["entityControls"]["controls"]["cart/curse"] + else: first["sourceUnchanged"] = False + self.assertFalse(target.all_orders_proven(dict(status="PASS"), changed), mutation) + self.assertFalse(target.all_orders_proven(dict(status="FAIL"), modes)) + + def test_curse_missing_phase_empty_checks_and_failed_stop_are_retained_failures(self): + for mutation in ("curse", "missing-phase", "empty-checks", "raw", "stop"): + with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as directory: + result = self.exercise(Path(directory), mutation) + self.assertEqual("FAIL", result["status"]) + self.assertTrue(result["sourceUnchanged"]) + self.assertEqual("NOT_RUN", result["entityControls"]["status"]) + if mutation == "stop": self.assertIn("stop/raw audit failure", result["error"]) + if mutation == "curse": self.assertEqual("FAIL", result["witnessChecks"]["converted"]["status"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_actors.py b/test-rig/migration/test_native262_actors.py new file mode 100644 index 00000000..334085e6 --- /dev/null +++ b/test-rig/migration/test_native262_actors.py @@ -0,0 +1,146 @@ +"""Native target layout and unchanged historical actor assertion controls.""" +import copy +from pathlib import Path +import tempfile +import unittest +import sys +from unittest.mock import patch + +import nbtlib +import native262_actors as target +import native262_layout as layout +from native117_actor_io import bind +import test_native12111_actor_network_adapters as previous_adapters +import test_native12111_actor_fixtures as previous_fixtures +import test_native12111_actor_comparison as previous_comparison +import test_native12111_stopped_actor_reference as previous_reference +import native262_actor_comparison as comparison +import native262_actor_fixtures as fixtures +import native262_stopped_actor_reference as reference +from test_native117_entity_storage import entity, entry, put, root as entity_root + + +def retained_controls(source, **bindings): + """Run every unchanged assertion against the new adapter, not its old module.""" + values = { + name: bind(value, **bindings) if callable(value) and hasattr(value, '__code__') else value + for name, value in vars(source).items() if name not in ('__dict__', '__weakref__')} + values['__module__'] = __name__ + return type('Native262' + source.__name__, (unittest.TestCase,), values) + + +Native262FixtureControls = retained_controls(previous_fixtures.NativeActorFixtureControls, + legal_ender_batteries=fixtures.legal_ender_batteries, source_components=target.source_components) +Native262ComparisonControls = retained_controls(previous_comparison.NativeActorComparisonTest, diagnosis=comparison) +Native262StoppedReferenceControls = retained_controls(previous_reference.SourceStoppedReferenceTest, reference=reference) +Native262PayloadControls = retained_controls(previous_adapters.ActorAdapters, actor_driver=target) + + +class Native262Actors(unittest.TestCase): + def test_diagnostic_profile_cannot_be_run_without_explicit_provisional_scope(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + argv = ['native262_actors.py', '--fixture', str(root), '--fixture-receipt', str(root / 'receipt.json'), + '--fixture-receipt-sha256', '0' * 64, '--profile', str(root / 'profile.json'), + '--seed', str(root / 'seed'), '--inventory', str(root / 'inventory.json'), + '--inventory-sha256', '0' * 64, '--java', str(root / 'java'), + '--output', str(root / 'output'), '--kind', 'actors', '--port', '28690'] + with patch.object(sys, 'argv', argv), patch.object(target, 'output_path', return_value=root / 'output'), \ + patch.object(target.native, 'read_json', return_value={'diagnosticScope': 'context-only'}), \ + patch.object(target.native, 'read_family_target') as stage: + with self.assertRaisesRegex(ValueError, 'explicit --provisional'): + target.main() + stage.assert_not_called() + + def test_explicit_dimension_paths_do_not_use_legacy_basenames(self): + for key in layout.VANILLA_DIMENSIONS: + self.assertEqual(layout.dimension_directory(Path('world'), key), + Path('world') / target.DIMENSION_PATHS[key][1]) + + def test_player_equipment_and_respawn_keep_exact_native_maps(self): + state = {'file': {'Inventory': [{'Slot': -106, 'id': 'basesteam:musket', 'count': 1, + 'components': {'minecraft:damage': 9}}], 'SpawnX': 1, 'SpawnY': 80, + 'SpawnZ': 3, 'SpawnDimension': -1, 'SpawnAngle': 27.0}, 'entities': {}} + result = target.expected_state(state, 'dimensions') + self.assertEqual([], result['file']['Inventory']) + self.assertEqual(9, result['file']['equipment']['offhand']['components']['minecraft:damage']) + self.assertEqual('minecraft:the_nether', result['file']['respawn']['dimension']) + actual = copy.deepcopy(result) + actual['file']['equipment']['offhand']['components']['minecraft:damage'] = 8 + self.assertEqual('FAIL', target.verdict(target.actors.compare, result, actual)['status']) + + def test_old_dimension_directory_and_wrong_snapshot_dimension_reject(self): + with tempfile.TemporaryDirectory() as root: + root = Path(root) + old = root / 'DIM-1' + old.mkdir() + with self.assertRaisesRegex(ValueError, 'native dimension'): + target.audit_entity_storage({}, old, root / 'evidence', dimension='minecraft:the_nether') + + def test_native_helper_bytes_are_exact_and_old_contract_rejects(self): + root = Path(__file__).resolve().parents[1] / 'src/main/resources/assets/advantageworkstest' + document = dict(helperPolicy='native262-leash-knot-nosave-v1', + helperContractSha256=target.HELPER_CONTRACT_SHA, dimensions={}, + entityCount=0, nonpersistentHelperCount=0, observedEntityCount=0) + self.assertEqual('NOT_REQUIRED', target.validate_helpers(document, (root / 'native262-helper-contract.json').read_bytes())['status']) + with self.assertRaises(AssertionError): + target.validate_helpers(document, (root / 'native12111-helper-contract.json').read_bytes()) + + def test_native_entity_audit_keeps_unknown_nbt_and_rejects_payload_loss(self): + with tempfile.TemporaryDirectory() as directory: + base = Path(directory) + world = layout.dimension_directory(base / 'world', 'minecraft:the_nether') + actor = entity() + chunk = entity_root((0, 0), [actor]) + chunk['DataVersion'] = nbtlib.Int(4903) + path, _ = put(world, (0, 0), value=chunk) + before = path.read_bytes() + record = dict(entities=[entry(actor)], worldDimensionKey='minecraft:the_nether', dimension=-1) + receipt = target.audit_entity_storage(record, world, base / 'positive') + self.assertEqual(1, receipt['matchedEntities']) + self.assertEqual(-1, receipt['dimension']) + damaged = copy.deepcopy(actor) + damaged['Items'][0]['tag']['Private'] = nbtlib.String('lost') + with self.assertRaisesRegex(AssertionError, 'Complete saved entity NBT'): + target.audit_entity_storage(dict(record, entities=[entry(damaged)]), world, base / 'negative') + self.assertEqual(before, path.read_bytes()) + + def test_old_wolf_proof_cannot_relabel_native26_failure(self): + old = previous_adapters.NativeDifferenceAssessment() + old.setUp() + strict = target.verdict(target.dimensions.verdict, old.before, old.after) + with patch.object(target.native, 'validate_hash'), patch.object(target.native, 'read_json', return_value={'noPower': True}): + with self.assertRaisesRegex(ValueError, 'Historical wolf controls'): + target.assess_native(old.before, old.after, strict, Path('old-proof'), 'strict', True) + + def test_native_wolf_assessment_keeps_raw_failure_and_rejects_unrelated_changes(self): + old = previous_adapters.NativeDifferenceAssessment() + old.setUp() + runtime = dict(minecraftVersion='26.2', targetDataVersion=4903, + inheritedJvmEnvironmentAbsent=True, jvmArguments=['-XX:ActiveProcessorCount=4'], + javaSha256=target.native.JAVA_SHA, mainClass='NativeWolfPlatformControl', + classpath=[dict(sha256='7C324FAF36362EDB1E1879EA11536CEA7F6E58BE3A0E783C13607BE7D7364DD6')]) + + def evidence(path): + if path.name == 'control-runtime.json': + return runtime + return dict(sourceDataVersion=int(path.stem.split('-')[1]), targetDataVersion=4903, + noPower=True, uuidPreserved=True, ownerPreserved=True, + sourceHealth=8, savedHealth=16, sourceMaxHealth=20, savedMaxHealth=40) + + strict = target.verdict(target.dimensions.verdict, old.before, old.after) + with patch.object(target.native, 'validate_hash'), patch.object(target.native, 'read_json', side_effect=evidence): + result = target.assess_native(old.before, old.after, strict, Path('proof'), 'strict', True) + self.assertEqual('PASS_WITH_NATIVE_DIFFERENCE', result['status']) + self.assertEqual('FAIL', result['rawStrictResult']['status']) + changed = copy.deepcopy(old.after) + changed['player']['Inventory'][0]['Count'] = 16 + bad = target.verdict(target.dimensions.verdict, old.before, changed) + self.assertEqual('FAIL', target.assess_native(old.before, changed, bad, Path('proof'), 'strict', True)['status']) + runtime['inheritedJvmEnvironmentAbsent'] = False + with self.assertRaisesRegex(ValueError, 'Historical wolf controls'): + target.assess_native(old.before, old.after, strict, Path('proof'), 'strict', True) + + +if __name__ == '__main__': + unittest.main() diff --git a/test-rig/migration/test_native262_broad.py b/test-rig/migration/test_native262_broad.py new file mode 100644 index 00000000..2bf34bcf --- /dev/null +++ b/test-rig/migration/test_native262_broad.py @@ -0,0 +1,172 @@ +"""Adapter rejection and orchestration controls; these tests launch no Minecraft.""" + +from contextlib import redirect_stderr, redirect_stdout +import io +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +import native262_broad as broad +from test_broad_campaign_status import passed + + +class Native262BroadTest(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory(prefix="native26-broad-", dir=broad.native.RIG / "build") + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name).resolve() + self.output = self.root / "result" + self.seed = self.root / "seed" + self.seed.mkdir() + (self.seed / "protected").write_bytes(b"untouched seed") + self.profile = self.root / "profile.json" + self.inventory = self.root / "inventory.json" + self.java = self.root / "java.exe" + for path in (self.profile, self.inventory, self.java): + path.write_bytes(b"unit test only, never launched") + self.mods = {} + for name in broad.native.VERSIONS: + path = self.root / (name + ".jar") + path.write_bytes(name.encode("ascii")) + self.mods[name] = ("unit.2601021", path, broad.digest(path)) + + def arguments(self): + return ["--profile", str(self.profile), "--profile-sha256", broad.digest(self.profile), + "--seed", str(self.seed), "--inventory", str(self.inventory), + "--inventory-sha256", broad.digest(self.inventory), "--java", str(self.java), + "--output", str(self.output), "--port", "26600"] + + def exercise(self, *, failed=False, changed_seed=False, changed_input=False, custody_failed=False): + def launch(*args, **kwargs): + self.assertEqual(args[2:5], (self.output, "packaged-current", 26600)) + self.assertEqual(21, len(args[5])) + self.assertEqual(set(self.mods), set(kwargs["native118_target"][2])) + self.assertIs(broad.Adapter, kwargs["native_adapter"]) + if changed_seed: + (self.seed / "protected").write_bytes(b"changed") + if changed_input: + self.profile.write_bytes(b"changed") + return dict(status="FAIL", profile="packaged-current", error="finite assertion failed") if failed else passed("packaged-current") + with patch.object(broad.native, "read_family_target", return_value=(self.seed, {}, self.mods)), \ + patch.object(broad.native, "java25", return_value=(self.java, "Java25 unit fixture")), \ + patch.object(broad, "oracle_closure", return_value={}), \ + patch.object(broad, "run_profile", side_effect=launch) as run, \ + patch.object(broad.native, "verify_runtime", side_effect=(AssertionError("jar changed") if custody_failed else None)), \ + redirect_stdout(io.StringIO()): + code = broad.main(self.arguments()) + self.assertEqual(1, run.call_count) + result = json.loads((self.output / broad.RECEIPT).read_text(encoding="utf-8")) + self.assertEqual({"packaged-current"}, set(result["profiles"])) + self.assertEqual(set(self.mods), set(result["jars"])) + return code, result + + def test_native_sdk_and_existing_oracles_are_not_historical_dispatch(self): + self.assertEqual(("26.2", "65.1.0"), (broad.Adapter.MINECRAFT, broad.Adapter.FORGE)) + self.assertIs(broad.native.Server, broad.Adapter.Server) + self.assertIs(broad.previous.Adapter.drill_tag, broad.Adapter.drill_tag) + self.assertIs(broad.native, broad.Adapter.drill_tickets.__globals__["native"]) + + def test_native_tile_component_witness_and_stock_are_projected_without_dropping_components(self): + row = {"nbt": '{components:{"minecraft:custom_data":{AdvBayWitness:"S-05:north"}},' + 'Items:[{Slot:0b,id:"minecraft:diamond",count:7,' + 'components:{"minecraft:custom_data":{AdvBayStock:"S-05:north",Unknown:[2,1]}}}]}' } + data = broad.Adapter.drill_data(row) + self.assertEqual({"AdvBayWitness": "S-05:north"}, data["ForgeData"]) + self.assertEqual(7, data["Items"][0]["Count"]) + custom = data["Items"][0]["components"]["minecraft:custom_data"] + self.assertEqual("S-05:north", custom["AdvBayStock"]) + self.assertEqual([2, 1], list(custom["Unknown"])) + self.assertEqual(custom, data["Items"][0]["tag"]) + + def test_legacy_witness_and_incorrect_native_item_types_are_rejected(self): + for nbt in ('{ForgeData:{AdvBayWitness:"stale"}}', + '{Items:[{id:"minecraft:diamond",count:7.0f}]}', + '{Items:[{id:"minecraft:diamond",count:7,Count:7b}]}'): + with self.subTest(nbt=nbt), self.assertRaises(AssertionError): + broad.Adapter.drill_data({"nbt": nbt}) + + def test_staging_passes_only_the_exact_native_target_and_launcher(self): + entries = {key: dict(version=value[0], path=value[1], sha256=value[2]) for key, value in self.mods.items()} + with patch.object(broad.native, "stage") as stage: + self.assertEqual(broad.native.LAUNCHER, + broad.Adapter.stage(self.output, (self.seed, {}, entries), 26600)) + stage.assert_called_once_with(self.output, self.seed, {}, self.mods, 26600) + + def test_supported_full_family_dispatches_all_twenty_one_stations(self): + code, result = self.exercise() + self.assertEqual(0, code) + self.assertEqual("PASS", result["status"]) + self.assertTrue(result["seedUnchanged"] and result["inputsUnchanged"]) + + def test_measured_failure_cannot_become_pass(self): + code, result = self.exercise(failed=True) + self.assertEqual(1, code) + self.assertEqual("FAIL", result["status"]) + self.assertIn("finite assertion failed", result["profiles"]["packaged-current"]["error"]) + + def test_staged_jar_change_cannot_become_pass(self): + code, result = self.exercise(custody_failed=True) + self.assertEqual(1, code) + self.assertEqual("FAIL", result["status"]) + self.assertIn("jar changed", result["profiles"]["packaged-current"]["custodyError"]) + + def test_protected_seed_change_cannot_become_pass(self): + code, result = self.exercise(changed_seed=True) + self.assertEqual(1, code) + self.assertFalse(result["seedUnchanged"]) + + def test_profile_change_cannot_become_pass(self): + code, result = self.exercise(changed_input=True) + self.assertEqual(1, code) + self.assertFalse(result["inputsUnchanged"]) + + def test_missing_mandatory_mod_refuses_before_java_or_output(self): + del self.mods["basemetals"] + with patch.object(broad.native, "read_family_target", return_value=(self.seed, {}, self.mods)), \ + patch.object(broad.native, "java25") as java, patch.object(broad, "run_profile") as launch, \ + redirect_stderr(io.StringIO()): + with self.assertRaises(SystemExit) as failure: + broad.main(self.arguments()) + self.assertEqual(2, failure.exception.code) + java.assert_not_called() + launch.assert_not_called() + self.assertFalse(self.output.exists()) + + def test_incorrect_profile_hash_refuses_before_family_or_launch(self): + args = self.arguments() + args[args.index("--profile-sha256") + 1] = "0" * 64 + with patch.object(broad.native, "read_family_target") as read, patch.object(broad, "run_profile") as launch: + with self.assertRaises(AssertionError): + broad.main(args) + read.assert_not_called() + launch.assert_not_called() + self.assertFalse(self.output.exists()) + + def test_unsupported_modes_do_not_touch_an_existing_failure(self): + self.output.mkdir() + receipt = self.output / broad.RECEIPT + old = b'{"status":"FAIL","reason":"preserve this"}\n' + receipt.write_bytes(old) + for mode in ("both", "packaged-decoupled"): + with patch.object(broad.native, "read_family_target") as read, redirect_stderr(io.StringIO()): + with self.assertRaises(SystemExit) as failure: + broad.main(self.arguments() + ["--mode", mode]) + self.assertEqual(2, failure.exception.code) + read.assert_not_called() + self.assertEqual(old, receipt.read_bytes()) + + def test_real_cli_refuses_unsupported_mode_before_missing_paths(self): + process = subprocess.run([sys.executable, "-B", str(Path(broad.__file__).resolve()), + "--mode", "packaged-decoupled"], capture_output=True, text=True, timeout=20) + self.assertEqual(2, process.returncode) + self.assertIn("BaseMetals is mandatory", process.stderr) + self.assertIn("26.2", process.stderr) + self.assertNotIn("Traceback", process.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_client.py b/test-rig/migration/test_native262_client.py new file mode 100644 index 00000000..e5118696 --- /dev/null +++ b/test-rig/migration/test_native262_client.py @@ -0,0 +1,588 @@ +"""Adapter and actual-SDK preflight tests. No Minecraft client is launched.""" + +import copy +import json +import nbtlib +from pathlib import Path +import shutil +import struct +import tempfile +import unittest +from unittest.mock import patch +import xml.etree.ElementTree as ET +import zlib +from zipfile import ZipFile + +from matrix import digest +import native262_client as client + + +def report_fixture(): + labels = {"inventory": "Inventory", "machines": {f"basepower:unit-{index}": f"Unit {index}" for index in range(39)}} + machines = [dict(block=key, menu_title=name, expected_title=name, status="PASS", required_text="PASS", + tooltip_and_inventory_hitbox="PASS", renders=9, blank_label_controls_rejected=9, + background_extraction_calls=1) for key, name in labels["machines"].items()] + for row in machines: + row["framebuffer_cases"] = [dict(viewport=list(viewport), fill=fill, slot_pixels={}, left=(viewport[0] - 176) // 2, + top=(viewport[1] - 240) // 2) for viewport in client.VIEWPORTS for fill in range(3)] + row["garbled_background_control"] = "synthetic-header-test-only" + gauges = [] + for tank in ("portable", "metal"): + gauges.append(dict(tank=tank, fluid="empty", amount_mb=0, fill_rows=0, different_pixels=0, render_scale=2)) + for fluid in ("water", "lava", "crude-oil", "refined-oil"): + for rows in (15, 30, 60): + gauges.append(dict(tank=tank, fluid=fluid, amount_mb=rows * 10, fill_rows=rows, different_pixels=0, render_scale=2)) + report = dict(status="PASS", minecraft="26.2", forge="65.1.0", advantage_mods=["basepower", "basesteam", "baseelectric"], + mod_versions={key: client.native.VERSIONS[key] for key in ("basepower", "basesteam", "baseelectric")}, + items=109, block_states=100, creative_entries=100, resource_errors=[], primary_resource_status="PASS", + scope_results={key: dict(status="PASS") for key in ("registered_resources", "tank_pixels", "machine_gui", "oil_renderer", "recipe_book", "child_geometry")}, + oil_bucket_identities=True, fluid_rendering=[dict(fluid=key, correct_sprite=True, vertices=4) for key in ( + "basepower:crude_oil", "basepower:flowing_crude_oil", "basepower:refined_oil", "basepower:flowing_refined_oil", "minecraft:water", "minecraft:lava")], + machine_gui_layout_status="PASS", machine_gui_text_status="PASS", machine_gui_text_spec=2, + machine_gui_framebuffer_spec=3, + machine_gui_layout_count=39, machine_gui_text_checks=351, machine_gui_negative_controls_rejected=356, + machine_gui_layouts=machines, tank_gauge_pixel_status="PASS", tank_gauge_pixels=gauges, + recipe_book_grouping_status="PASS", recipe_book_grouping=[dict(packet=f"recipes-{mode}-sweep.bin", status="PASS", + recipes=10, displays=10, cells=10, item_tags=10) for mode in ("normal", "tech", "apocalyptic")], + child_renderer_geometry_status="PASS_WITH_MANUAL_GAPS", child_renderer_geometry=dict(status="PASS_WITH_MANUAL_GAPS", + passed_geometry_cases=25, manual_gaps=1, cases=[dict(case="unit-" + str(i), status="PASS") for i in range(25)] + + [dict(case="hydroturbine-registration", status="MANUAL_GAP_WORLD_REQUIRED")]), + login=dict(status="PASS", realWorld=True, integratedServer=True, nativeComponentsBound=True, + loggingInEventObserved=True, connectionConnected=True, clientLoaded=True, + syntheticComponentBinding=False, itemHolders=100), + projectionCalibration={"synthetic-header-test-only": True}, recipe_registry_ids_match_actual_login=True, + experimentalFixtureGate=dict(protocol=client.FIXTURE_PROTOCOL, status="NOT_OBSERVED", actionCount=0)) + report["world_oil_visual"] = {"synthetic-header-test-only": True} + report["child_geometry_context"] = dict(stage="GENUINELY_WORLDLESS_BEFORE_LOGIN", levelPresent=False, + playerPresent=False, singleplayerServerPresent=False) + return report, labels + + +class ClientAdapterTest(unittest.TestCase): + def test_offline_username_is_bounded_to_native_ascii_protocol_names(self): + for name in ("A", "AdvantageProbe", "A_0123456789abcd"): + self.assertEqual(name, client.validate_username(name)) + for name in ("", "AdvantageClientProbe", "name with space", "name-dash", "caf\u00e9", None, 10): + with self.assertRaisesRegex(AssertionError, "1..16 ASCII"): + client.validate_username(name) + def test_experimental_fixture_policy_rejects_other_profile_receipt_and_copied_level(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source = root / "campaign/packaged-current/AdvantageWorksMigration" + source.mkdir(parents=True) + versions = {"minecraft": client.native.MINECRAFT, "forge": client.native.FORGE, **client.native.VERSIONS} + level = nbtlib.File({"Data": nbtlib.Compound({"DataVersion": nbtlib.Int(4903), + "Version": nbtlib.Compound({"Name": nbtlib.String("26.2")})}), + "fml": nbtlib.Compound({"LoadingModList": nbtlib.List[nbtlib.Compound]([ + nbtlib.Compound({"ModId": nbtlib.String(key), "ModVersion": nbtlib.String(value)}) + for key, value in versions.items()])})}) + level.save(source / "level.dat", gzipped=True) + copied_world = root / "runtime/saves" / client.CLIENT_WORLD + copied_world.mkdir(parents=True) + shutil.copy2(source / "level.dat", copied_world / "level.dat") + (root / "runtime/client-world-source-files.json").write_text(json.dumps({"level.dat": digest(source / "level.dat")})) + profile = root / "profile.json" + mods = [dict(id=key, version=value, path=str(root / (key + ".jar")), sha256=client.hashlib.sha256(key.encode()).hexdigest().upper()) + for key, value in client.native.VERSIONS.items()] + profile.write_text(json.dumps(dict(minecraftVersion="26.2", forge=dict(version="26.2-65.1.0"), mods=mods))) + profile_sha = digest(profile) + receipt_path = source.parents[1] / "native262-broad-results.json" + receipt = dict(status="PASS", minecraft="26.2", forge="65.1.0", inputsUnchanged=True, seedUnchanged=True, + profileSha256=profile_sha, scope="PACKAGED_WALKABLE_WORKS_AND_TWELVE_DRILL_WITNESSES", + jars={row["id"]: row["sha256"] for row in mods}, profiles={"packaged-current": dict(status="PASS", profile="packaged-current", + expectedMods=versions, phases={phase: dict(status="PASS") for phase in ("first", "initial", "operation", "persistence", "restarted")})}) + receipt_path.write_text(json.dumps(receipt)) + policy = client.fixture_policy(root / "runtime", profile, profile_sha, receipt_path, source) + self.assertEqual(client.FIXTURE_SCOPE, policy["eligibility"]) + self.assertEqual(digest(source / "level.dat"), policy["copiedLevelSha256"]) + with self.assertRaisesRegex(AssertionError, "SHA-256 mismatch"): + client.fixture_policy(root / "runtime", profile, "0" * 64, receipt_path, source) + for change in (lambda doc: doc.update(status="FAIL"), lambda doc: doc.update(inputsUnchanged=False), + lambda doc: doc.update(profileSha256="0" * 64), lambda doc: doc["profiles"]["packaged-current"]["phases"]["restarted"].update(status="RUNNING"), + lambda doc: doc["jars"].update(orespawn="0" * 64)): + wrong = copy.deepcopy(receipt) + change(wrong) + receipt_path.write_text(json.dumps(wrong)) + with self.assertRaises(AssertionError): + client.fixture_policy(root / "runtime", profile, profile_sha, receipt_path, source) + receipt_path.write_text(json.dumps(receipt)) + (copied_world / "level.dat").write_bytes(b"copy") + with self.assertRaisesRegex(AssertionError, "stopped source/copy"): + client.fixture_policy(root / "runtime", profile, profile_sha, receipt_path, source) + + def test_failed_native_producer_is_reported_before_success_only_artwork_contract(self): + failed = dict(status="FAIL", error="Native real-world login/component binding did not complete", + loginReadiness=dict(screenClass="unit.UnknownScreen", stage="BEFORE_INTEGRATED_SERVER")) + before = copy.deepcopy(failed) + with self.assertRaisesRegex(AssertionError, "Native real-world login/component binding"): + client.verify_producer(failed) + self.assertEqual(before, failed) + client.verify_producer(dict(status="PASS")) + with self.assertRaisesRegex(AssertionError, "Missing source artwork provenance"): + client.source_references(dict(status="PASS"), {}) + + def test_source_artwork_hashes_and_unique_provider_are_still_required(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + path = root / "power.jar" + name, content = "basepower:textures/gui/unit.png", b"strict-artwork-bytes" + with ZipFile(path, "w") as jar: + jar.writestr("assets/basepower/textures/gui/unit.png", content) + proof = dict(resource=name, sha256=client.hashlib.sha256(content).hexdigest().upper(), width=256, height=256) + mods = dict(basepower=("unit", path, digest(path))) + references = client.source_references(dict(source=proof), mods) + client.require_source(proof, references) + with self.assertRaisesRegex(AssertionError, "frozen jar bytes"): + client.require_source({**proof, "sha256": "0" * 64}, references) + with self.assertRaisesRegex(AssertionError, "Ambiguous"): + client.source_references(dict(source=proof), {**mods, "other": ("unit", path, digest(path))}) + + def test_native_vanilla_root_control_requires_exact_sdk_bytes_and_merged_presence(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + archive = root / "client-extra-26.2-20260616.103818.jar" + resources = {"minecraft:textures/block/stone.png": b"unit-stone-only", + "minecraft:atlases/blocks.json": b'{"sources":[]}'} + with ZipFile(archive, "w") as jar: + for key, value in resources.items(): + jar.writestr("assets/" + key.replace(":", "/", 1), value) + code = dict(archives=[client.record(archive)]) + self.assertEqual("NOT_CAPTURED", client.verify_vanilla_resources(root, code)["status"]) + folder = root / "vanilla-resource-controls" + folder.mkdir() + rows = [dict(resource=key, vanilla=dict(present=True, bytes=len(value), + sha256=client.hashlib.sha256(value).hexdigest().upper()), merged=[dict(pack="vanilla", + bytes=len(value), sha256=client.hashlib.sha256(value).hexdigest().upper())]) + for key, value in resources.items()] + original = dict(protocol="NATIVE65_VANILLA_RESOURCE_ROOTS_V1", status="PASS", + phase="reload-preparation-before-atlas-upload", resources=rows) + path = folder / "control-01.json" + path.write_text(json.dumps(original)) + self.assertEqual("PASS", client.verify_vanilla_resources(root, code)["status"]) + overlay = root / "forge-26.2-65.1.0.jar" + overlay_data = b'{"sources":[{"type":"unit-forge-overlay"}]}' + with ZipFile(overlay, "w") as jar: + jar.writestr("assets/minecraft/atlases/blocks.json", overlay_data) + code["archives"].append(client.record(overlay)) + original["resources"][1]["merged"].append(dict(pack="mod:forge", bytes=len(overlay_data), + sha256=client.hashlib.sha256(overlay_data).hexdigest().upper())) + path.write_text(json.dumps(original)) + self.assertEqual("PASS", client.verify_vanilla_resources(root, code)["status"]) + for change in (lambda doc: doc["resources"][0]["vanilla"].update(present=False), + lambda doc: doc["resources"][0]["vanilla"].update(sha256="0" * 64), + lambda doc: doc["resources"][0].update(merged=[]), + lambda doc: doc["resources"][0]["merged"].append(doc["resources"][0]["merged"][0]), + lambda doc: doc["resources"].pop()): + wrong = copy.deepcopy(original) + change(wrong) + path.write_text(json.dumps(wrong)) + with self.assertRaises(AssertionError): + client.verify_vanilla_resources(root, code) + + def test_preparation_failure_retains_receipt_only_in_a_new_owned_runtime(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def fail(args, owned): + owned["runtime"] = root + raise AssertionError("synthetic preparation failure") + + with patch.object(client, "_run", side_effect=fail): + with self.assertRaises(AssertionError): + client.run(None) + self.assertEqual("FAIL", json.loads((root / client.RESULT).read_text())["status"]) + before = (root / client.RESULT).read_bytes() + with patch.object(client, "_run", side_effect=AssertionError("unowned preflight failure")): + with self.assertRaises(AssertionError): + client.run(None) + self.assertEqual(before, (root / client.RESULT).read_bytes()) + + def test_actual_native_sdk_launch_is_read_only_and_excludes_mutable_child_outputs(self): + source = client.native.RIG.parent / "runClient.launch" + before = source.read_bytes() + plan = client.inspect_launch(source) + self.assertEqual(("26.2", "65.1.0", "20260616.103818"), (plan["minecraft"], plan["forge"], plan["mcp"])) + self.assertEqual("", plan["environment"]["MOD_CLASSES"]) + self.assertEqual(before, source.read_bytes()) + self.assertGreater(len(plan["archives"]), 50) + self.assertTrue(all(not client.mod_ids(row["path"]) & client.FAMILY for row in plan["archives"])) + self.assertEqual(client.ASSET_INDEX_SHA1, "941B1EA6AFB4A472D164C0D12E3E3272BF3435F8") + + def test_missing_native_client_extra_cannot_pass_sdk_preflight(self): + tree = ET.parse(client.native.RIG.parent / "runClient.launch") + entries = next(row for row in tree.getroot().findall("listAttribute") + if row.attrib["key"] == "org.eclipse.jdt.launching.CLASSPATH") + for row in list(entries): + if "client-extra-26.2-20260616.103818.jar" in row.attrib["value"]: + entries.remove(row) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "launch.xml" + tree.write(path, encoding="utf-8") + with self.assertRaisesRegex(AssertionError, "native vanilla client resources"): + client.inspect_launch(path) + + def mutate_launch(self, key, transform): + tree = ET.parse(client.native.RIG.parent / "runClient.launch") + node = next(row for row in tree.findall("stringAttribute") if row.attrib["key"] == key) + node.attrib["value"] = transform(node.attrib["value"]) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "launch.xml" + tree.write(path, encoding="utf-8") + with self.assertRaises(AssertionError): + client.inspect_launch(path) + + def test_agents_classpath_injection_legacy_target_and_connection_arguments_fail(self): + vm = "org.eclipse.jdt.launching.VM_ARGUMENTS" + program = "org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" + for option in (" -javaagent:wrong.jar", " -cp unpinned", " -Dmixin.debug.export=true"): + self.mutate_launch(vm, lambda value, option=option: value + option) + for change in (lambda value: value + " --server localhost", lambda value: value.replace("--gameDir .", "--gameDir protected"), + lambda value: value.replace("--assetIndex 32", "--assetIndex 29"), + lambda value: value.replace("forge_userdev_client", "forge_userdev_server")): + self.mutate_launch(program, change) + + def test_whole_result_requires_all39_gui_and_all26_gauges_without_promoting_manual_gaps(self): + report, labels = report_fixture() + before = copy.deepcopy(report) + with patch.object(client, "png_record", side_effect=lambda root, name, dims: dict(file=name, dimensions=dims)) as images, \ + patch.object(client, "verify_machine_frame", return_value={}), \ + patch.object(client, "verify_tank_frames"), \ + patch.object(client, "verify_projection_calibration", return_value=dict(status="PASS")), \ + patch.object(client, "verify_world_oil", return_value=dict(status="PASS")), \ + patch.object(client, "read_frame", return_value=(320, 240, b"header-test-only", {})), \ + patch.object(client, "slot_mismatches", return_value=1): + result = client.validate_result(report, Path("NOT_A_GAME"), labels, {}) + self.assertEqual(416, images.call_count) + self.assertEqual(1, len(result["manualRequired"])) + self.assertEqual(before, report) + changes = (lambda value: value.update(minecraft="1.21.11"), + lambda value: value["login"].update(realWorld=False), + lambda value: value["login"].update(loggingInEventObserved=False), + lambda value: value["login"].update(syntheticComponentBinding=True), + lambda value: value["experimentalFixtureGate"].update(actionCount=1), + lambda value: value["experimentalFixtureGate"].update(status="AUTO_ACCEPTED"), + lambda value: value["child_geometry_context"].update(levelPresent=True), + lambda value: value.update(recipe_registry_ids_match_actual_login=False), + lambda value: value.update(machine_gui_text_checks=350), + lambda value: value.update(machine_gui_negative_controls_rejected=355), + lambda value: value["machine_gui_layouts"][0].update(background_extraction_calls=True), + lambda value: value["machine_gui_layouts"][0].update(menu_title="Wrong name"), + lambda value: value["machine_gui_layouts"].pop(), + lambda value: value["tank_gauge_pixels"][0].update(different_pixels=1), + lambda value: value["tank_gauge_pixels"].pop(), + lambda value: value["fluid_rendering"][0].update(correct_sprite=False), + lambda value: value["recipe_book_grouping"].pop(), + lambda value: value["scope_results"].pop("oil_renderer"), + lambda value: value["child_renderer_geometry"]["cases"][-1].update(status="PASS")) + for change in changes: + wrong = copy.deepcopy(report) + change(wrong) + with self.assertRaises((AssertionError, KeyError)): + client.validate_result(wrong, Path("NOT_A_GAME"), labels, {}) + + def test_png_integrity_requires_real_bounded_pixel_stream_not_just_an_ihdr_header(self): + def chunk(kind, value): + return struct.pack(">I", len(value)) + kind + value + struct.pack(">I", zlib.crc32(kind + value) & 0xFFFFFFFF) + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + header = chunk(b"IHDR", struct.pack(">IIBBBBB", 2, 2, 8, 6, 0, 0, 0)) + raw = b"\0" + bytes((1, 2, 3, 255)) * 2 + valid = b"\x89PNG\r\n\x1a\n" + header + chunk(b"IDAT", zlib.compress(raw * 2)) + chunk(b"IEND", b"") + path = root / "unit.png" + path.write_bytes(valid) + self.assertEqual(2, client.png_record(root, path.name, (2, 2))["width"]) + for wrong in (valid[:24], valid + b"extra", valid[:-1] + b"x", + b"\x89PNG\r\n\x1a\n" + header + chunk(b"IDAT", zlib.compress(b"")) + chunk(b"IEND", b"")): + path.write_bytes(wrong) + with self.assertRaises((AssertionError, zlib.error)): + client.png_record(root, path.name, (2, 2)) + + +def write_frame(root, name, width, height, data, texts=()): + """Synthetic framebuffer fixture; no native render or campaign PASS is claimed.""" + def chunk(kind, value): + return struct.pack(">I", len(value)) + kind + value + struct.pack(">I", zlib.crc32(kind + value) & 0xFFFFFFFF) + + rgba = root / (name + ".rgba") + rgba.write_bytes(data) + png = root / (name + ".png") + scanlines = b"".join(b"\0" + data[y * width * 4:(y + 1) * width * 4] for y in range(height)) + png.write_bytes(b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)) + + chunk(b"IDAT", zlib.compress(scanlines)) + chunk(b"IEND", b"")) + state = dict(protocol="NATIVE65_GUI_FRAMEBUFFER_FENCED_V1", width=width, height=height, + renderer="net.minecraft.client.gui.render.GuiRenderer", extractor="net.minecraft.client.gui.GuiGraphicsExtractor", + renderSubmitted=True, fenceCompleted=True, screenshotCallbackCompleted=True, texts=list(texts), + blits=[dict(x0=0, y0=12, x1=176, y1=140, color=-1, textureViewIdentity=1, + pipeline="minecraft:pipeline/gui_textured", pose=[1, 0, 0, 1, 0, 0], u0=0, v0=0, u1=1, v1=1)]) + native = root / (name + "-native.json") + native.write_text(json.dumps(state)) + return dict(width=width, height=height, rgba=dict(file=rgba.name, sha256=digest(rgba)), + png=dict(file=png.name, sha256=digest(png)), native=dict(file=native.name, sha256=digest(native))) + + +def machine_frame_fixture(root, *, rendered_text=True): + width, height, left = 320, 240, 72 + slots = [[7 + i % 9 * 18, 157 + i // 9 * 18] for i in range(27)] + [[7 + i * 18, 215] for i in range(9)] + points = [[x, y, -1 if (x + y) % 2 == 0 else -8355712] for x in range(18) for y in range(18) + if x in (0, 17) or y in (0, 17)] + background = bytearray(bytes((20, 30, 40, 255)) * width * height) + for slot_x, slot_y in slots: + for x, y, color in points: + at = ((slot_y + y) * width + left + slot_x + x) * 4 + background[at:at + 4] = bytes((color >> 16 & 255, color >> 8 & 255, color & 255, 255)) + expected = bytearray(background) + if rendered_text: + for x0, y0 in ((112, 2), (80, 141)): + for y in range(y0, y0 + 2): + for x in range(x0, x0 + 40): + at = (y * width + x) * 4 + expected[at:at + 4] = b"\xff" * 4 + texts = [dict(text=name, x=x, y=y, color=-1, dropShadow=True, pose=[1, 0, 0, 1, 72, 0]) + for name, x, y in (("Unit machine", 40, 1), ("Inventory", 8, 140))] + actual = write_frame(root, "machine-actual", width, height, expected, texts) + reference = write_frame(root, "machine-expected", width, height, expected, texts) + hidden = write_frame(root, "machine-background", width, height, background) + sources = {"basepower:textures/gui/container/player_inventory.png": "A" * 64, + "basepower:textures/gui/container/unit.png": "B" * 64} + proof = dict(viewport=[320, 240, 1], fill=0, left=72, top=0, actual=actual, expected=reference, background=hidden, + slot_pixels=dict(border_points=points, slots=slots, checked_pixels=36 * 68, + source_sampling="PHYSICAL_PIXEL_CENTERS_ON_256_LOGICAL_UV_V1", + logical_texture_size=256, expected_machine_slots=[], + slot_border_colors=[[value for _, _, value in points] for _ in slots], + inventory_source=dict(resource=list(sources)[0], sha256="A" * 64, width=256, height=256), + machine_source=dict(resource=list(sources)[1], sha256="B" * 64, width=256, height=256))) + return proof, sources + + +class FramebufferReplayTest(unittest.TestCase): + def test_world_oil_replay_requires_raw_negative_controls_replaced_atlas_and_real_renderer_metadata(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + report = dict(protocol="NATIVE65_LEVEL_RENDERER_OIL_RELOAD_V1", status="PASS", mipmapLevel=4, + scope="ACTUAL_LEVEL_RENDERER_NOT_GUI_OR_SYNTHETIC_WORLD", frames={}, pixelControls=[], + modelsBeforeReload=[], modelsAfterReload=[], nativeGeometryAfterReload=dict(fluid_rendering=[ + dict(correct_sprite=True, vertices=4) for _ in range(6)])) + for name in ("original", "empty", "wrong_water", "restored", "after_reload"): + pixels = bytearray(bytes((40, 50, 60, 255)) * 320 * 240) + matrix = [.06, 0, 0, 0, 0, .01, 0, 0, 0, .045, .01, 0, 0, 0, 0, 1] + camera = [512.5, 15.62, 527] + pools = [] + for index in range(3): + fluid = "minecraft:water" if index == 2 or name == "wrong_water" else "minecraft:empty" if name == "empty" else ( + "basepower:crude_oil" if index == 0 else "basepower:refined_oil") + color = (20, 70, 200, 255) if fluid == "minecraft:water" else (230, 230, 230, 255) if fluid == "minecraft:empty" else ( + (20, 20, 20, 255) if index == 0 else (210, 170, 50, 255)) + samples = [] + for x in range(4): + for z in range(4): + point = [(504, 510, 516)[index] + .75 + x * .75, 6 + 8 / 9, 510 + .75 + z * .75] + px, py = client.world_sample_pixel(matrix, camera, point, 320, 240) + samples.append(point + [px, py]) + for dy in range(-1, 2): + for dx in range(-1, 2): + at = ((py + dy) * 320 + px + dx) * 4 + pixels[at:at + 4] = bytes(color) + pools.append(dict(pool=("crude-oil", "refined-oil", "water-control")[index], fluid=fluid, + compiledAndVisible=True, samples=samples)) + frame = write_frame(root, "synthetic-world-" + name, 320, 240, pixels) + frame.pop("native") + frame.update(renderer="net.minecraft.client.renderer.LevelRenderer", dimension="minecraft:overworld", + fenceCompleted=True, hudHidden=True, renderedSections=4, camera=camera, + viewRotationProjection=matrix, pools=pools) + report["frames"][name] = frame + for actual in ("original", "after_reload"): + for index, pool in enumerate(("crude-oil", "refined-oil", "water-control")): + report["pixelControls"].append(dict(actual=actual, pool=pool, samples=144, + emptyDifferent=0 if index == 2 else 144, wrong_waterDifferent=0 if index == 2 else 144)) + for key, offset in (("modelsBeforeReload", 0), ("modelsAfterReload", 10)): + for index, name in enumerate(("crude_oil", "refined_oil")): + report[key].append(dict(fluid="basepower:" + name, layer="TRANSLUCENT", + stillSprite=f"basepower:blocks/{name}_still", flowingSprite=f"basepower:blocks/{name}_flow", + stillIdentity=1 + index * 2 + offset, flowingIdentity=2 + index * 2 + offset)) + self.assertEqual(8, client.verify_world_oil(root, report)["oilNegativeComparisons"]) + for change in (lambda doc: doc.update(status="FAIL"), lambda doc: doc.update(mipmapLevel=0), + lambda doc: doc["frames"]["original"].update(renderer="net.minecraft.client.gui.render.GuiRenderer"), + lambda doc: doc["frames"]["original"]["pools"][0].update(compiledAndVisible=False), + lambda doc: doc["frames"]["original"]["pools"][0]["samples"][0].__setitem__(3, 10), + lambda doc: doc["frames"]["after_reload"].update(camera=[512.5, 15.63, 527]), + lambda doc: doc["frames"]["after_reload"].update(rgba=doc["frames"]["empty"]["rgba"]), + lambda doc: doc["frames"]["original"].update(rgba=doc["frames"]["wrong_water"]["rgba"]), + lambda doc: doc["pixelControls"][0].update(emptyDifferent=0), + lambda doc: doc["modelsAfterReload"][0].update(stillIdentity=doc["modelsBeforeReload"][0]["stillIdentity"]), + lambda doc: doc["modelsAfterReload"][0].update(stillSprite="minecraft:missingno")): + wrong = copy.deepcopy(report) + change(wrong) + with self.assertRaises(AssertionError): + client.verify_world_oil(root, wrong) + + def test_scale2_high_resolution_border_requires_each_physical_sample_not_repeated_center(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + proof, _ = machine_frame_fixture(root) + slots = proof["slot_pixels"] + slots["slots"].insert(0, [31, 40]) + slots["expected_machine_slots"] = [[31, 40]] + slots["slot_border_colors"] = [[0xFF123410 + point % 2 * 16 + sample for point in range(68) for sample in range(4)]] + [ + [value for value in row for _ in range(4)] for row in slots["slot_border_colors"]] + slots["checked_pixels"] = 37 * 68 * 4 + pixels = bytearray(bytes((20, 30, 40, 255)) * 640 * 480) + for index, (slot_x, slot_y) in enumerate(slots["slots"]): + for point, (x, y, _) in enumerate(slots["border_points"]): + for dy in range(2): + for dx in range(2): + color = slots["slot_border_colors"][index][point * 4 + dy * 2 + dx] + at = (((slot_y + y) * 2 + dy) * 640 + (72 + slot_x + x) * 2 + dx) * 4 + pixels[at:at + 4] = bytes((color >> 16 & 255, color >> 8 & 255, color & 255, 255)) + frame = (640, 480, pixels, {}) + self.assertEqual(0, client.slot_mismatches(frame, slots, 72, 0, 2)) + wrong = copy.deepcopy(slots) + wrong["slot_border_colors"][0] = [0xFF123413 + point % 2 * 16 for point in range(68) for _ in range(4)] + self.assertGreater(client.slot_mismatches(frame, wrong, 72, 0, 2), 0) + + def test_projection_ruler_replays_every_pixel_and_shifted_negative_at_both_scales(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + calibration = dict(protocol="NATIVE65_GUI_PROJECTION_PIXEL_RULER_V1", status="PASS", cases=[]) + for scale in (1, 2): + row = dict(scale=scale, logicalWidth=64, logicalHeight=48, exactPixelsChecked=64 * 48 * scale * scale) + for key, shift in (("actual", 0), ("shiftedRejected", 1)): + pixels = bytearray() + for y in range(48 * scale): + for x in range(64 * scale): + u, v = x // scale, y // scale + color = 0xFF37A95C if 7 + shift <= u < 23 + shift and 9 <= v < 25 else ( + 0xFF3158D5 if (u, v) == (2, 2) else 0xFFDD7341 if (u, v) == (63, 47) else 0xFF101820) + pixels.extend((color >> 16 & 255, color >> 8 & 255, color & 255, 255)) + frame = write_frame(root, f"ruler-{scale}-{key}", 64 * scale, 48 * scale, pixels) + state_path = root / frame["native"]["file"] + state = json.loads(state_path.read_text()) + state.update(blits=[], fills=[dict(x0=x, y0=y, x1=x + w, y1=y + h, col1=-1, col2=-1, + pipeline="minecraft:pipeline/gui", pose=[1, 0, 0, 1, 0, 0]) + for x, y, w, h in ((7 + shift, 9, 16, 16), (2, 2, 1, 1), (63, 47, 1, 1))]) + state_path.write_text(json.dumps(state)) + frame["native"]["sha256"] = digest(state_path) + row[key] = frame + calibration["cases"].append(row) + self.assertEqual(15360, client.verify_projection_calibration(root, calibration)["physicalPixels"]) + for key in ("actual", "shiftedRejected"): + wrong = copy.deepcopy(calibration) + wrong["cases"][1][key] = copy.deepcopy(wrong["cases"][1]["shiftedRejected" if key == "actual" else "actual"]) + with self.assertRaisesRegex(AssertionError, "ruler pixels"): + client.verify_projection_calibration(root, wrong) + + def test_custom_gold_machine_border_is_distinct_from_fixed_player_inventory(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + proof, _ = machine_frame_fixture(root) + frame = client.read_frame(root, proof["actual"]) + slots = proof["slot_pixels"] + gold = [0xFFDBAA20 if index % 2 else 0xFF805311 for index in range(68)] + slots["slots"].insert(0, [31, 40]) + slots["expected_machine_slots"] = [[31, 40]] + slots["slot_border_colors"].insert(0, gold) + slots["checked_pixels"] += 68 + pixels = bytearray(frame[2]) + for (x, y, _), color in zip(slots["border_points"], gold): + at = ((40 + y) * 320 + 72 + 31 + x) * 4 + pixels[at:at + 4] = bytes((color >> 16 & 255, color >> 8 & 255, color & 255, 255)) + self.assertEqual(0, client.slot_mismatches((320, 240, pixels, {}), slots, 72, 0, 1)) + wrong = copy.deepcopy(slots) + wrong["slot_border_colors"][0] = wrong["slot_border_colors"][-1] + self.assertGreater(client.slot_mismatches((320, 240, pixels, {}), wrong, 72, 0, 1), 0) + wrong = copy.deepcopy(slots) + wrong["slots"][0][0] += 1 + with self.assertRaisesRegex(AssertionError, "GUI declaration"): + client.slot_mismatches((320, 240, pixels, {}), wrong, 72, 0, 1) + + def test_caption_pixels_and_slot_borders_are_replayed_independently(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + proof, sources = machine_frame_fixture(root) + result = client.verify_machine_frame(root, proof, "Unit machine", "Inventory", sources) + self.assertEqual(160, result["captionInkPixels"]) + self.assertEqual(36 * 68, result["slotPixels"]) + actual = root / proof["actual"]["rgba"]["file"] + data = bytearray(actual.read_bytes()) + for x, y, message in ((112, 2, "Caption framebuffer"), (79, 157, "Slot framebuffer")): + changed = bytearray(data) + at = (y * 320 + x) * 4 + changed[at:at + 4] = bytes((0, 0, 0, 255)) + actual.write_bytes(changed) + proof["actual"]["rgba"]["sha256"] = digest(actual) + with self.assertRaisesRegex(AssertionError, message): + client.verify_machine_frame(root, proof, "Unit machine", "Inventory", sources) + + def test_extracted_caption_without_actual_glyph_pixels_cannot_pass(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + proof, sources = machine_frame_fixture(root, rendered_text=False) + with self.assertRaisesRegex(AssertionError, "Blank expected caption"): + client.verify_machine_frame(root, proof, "Unit machine", "Inventory", sources) + + def test_missing_fence_or_native_queue_is_rejected_even_with_valid_png_and_rgba(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + frame = write_frame(root, "fence", 2, 2, bytes((1, 2, 3, 255)) * 4) + path = root / frame["native"]["file"] + original = json.loads(path.read_text()) + for change in (lambda value: value.update(fenceCompleted=False), lambda value: value.update(blits=[]), + lambda value: value["blits"][0].update(textureViewIdentity=0), + lambda value: value["blits"][0].update(u0=float("nan"))): + changed = copy.deepcopy(original) + change(changed) + path.write_text(json.dumps(changed)) + frame["native"]["sha256"] = digest(path) + with self.assertRaises(AssertionError): + client.read_frame(root, frame) + + def test_tank_pixels_outside_gauge_and_accepted_garbled_control_are_rejected(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + empty = bytearray(bytes((10, 20, 30, 255)) * 176 * 240) + expected, wrong = bytearray(empty), bytearray(empty) + for y in range(66, 81): + for x in range(80, 96): + at = (y * 176 + x) * 4 + expected[at:at + 4] = bytes((20, 70, 200, 255)) + wrong[at:at + 4] = bytes((200, 70, 20, 255)) + row = dict(actual_frame=write_frame(root, "tank-actual", 176, 240, expected), + expected_frame=write_frame(root, "tank-expected", 176, 240, expected), + empty_frame=write_frame(root, "tank-empty", 176, 240, empty), + garbled_frame=write_frame(root, "tank-garbled", 176, 240, wrong), + source_artwork=dict(resource="basepower:tank.png", sha256="A" * 64, width=256, height=256), + render_scale=1, + fill_rows=15, whole_panel_different_pixels=0, blank_control_different_pixels=240, + garbled_control_different_pixels=240) + for key in ("actual_frame", "expected_frame"): + path = root / row[key]["native"]["file"] + state = json.loads(path.read_text()) + blit = {**state["blits"][0], "x0": 80, "y0": 66, "x1": 96, "y1": 82, + "scissor": [80, 66, 16, 15]} + state["blits"].append(blit) + path.write_text(json.dumps(state)) + row[key]["native"]["sha256"] = digest(path) + client.verify_tank_frames(root, row, {"basepower:tank.png": "A" * 64}) + actual = root / row["actual_frame"]["rgba"]["file"] + changed = bytearray(expected) + changed[(20 * 176 + 5) * 4] = 255 + actual.write_bytes(changed) + row["actual_frame"]["rgba"]["sha256"] = digest(actual) + with self.assertRaisesRegex(AssertionError, "Whole tank"): + client.verify_tank_frames(root, row, {"basepower:tank.png": "A" * 64}) + actual.write_bytes(expected) + row["actual_frame"]["rgba"]["sha256"] = digest(actual) + row["garbled_frame"] = copy.deepcopy(row["expected_frame"]) + with self.assertRaisesRegex(AssertionError, "garbled tank control"): + client.verify_tank_frames(root, row, {"basepower:tank.png": "A" * 64}) + state_path = root / row["actual_frame"]["native"]["file"] + state = json.loads(state_path.read_text()) + state["blits"][-1]["y1"] = 81 + state_path.write_text(json.dumps(state)) + row["actual_frame"]["native"]["sha256"] = digest(state_path) + with self.assertRaisesRegex(AssertionError, "Native fluid extraction"): + client.verify_tank_frames(root, row, {"basepower:tank.png": "A" * 64}) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_client_diagnostics.py b/test-rig/migration/test_native262_client_diagnostics.py new file mode 100644 index 00000000..496f70c4 --- /dev/null +++ b/test-rig/migration/test_native262_client_diagnostics.py @@ -0,0 +1,75 @@ +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch +from zipfile import ZipFile, ZipInfo + +import native12111 +import native262_client_diagnostics as diagnostics + + +class Native262ClientDiagnosticsTest(unittest.TestCase): + def test_predecessor_status_ping_exceptions_are_not_waived(self): + text = (Path(__file__).parent / "fixtures/native2612-client-netty-java25.txt").read_text(encoding="utf-8") + strict = native12111.log_findings(text) + self.assertTrue(strict[0]) + actual = diagnostics.classify(text, native12111.log_findings) + self.assertTrue(set(strict[0]) <= set(actual[0])) + self.assertEqual([], actual[1]) + + @staticmethod + def text(rows): + return "\n".join("[time] [" + row["thread"] + "/" + row["level"] + "] [" + row["logger"] + "]: " + + row["message"] + "\n" + "\n".join(row["body"]) for row in rows) + + def test_exact_native_sequences_retain_diagnostics_but_no_other_failures(self): + import copy + for name, rows in diagnostics.variants().items(): + failures, notes = diagnostics.classify(self.text(rows), native12111.log_findings) + self.assertFalse(failures) + self.assertEqual(len(rows), len(notes)) + for slot in range(len(rows)): + for field in ("thread", "level", "logger", "message", "body"): + changed = copy.deepcopy(rows) + if field == "body": + changed[slot][field].append("\tat zone.moddev.mc.basepower.Machine.tick(Machine.java:10)") + else: + changed[slot][field] += "changed" + with self.subTest(appender=name, slot=slot, field=field): + self.assertTrue(diagnostics.classify(self.text(changed), native12111.log_findings)[0]) + self.assertTrue(diagnostics.classify(self.text(rows[:slot] + rows[slot+1:]), native12111.log_findings)[0]) + self.assertTrue(diagnostics.classify(self.text(rows) + "\njava.lang.IllegalStateException: unrelated", native12111.log_findings)[0]) + + def test_missing_duplicate_and_unknown_dependencies_fail(self): + for rows in ([], [{"path": "unknown.jar", "sha256": "0" * 64}], + [{"path": "netty-common-4.2.15.Final.jar", "sha256": "0" * 64}] * 4): + with self.subTest(rows=rows), self.assertRaises(AssertionError): + diagnostics.verify_bindings({"archives": rows}) + + def test_sdk_metadata_can_change_but_payload_mutations_cannot(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + def archive(name, rows, stamp): + path = root / name + with ZipFile(path, "w") as output: + for key, value in rows: + output.writestr(ZipInfo(key, date_time=stamp), value) + return path + rows = [("a/A.class", b"class-bytes"), ("META-INF/MANIFEST.MF", b"Manifest-Version: 1.0\n")] + original = archive("original.jar", rows, (2026, 1, 1, 0, 0, 0)) + reordered = archive("repacked.jar", list(reversed(rows)), (2026, 11, 12, 0, 0, 0)) + with patch.object(diagnostics, "SDK_PAYLOAD_SHA", diagnostics.sdk_payload_sha(original)): + diagnostics.verify_sdk_payload(reordered) + for label, changed in (("changed", [("a/A.class", b"mutated"), rows[1]]), + ("missing", rows[1:]), ("added", rows + [("other.class", b"extra")]), + ("renamed", [("a/B.class", rows[0][1]), rows[1]])): + with self.subTest(label=label), self.assertRaises(AssertionError): + diagnostics.verify_sdk_payload(archive(label + ".jar", changed, (2026, 1, 1, 0, 0, 0))) + with self.assertWarns(UserWarning): + duplicate = archive("duplicate.jar", rows + [rows[0]], (2026, 1, 1, 0, 0, 0)) + with self.assertRaisesRegex(AssertionError, "duplicate SDK"): + diagnostics.verify_sdk_payload(duplicate) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_endurance.py b/test-rig/migration/test_native262_endurance.py new file mode 100644 index 00000000..87b5476a --- /dev/null +++ b/test-rig/migration/test_native262_endurance.py @@ -0,0 +1,247 @@ +import copy +import gzip +import json +import os +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +import nbtlib as n + +import native262 as native +import native262_endurance as producer +import native262_endurance_replay as replay +from native262_layout import shared_file, region_file +import test_native12111_endurance_replay as retained_controls + +VERIFICATION = Path("D:/PowerAdvantage/_verification/base-262-port-20261112") + + +def native_control_seal(): + keys = ("NATIVE262_CONTROL_PROFILE", "NATIVE262_CONTROL_PROFILE_SHA256", + "NATIVE262_CONTROL_ENGINE_RECEIPT", "NATIVE262_CONTROL_ENGINE_SHA256") + selected = [os.environ.get(key) for key in keys] + if any(selected): + if not all(selected): + raise ValueError("Select the complete hash-bound native endurance control fixture") + return replay.seal(Path(selected[0]), selected[1], + native.RIG / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json", + VERIFICATION / "native262-code-inventory-01.json", + "D678E1F645EE29AFC01464D65A6C17F992FB710D3A4612961CACEF8A79240F5C", + Path("C:/Users/johnb/.jdks/jdk-25.0.3+9/bin/java.exe"), replay.Budget(), retrospective=True, + engine_receipt=Path(selected[2]), engine_receipt_sha256=selected[3]) + return replay.seal(VERIFICATION / "native262-local-candidate-03.json", + "A98D4F9B54A0AE47392D9A3CF81335F5E228F718C7D322DF9838A94AD27D9736", + native.RIG / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json", + VERIFICATION / "native262-code-inventory-01.json", + "D678E1F645EE29AFC01464D65A6C17F992FB710D3A4612961CACEF8A79240F5C", + Path("C:/Users/johnb/.jdks/jdk-25.0.3+9/bin/java.exe"), replay.Budget(), retrospective=True, + engine_receipt=VERIFICATION / "engine-proof-02/receipt.json", + engine_receipt_sha256="F270C63CD876098DDF8B7176802E203004D306B345DD3569E78EF51D0D8A56D4") + + +def put_nbt(path, value): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(gzip.compress(retained_controls.encoded(value))) + + +def stopped_fixture(root, dimension="minecraft:overworld"): + world = root / replay.WORLD + put_nbt(world / "level.dat", n.Compound(Data=n.Compound(DataVersion=n.Int(4903)))) + put_nbt(shared_file(world, "minecraft:world_gen_settings"), n.Compound( + DataVersion=n.Int(4903), data=n.Compound(seed=n.Long(77), generate_structures=n.Byte(0), + unknown=n.IntArray([3, 2, 1])))) + chunk = retained_controls.chunk() + chunk["DataVersion"] = n.Int(4903) + path = region_file(world, dimension, 0, 0) + path.parent.mkdir(parents=True) + path.write_bytes(retained_controls.region(retained_controls.payload(chunk))) + for station in replay.endurance.CHECKED: + document = retained_controls.document() + document["dimension"] = dimension + retained_controls.put_json(root / "advantage-works-results" / f"{station}-checkpoint-restart-resumed.json", document) + return world + + +def complete_report(): + report = retained_controls.complete_report() + report.update(minecraft="26.2", forge="65.1.0") + report["profiles"] = {mode: copy.deepcopy(next(iter(report["profiles"].values()))) for mode in replay.MODES} + return report + + +class Native262EnduranceContracts(unittest.TestCase): + def test_complete_two_world_gate_rejects_held_smoke_missing_resume_and_low_ticks(self): + replay.complete(complete_report()) + for key, value in (("status", "HELD"), ("status", "SMOKE_PASS_NOT_ENDURANCE"), + ("minecraft", "1.21.11"), ("duration_seconds", 1799), ("retained_unchanged", False)): + changed = complete_report() + changed[key] = value + with self.subTest(key=key, value=value), self.assertRaises(AssertionError): + replay.complete(changed) + for key, value in (("resumed_processing", "FAIL"), ("endurance_batches", 29), + ("verified_checkpoints", 32), ("game_ticks", 32399), ("elapsed_seconds", 1799)): + changed = complete_report() + changed["profiles"][replay.MODES[1]][key] = value + with self.subTest(key=key), self.assertRaises(AssertionError): + replay.complete(changed) + + def test_stopped_exact_typed_nbt_all_dimensions_and_nested_custom_dimension(self): + for dimension in ("minecraft:overworld", "minecraft:the_nether", "minecraft:the_end", "example:deep/cave"): + with self.subTest(dimension=dimension), tempfile.TemporaryDirectory() as folder: + root = Path(folder) + world = stopped_fixture(root, dimension) + before = {str(path.relative_to(world)): path.read_bytes() for path in world.rglob("*") if path.is_file()} + actual = replay.raw_final(root, replay.Budget()) + self.assertEqual("PASS", actual["status"]) + self.assertEqual(4903, actual["dataVersion"]) + self.assertEqual(len(replay.endurance.CHECKED), actual["comparedRows"]) + self.assertEqual(before, {str(path.relative_to(world)): path.read_bytes() for path in world.rglob("*") if path.is_file()}) + station = replay.endurance.CHECKED[0] + path = root / "advantage-works-results" / f"{station}-checkpoint-restart-resumed.json" + document = json.loads(path.read_text()) + value = retained_controls.tile() + value["Items"][0]["count"] = n.Int(2) + document["snapshot"][0]["nbt"] = value.snbt() + path.write_text(json.dumps(document)) + with self.assertRaisesRegex(AssertionError, "raw state/component mismatch"): + replay.raw_final(root, replay.Budget()) + + def test_missing_external_worldgen_is_not_recovered_from_level_dat(self): + with tempfile.TemporaryDirectory() as folder: + root = Path(folder) + world = stopped_fixture(root) + shared_file(world, "minecraft:world_gen_settings").unlink() + with self.assertRaises(FileNotFoundError): + replay.raw_final(root, replay.Budget()) + + def test_native_terrain_rejects_old_version_wrong_count_and_packed_tile(self): + native_chunk = retained_controls.chunk() + with self.assertRaises(AssertionError): + replay.terrain(retained_controls.payload(native_chunk), (0, 0)) + native_chunk["DataVersion"] = n.Int(4903) + for compression in (1, 2, 3): + replay.terrain(retained_controls.payload(native_chunk, compression), (0, 0)) + for callback in (lambda value: value.__setitem__("DataVersion", n.Short(4903)), + lambda value: value["block_entities"][0]["Items"][0].__setitem__("count", n.Byte(1)), + lambda value: value["block_entities"][0].__setitem__("keepPacked", n.Byte(1))): + changed = copy.deepcopy(native_chunk) + callback(changed) + with self.assertRaises(AssertionError): + replay.terrain(retained_controls.payload(changed), (0, 0)) + + def test_preseal_time_precedes_both_setups_retrospective_never_becomes_genuine(self): + with tempfile.TemporaryDirectory() as folder: + root = Path(folder) + for mode in replay.MODES: + retained_controls.put_json(root / mode / "advantage-works-results/W-01-checkpoint-setup.json", + dict(recordedAtUtc="2026-11-12T04:00:00+00:00")) + seal = dict(purpose=replay.PRE_RUN, createdAtUtc="2026-11-12T03:59:59+00:00") + self.assertTrue(replay.custody(seal, root, replay.Budget())["preRunCustody"]) + seal["createdAtUtc"] = "2026-11-12T04:00:01+00:00" + with self.assertRaises(AssertionError): + replay.custody(seal, root, replay.Budget()) + seal["purpose"] = replay.RETROSPECTIVE + self.assertFalse(replay.custody(seal, root, replay.Budget())["preRunCustody"]) + + def test_hold_request_is_not_a_pass(self): + with tempfile.TemporaryDirectory() as folder: + root = Path(folder) + (root / "hold-endurance.request").touch() + with self.assertRaises(producer.EnduranceHold): + producer.check_hold(root) + + def test_qualified_run_cannot_silently_omit_retained_world(self): + with self.assertRaisesRegex(ValueError, "original-world migration"): + producer.retained_input(SimpleNamespace(retained=None, seconds=1800)) + self.assertIsNone(producer.retained_input(SimpleNamespace(retained=None, seconds=60))) + + def test_retained_receipt_must_be_full_original110_not_hook_only_or_an_old_candidate(self): + profile_sha, inventory_sha = "A" * 64, "B" * 64 + capture = dict(status="PASS", witnesses=14, + serialized=dict(status="PASS", witnesses=14, nativeDataVersion=4903)) + # Schema-shaped unit input, not evidence of an actual migration or endurance run. + prior = dict(status="PASS", scope=producer.MIGRATION_SCOPE, route="original110", + minecraft="26.2", forge="65.1.0", profileSha256=profile_sha, + inputHashes={"unit-inventory": inventory_sha}, sourceUnchanged=True, + referenceUnchanged=True, seedUnchanged=True, inputsUnchanged=True, + phases=dict(converted=copy.deepcopy(capture), restarted=copy.deepcopy(capture), + operation={key: {} for key in ("destination", "steamCrusher", "electricFurnace", "readiness")})) + producer.require_retained_receipt(prior, profile_sha, inventory_sha) + for field, value in (("route", "released110"), ("scope", "INSTRUMENTED_MIGRATION_HOOK_CONVERSION_RESTART_ONLY"), + ("profileSha256", "0" * 64), ("referenceUnchanged", False)): + changed = copy.deepcopy(prior) + changed[field] = value + with self.subTest(field=field), self.assertRaises(AssertionError): + producer.require_retained_receipt(changed, profile_sha, inventory_sha) + for field in ("operation", "restarted"): + changed = copy.deepcopy(prior) + changed["phases"][field] = {} + with self.subTest(phase=field), self.assertRaises(AssertionError): + producer.require_retained_receipt(changed, profile_sha, inventory_sha) + + def test_every_interval_has_real_18_tps_and_starvation_is_not_a_gameplay_diagnosis(self): + before = dict(worldTime=100, recordedAtUtc="2026-11-12T04:00:00+00:00") + after = dict(worldTime=1300, recordedAtUtc="2026-11-12T04:01:00+00:00") + self.assertEqual(20, producer.interval_metrics(before, after, "cycle-001")["ticksPerSecond"]) + after["worldTime"] = 1179 + with self.assertRaisesRegex(producer.ThroughputFailure, "not a gameplay diagnosis"): + producer.interval_metrics(before, after, "cycle-001") + after["worldTime"] = True + with self.assertRaises(AssertionError): + producer.interval_metrics(before, after, "cycle-001") + + def test_fresh_world_info_retains_full_native_unknown_data(self): + with tempfile.TemporaryDirectory() as folder: + root = Path(folder) + world = stopped_fixture(root) + actual = producer.world_info(world) + self.assertEqual(77, actual["RandomSeed"]) + self.assertEqual(0, actual["MapFeatures"]) + self.assertIn("unknown", actual["fullWorldgenSnbt"]) + self.assertIn("[I; 3, 2, 1]", actual["fullWorldgenSnbt"]) + + def test_frozen_oracle_verifier_rejects_changed_bytes_duplicate_fields_and_wrong_purpose(self): + with tempfile.TemporaryDirectory() as folder: + root = Path(folder) + oracle = root / "oracle.py" + oracle.write_text("test fixture, not a production oracle") + seal_path = root / "seal.json" + document = native_control_seal() + document["files"][str(oracle)] = native.digest(oracle) + seal_path.write_text(json.dumps(document)) + pin = native.digest(seal_path) + replay.verify_seal(seal_path, pin, replay.Budget()) + oracle.write_text("changed") + with self.assertRaises(AssertionError): + replay.verify_seal(seal_path, pin, replay.Budget()) + seal_path.write_text('{"format":1,"format":1}') + with self.assertRaises(ValueError): + replay.verify_seal(seal_path, native.digest(seal_path), replay.Budget()) + + def test_full_closure_binds_classifier_review_receipt_and_native_primary_sources(self): + document = native_control_seal() + self.assertEqual(replay.RETROSPECTIVE, document["purpose"], "Unit control is never a genuine qualification seal") + self.assertIn(str(Path(native.__file__).resolve()), document["files"]) + self.assertIn(str(Path(replay.platform_diagnostics.__file__).resolve()), document["files"]) + self.assertIn(document["engineReceipt"], document["files"]) + self.assertIn(replay.platform_diagnostics.SOURCES_JAR_SHA, document["files"].values()) + with tempfile.TemporaryDirectory() as folder: + path = Path(folder) / "seal.json" + for change in ("omit-oracle", "omit-engine", "old-purpose"): + altered = copy.deepcopy(document) + if change == "omit-oracle": + del altered["files"][altered["oracles"][0]] + elif change == "omit-engine": + del altered["files"][altered["engineReceipt"]] + else: + altered["purpose"] = "NATIVE61_ENDURANCE_PRE_RUN_SEAL" + path.write_text(json.dumps(altered)) + with self.subTest(change=change), self.assertRaises(AssertionError): + replay.verify_seal(path, native.digest(path), replay.Budget()) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_interop.py b/test-rig/migration/test_native262_interop.py new file mode 100644 index 00000000..50cabb7f --- /dev/null +++ b/test-rig/migration/test_native262_interop.py @@ -0,0 +1,85 @@ +"""Keep the native capability checks strict; synthetic documents are not gameplay proof.""" + +import copy +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +import native262_interop as adapter + + +def observation(phase="exercise"): + ids = adapter.contracts.EXERCISE_CHECK_IDS if phase == "exercise" else adapter.contracts.RESTORED_CHECK_IDS + return dict(phase=phase, status="PASS", worldTime=20, + checks=[dict(id=key, status="PASS") for key in sorted(ids)], + sharedChecks=[dict(id="basemetals/native-power-crushing", status="PASS")]) + + +class Native262InteropTest(unittest.TestCase): + def validate(self, document, phase="exercise"): + adapter.validate_observation(document, phase, dict(completedWorldTime=20)) + + def test_both_phases_retain_the_exact_interface_contracts(self): + for phase in ("exercise", "restored"): + self.validate(observation(phase), phase) + self.assertEqual(("26.2", "65.1.0"), (adapter.native.MINECRAFT, adapter.native.FORGE)) + + def test_missing_duplicate_and_failed_capability_witnesses_are_rejected(self): + document = observation() + changes = [copy.deepcopy(document) for _ in range(3)] + changes[0]["checks"].pop() + changes[1]["checks"][-1] = changes[1]["checks"][0] + changes[2]["checks"][0]["status"] = "FAIL" + for changed in changes: + with self.subTest(checks=changed["checks"]), self.assertRaises(AssertionError): + self.validate(changed) + + def test_wrong_phase_time_or_overall_status_cannot_be_pass(self): + for field, value in (("phase", "restored"), ("worldTime", 19), ("worldTime", 20.0), ("status", "MANUAL_REQUIRED")): + changed = observation() + changed[field] = value + with self.subTest(field=field), self.assertRaises(AssertionError): + self.validate(changed) + + def test_central_base_metals_crushing_is_mandatory(self): + for checks in ([], [dict(id="basemetals/native-power-crushing", status="SKIP")], + [dict(id="local-fallback", status="PASS")], + [dict(id="basemetals/native-power-crushing", status="PASS")] * 2): + changed = observation() + changed["sharedChecks"] = checks + with self.subTest(checks=checks), self.assertRaises(AssertionError): + self.validate(changed) + + def test_survival_oil_contract_keeps_both_oils_and_all_ten_checks(self): + ids = [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")] + good = dict(gameMode="survival", worldTime=20, checks=[dict(id=key, status="PASS") for key in ids]) + adapter.previous.require_oil_buckets(good, dict(completedWorldTime=20)) + for field, value in (("gameMode", "creative"), ("checks", good["checks"][:-1]), ("worldTime", 19)): + changed = copy.deepcopy(good) + changed[field] = value + with self.subTest(field=field), self.assertRaises(AssertionError): + adapter.previous.require_oil_buckets(changed, dict(completedWorldTime=20)) + + def test_preflight_does_not_start_or_stage_a_world(self): + args = SimpleNamespace(port=26601, preflight_only=True) + inputs = SimpleNamespace(receipt=lambda: dict(minecraft="26.2", forge="65.1.0")) + with patch.object(adapter, "preflight", return_value=inputs), \ + patch.object(adapter.native, "stage") as stage, patch.object(adapter.native, "Server") as server: + result = adapter.run(args) + self.assertEqual("READY_NOT_RUN", result["status"]) + self.assertFalse(result["qualificationAllowed"]) + stage.assert_not_called() + server.assert_not_called() + + def test_privileged_port_is_refused_before_inputs_or_launch(self): + with patch.object(adapter, "preflight") as preflight, patch.object(adapter.native, "Server") as server: + with self.assertRaises(ValueError): + adapter.run(SimpleNamespace(port=80)) + preflight.assert_not_called() + server.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_layout.py b/test-rig/migration/test_native262_layout.py new file mode 100644 index 00000000..4ead2c0d --- /dev/null +++ b/test-rig/migration/test_native262_layout.py @@ -0,0 +1,165 @@ +import copy +from pathlib import Path +import tempfile +import unittest +import uuid + +import nbtlib + +import native262_layout as layout + + +class Native262LayoutTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.world = Path(self.temp.name) / "world" + self.world.mkdir() + self.identity = uuid.UUID("12345678-1234-5678-9abc-def012345678") + + def save(self, path, value): + path.parent.mkdir(parents=True, exist_ok=True) + nbtlib.File(value, gzipped=True).save(path) + + def level(self, **extra): + data = nbtlib.Compound(DataVersion=nbtlib.Int(4903), **extra) + self.save(self.world / "level.dat", nbtlib.Compound(Data=data)) + return data + + def words(self, identity=None): + value = (identity or self.identity).int + return nbtlib.IntArray([((value >> shift) & 0xffffffff) - + (0x100000000 if (value >> shift) & 0x80000000 else 0) + for shift in (96, 64, 32, 0)]) + + def test_all_vanilla_dimensions_are_namespaced_not_legacy_folders(self): + for key, name in (("minecraft:overworld", "overworld"), + ("minecraft:the_nether", "the_nether"), + ("minecraft:the_end", "the_end")): + self.assertEqual(self.world / "dimensions/minecraft" / name, + layout.dimension_directory(self.world, key)) + + def test_custom_nested_dimension_identity_is_not_inferred_from_basename(self): + for key in ("test:deep/caverns", "test:dim-1", "other:dim1"): + namespace, path = key.split(":") + self.assertEqual(self.world / "dimensions" / namespace / path, + layout.dimension_directory(self.world, key)) + self.assertNotEqual(layout.dimension_directory(self.world, "test:dim-1"), + layout.dimension_directory(self.world, "minecraft:the_nether")) + + def test_legacy_folders_never_override_target_layout(self): + for path in ("region", "DIM-1/region", "DIM1/region", "playerdata"): + (self.world / path).mkdir(parents=True) + self.assertEqual(self.world / "dimensions/minecraft/overworld/region/r.0.0.mca", + layout.region_file(self.world, "minecraft:overworld", 0, 0)) + self.assertEqual(self.world / "players/data" / (str(self.identity) + ".dat"), + layout.player_file(self.world, self.identity)) + + def test_negative_chunk_coordinates_use_floor_division_for_every_storage_kind(self): + for x, z, rx, rz in ((-33, -1, -2, -1), (-32, -32, -1, -1), + (-1, 32, -1, 1), (31, -33, 0, -2), (32, 0, 1, 0)): + for kind in ("region", "entities", "poi"): + path = layout.region_file(self.world, "test:deep/caverns", x, z, kind) + self.assertEqual(self.world / "dimensions/test/deep/caverns" / kind / + f"r.{rx}.{rz}.mca", path) + + def test_coordinates_and_storage_kinds_fail_closed(self): + for x, z in ((True, 0), (0, False), (1.5, 0), (0, "1")): + with self.subTest(x=x, z=z), self.assertRaises(ValueError): + layout.region_file(self.world, "minecraft:overworld", x, z) + for kind in ("DIM-1", "../entities", "data", ""): + with self.subTest(kind=kind), self.assertRaises(ValueError): + layout.region_file(self.world, "minecraft:overworld", 0, 0, kind) + + def test_resource_keys_cannot_escape_or_change_phase(self): + for key in ("overworld", "../minecraft:overworld", "minecraft:../overworld", + "minecraft:/overworld", "minecraft:a//b", "minecraft:a/./b", + "minecraft:a\\b", "Minecraft:overworld", "minecraft:", "a:b:c"): + with self.subTest(key=key), self.assertRaises(ValueError): + layout.dimension_directory(self.world, key) + + def test_shared_data_uses_its_full_namespaced_resource_key(self): + self.assertEqual(self.world / "data/minecraft/world_gen_settings.dat", + layout.shared_file(self.world, "minecraft:world_gen_settings")) + self.assertEqual(self.world / "data/test/deep/unknown.dat", + layout.shared_file(self.world, "test:deep/unknown")) + + def test_player_uuid_is_not_a_path_or_a_loose_alias(self): + for value in ("../player", "not-a-uuid", "12345678123456789abcdef012345678", + str(self.identity).upper(), None, 0): + with self.subTest(value=value), self.assertRaises(ValueError): + layout.player_file(self.world, value) + + def test_level_requires_the_native_target_version_without_coercion(self): + for value in (nbtlib.Int(4671), nbtlib.String("4903"), nbtlib.Long(4903)): + self.save(self.world / "level.dat", nbtlib.Compound(Data=nbtlib.Compound(DataVersion=value))) + with self.subTest(value=value), self.assertRaises(ValueError): + layout.read_level(self.world) + self.level(Unknown=nbtlib.Compound(keep=nbtlib.String("exact"))) + data = layout.read_level(self.world) + self.assertEqual("exact", data["Unknown"]["keep"]) + + def test_missing_shared_worldgen_does_not_fall_back_to_embedded_legacy_data(self): + self.level(WorldGenSettings=nbtlib.Compound(seed=nbtlib.Long(17))) + with self.assertRaises(FileNotFoundError): + layout.read_worldgen(self.world) + + def test_shared_worldgen_preserves_unknown_complete_nbt(self): + self.level() + settings = nbtlib.Compound(seed=nbtlib.Long(-17), + Unknown=nbtlib.Compound(raw=nbtlib.ByteArray([1, -2, 3]))) + path = layout.shared_file(self.world, "minecraft:world_gen_settings") + self.save(path, nbtlib.Compound(DataVersion=nbtlib.Int(4903), data=settings, + UnknownEnvelope=nbtlib.String("retain"))) + before = path.read_bytes() + result = layout.read_worldgen(self.world) + self.assertEqual(settings, result["data"]) + self.assertEqual("retain", result["UnknownEnvelope"]) + result["data"]["Unknown"]["raw"][0] = 99 + self.assertEqual(before, path.read_bytes()) + + def test_shared_data_requires_native_version_and_compound_payload(self): + self.level() + path = layout.shared_file(self.world, "minecraft:world_gen_settings") + for tag in (nbtlib.Compound(DataVersion=nbtlib.Int(4671), data=nbtlib.Compound()), + nbtlib.Compound(DataVersion=nbtlib.Int(4903)), + nbtlib.Compound(DataVersion=nbtlib.Int(4903), data=nbtlib.String("wrong"))): + self.save(path, tag) + with self.subTest(tag=tag), self.assertRaises(ValueError): + layout.read_worldgen(self.world) + + def test_singleplayer_uuid_is_explicit_authority_and_missing_is_not_invented(self): + self.level() + self.assertIsNone(layout.singleplayer_uuid(self.world)) + self.level(singleplayer_uuid=self.words()) + self.assertEqual(self.identity, layout.singleplayer_uuid(self.world)) + + def test_malformed_or_embedded_player_authority_is_rejected(self): + for value in (nbtlib.String(str(self.identity)), nbtlib.IntArray([1, 2]), + nbtlib.List[nbtlib.Int]([1, 2, 3, 4])): + self.level(singleplayer_uuid=value) + with self.subTest(value=value), self.assertRaises(ValueError): + layout.singleplayer_uuid(self.world) + for extra in ({"Player": nbtlib.Compound(UUID=self.words())}, + {"singleplayer_uuid": self.words(), + "Player": nbtlib.Compound(UUID=self.words(uuid.uuid4()))}): + self.level(**extra) + with self.assertRaises(ValueError): + layout.singleplayer_uuid(self.world) + + def test_player_file_keeps_all_unknown_fields_and_checks_identity(self): + tag = nbtlib.Compound(DataVersion=nbtlib.Int(4903), UUID=self.words(), + Dimension=nbtlib.String("test:deep/caverns"), + Unknown=nbtlib.Compound(opaque=nbtlib.LongArray([1, -2]))) + path = layout.player_file(self.world, self.identity) + self.save(path, tag) + expected = copy.deepcopy(tag) + self.assertEqual(expected.snbt(), layout.read_player(self.world, self.identity).snbt()) + for changed in (dict(UUID=self.words(uuid.uuid4())), dict(DataVersion=nbtlib.Int(4671))): + self.save(path, nbtlib.Compound({**tag, **changed})) + with self.assertRaises(ValueError): + layout.read_player(self.world, self.identity) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_legacy_alternatives.py b/test-rig/migration/test_native262_legacy_alternatives.py new file mode 100644 index 00000000..88c383c0 --- /dev/null +++ b/test-rig/migration/test_native262_legacy_alternatives.py @@ -0,0 +1,91 @@ +import copy +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import native262_legacy_alternatives as current +import native262_retained_worldgen as retained +import native262_worldgen as old + + +class VisibleAlternativeContractTest(unittest.TestCase): + def fixture(self, root): + expected = dict(ores={}, fluid_deposits={}) + files = [] + for old_owner, owner, blocks in (("poweradvantage", "basepower", ["crude_oil"]), + ("electricadvantage", "baseelectric", ["li_ore", "sulfur_ore"])): + document = dict(dimensions=[dict(dimension="+", ores=[dict(blockID=old_owner + ":" + block, + minHeight=4, maxHeight=32, frequency=0.375, size=8, variation=2, biomes=[]) for block in blocks])]) + source = root / (old_owner + ".json") + source.write_text(json.dumps(document), encoding="utf-8") + files.append((source, Path("config/orespawn") / source.name)) + expected["ores"].update(retained.os1_profile(document, owner, old_owner)["ores"]) + result = copy.deepcopy(expected) + result["schema_version"] = 7 + for target in current.contract_spec()["replacesOnly"]: + dimension = dict(enabled=True, min_y=1, max_y=31, frequency=0.125, + host_blocks=["minecraft:stone"], host_tags=["forge:stone"], + min_quantity=6, max_quantity=9, pattern="default", placement_channel="orespawn:standard", + min_radius=5, max_radius=12, min_vertical_radius=2, max_vertical_radius=5) + result[target["section"]][target["entry"]] = dict(block=target["block"], enabled=False, + retrogen=False, dimensions={"minecraft:overworld": dimension}) + spec = dict(configFixtures=files) + assertions = [dict(id=row["id"], section=row["section"], entry=row["entry"], + **current.contract_spec()["replacement"]) for row in current.contract_spec()["replacesOnly"]] + return result, spec, assertions + + def test_only_three_explicit_absence_assertions_change_and_history_stays_unchanged(self): + before = old.campaign_spec("legacy-import") + changed = current.campaign_spec("legacy-import") + targets = {row["id"] for row in current.contract_spec()["replacesOnly"]} + expected = copy.deepcopy(before) + for phase in ("first", "restart"): + for row in expected[phase + "ProfileAssertions"]: + if row["id"] in targets: + row.update(current.contract_spec()["replacement"]) + self.assertEqual(expected, changed) + self.assertEqual(before, old.campaign_spec("legacy-import")) + self.assertIs(current.worldgen.validate_restart, old.validate_restart) + with self.assertRaises(AssertionError): current.campaign_spec("advantage") + + def test_complete_disabled_alternatives_and_all_raw_os1_settings_pass(self): + with tempfile.TemporaryDirectory() as directory: + document, spec, assertions = self.fixture(Path(directory)) + with patch.object(current, "campaign_spec", return_value=spec): + rows = current.validate_profile(document, assertions) + self.assertEqual("PASS", rows[-1]["status"]) + + def test_enabled_missing_or_incomplete_alternatives_are_rejected(self): + with tempfile.TemporaryDirectory() as directory: + original, spec, assertions = self.fixture(Path(directory)) + for mutation in ("enabled", "missing", "empty", "channel", "host", "geometry"): + document = copy.deepcopy(original) + rule = document["ores"]["baseelectric:ore/lithium"] + if mutation == "enabled": rule["enabled"] = True + elif mutation == "missing": del document["ores"]["baseelectric:ore/lithium"] + elif mutation == "empty": rule["dimensions"] = {} + else: + dimension = rule["dimensions"]["minecraft:overworld"] + dimension.pop({"channel": "placement_channel", "host": "host_blocks", "geometry": "max_y"}[mutation]) + with self.subTest(mutation=mutation), patch.object(current, "campaign_spec", return_value=spec): + with self.assertRaises(AssertionError): current.validate_profile(document, assertions) + + def test_changed_legacy_rates_or_a_new_active_duplicate_cannot_hide_behind_disabled_guards(self): + with tempfile.TemporaryDirectory() as directory: + original, spec, assertions = self.fixture(Path(directory)) + for mutation in ("rate", "duplicate", "disabled", "output"): + document = copy.deepcopy(original) + key = "baseelectric:legacy/baseelectric_li_ore_0" + rule = document["ores"][key] + if mutation == "rate": rule["dimension_selectors"]["orespawn:all_except_nether_end"]["frequency"] = 99 + elif mutation == "duplicate": document["ores"]["pack:unapproved_lithium"] = copy.deepcopy(rule) + elif mutation == "disabled": rule["enabled"] = False + else: rule["block"] = "baseelectric:unknown_block" + with self.subTest(mutation=mutation), patch.object(current, "campaign_spec", return_value=spec): + with self.assertRaises(AssertionError): current.validate_profile(document, assertions) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_machines.py b/test-rig/migration/test_native262_machines.py new file mode 100644 index 00000000..f7c1fdfc --- /dev/null +++ b/test-rig/migration/test_native262_machines.py @@ -0,0 +1,56 @@ +from pathlib import Path +import tempfile +import unittest + +import lifecycle +from native117_identity import fixture_ids +from native117_lifecycle import NativeLifecycleRun, exercise +import native1201_machines +import native262_machines as machines + + +class NativeMachineAdapterTest(unittest.TestCase): + def test_harvest_reuses_the_old_oracle_without_mutating_its_module(self): + self.assertIs(machines.harvest.__globals__["native1201"].Server, machines.Server) + self.assertEqual(machines.harvest.__globals__["native1201"].MINECRAFT, machines.native.MINECRAFT) + self.assertIsNot(native1201_machines.harvest.__globals__["native1201"], machines.native) + self.assertIs(machines.harvest.__globals__["require_checks"], + native1201_machines.harvest.__globals__["require_checks"]) + self.assertIs(machines.harvest.__globals__["require_isolation"], + native1201_machines.harvest.__globals__["require_isolation"]) + + def test_lifecycle_keeps_all_twelve_moves_and_three_processing_networks(self): + self.assertTrue(issubclass(machines.LifecycleRun, NativeLifecycleRun)) + self.assertIs(machines.exercise, exercise) + rows = fixture_ids(lifecycle.fixtures()) + self.assertEqual(15, len(rows)) + for prefix in ("S-MOVE-", "E-MOVE-"): + selected = [row for row in rows if row["id"].startswith(prefix)] + self.assertEqual(6, len(selected)) + self.assertEqual({"down", "up", "north", "south", "west", "east"}, + {row["facing"] for row in selected}) + self.assertEqual({"C-STEAM", "C-ELECTRIC", "C-FLUID"}, {row["id"] for row in rows if "from" not in row}) + + def test_boot_identity_rejects_historical_engines_and_missing_native_markers(self): + with tempfile.TemporaryDirectory() as directory: + log = Path(directory) / "console.log" + valid = ("Starting minecraft server version 26.2\n" + "Forge mod loading, version 65.1.0, for MC 26.2 with MCP 20260616.103818\n") + log.write_text(valid, encoding="utf-8") + machines.native_boot(log) + for wrong in (valid.replace("26.2", "1.21.11"), valid.replace("65.1.0", "64.0.9"), + "Starting minecraft server version 26.2\n", + valid + "[00:00:00] [main/ERROR] Resource failure\n"): + log.write_text(wrong, encoding="utf-8") + with self.assertRaises(AssertionError): + machines.native_boot(log) + + def test_native_lifecycle_configuration_does_not_redirect_to_old_java_or_launcher(self): + self.assertEqual("26.2", machines.LifecycleRun.minecraft) + self.assertEqual(11, machines.LifecycleRun.max_ticket_distance) + self.assertEqual("25.0.3+9", machines.native.JAVA_VERSION) + self.assertIn("26.2-65.1.0", machines.native.LAUNCHER) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_networks.py b/test-rig/migration/test_native262_networks.py new file mode 100644 index 00000000..a4f6c68a --- /dev/null +++ b/test-rig/migration/test_native262_networks.py @@ -0,0 +1,159 @@ +import copy +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import nbtlib + +from matrix import digest, fingerprint +from native117_geometry import require_geometry +from native117_partial import exercise_bridge, exercise_partial +from native117_ticket_networks import NativeTicketExtendedRun +import native117_extended +import native262_networks as networks + + +class NativeNetworkAdapterTest(unittest.TestCase): + def test_native_sdk_and_historical_oracles_remain_explicit(self): + self.assertEqual(("26.2", "65.1.0", "20260616.103818", "25.0.3+9"), + (networks.native.MINECRAFT, networks.native.FORGE, + networks.native.MCP, networks.native.JAVA_VERSION)) + self.assertEqual(11, networks.MAX_TICKET_DISTANCE) + self.assertEqual(13, networks.HOLDER_RADIUS) + self.assertTrue(issubclass(networks.PartialRun, NativeTicketExtendedRun)) + self.assertIs(networks.CAMPAIGNS["partial"][1], exercise_partial) + self.assertIs(networks.CAMPAIGNS["bridge"][1], exercise_bridge) + self.assertIs(networks.CAMPAIGNS["extended"][1], native117_extended.exercise) + + def test_live_geometry_cannot_accept_an_old_radius_or_unobserved_forced_ticket(self): + original = dict(ticketGeometry=dict(fullLevel=33, entityTickingLevel=31, maxDistance=11, + lastHolderLevel=44, holderRadius=13, observedForcedLevels=[31])) + require_geometry(original, forced=True, max_distance=networks.MAX_TICKET_DISTANCE) + for key, value in (("maxDistance", 12), ("lastHolderLevel", 45), + ("holderRadius", 12), ("observedForcedLevels", []), + ("observedForcedLevels", [32])): + wrong = copy.deepcopy(original) + wrong["ticketGeometry"][key] = value + with self.assertRaises(AssertionError): + require_geometry(wrong, forced=True, max_distance=networks.MAX_TICKET_DISTANCE) + + def test_three_network_types_are_outside_each_others_loaded_holder_apron(self): + linear = networks.linear_fixtures(networks.HOLDER_RADIUS) + bridges = networks.bridge_fixtures(networks.HOLDER_RADIUS) + self.assertEqual({"C-STEAM", "C-ELECTRIC", "C-FLUID"}, {row["id"] for row in linear}) + self.assertEqual(3, len(bridges)) + for rows, left, right in ((linear, "supply", "machine"), (bridges, "bridge", "endpoints")): + for row in rows: + a, b = row["groups"][left][0], row["groups"][right][0] + self.assertGreater(max(abs(a[0] - b[0]), abs(a[1] - b[1])), 2 * networks.HOLDER_RADIUS) + + def test_extended_retains_six_facings_vertical_routes_and_junctions_for_both_drills(self): + rows = networks.extended_fixtures(networks.HOLDER_RADIUS) + self.assertEqual(21, len(rows)) + faces = {"down", "up", "north", "south", "west", "east"} + for prefix in ("S-EXT-", "E-EXT-"): + routes = [row for row in rows if row["id"].startswith(prefix)] + self.assertEqual(9, len(routes)) + self.assertEqual(faces, {row["facing"] for row in routes}) + self.assertTrue({prefix + suffix for suffix in ("vertical-up", "vertical-down", "junction")} + <= {row["id"] for row in routes}) + + def test_item_command_adapter_preserves_all_typed_custom_fields(self): + old = 'minecraft:dirt{AdvWorksWitness:"unit",Extra:{kind:5s}}' + converted = networks.native_inventory_item(old) + self.assertTrue(converted.startswith("minecraft:dirt[minecraft:custom_data=")) + self.assertEqual(nbtlib.parse_nbt(old.partition("{")[1] + old.partition("{")[2]), + nbtlib.parse_nbt(converted.partition("=")[2][:-1])) + self.assertEqual("minecraft:coal", networks.native_inventory_item("minecraft:coal")) + with self.assertRaises(ValueError): + networks.native_inventory_item('minecraft:dirt[minecraft:custom_data={A:1}]') + + def test_derived_inventory_view_preserves_components_unknown_fields_and_original(self): + original = dict(nbt='{Items:[{id:"minecraft:dirt",count:7,components:{' + '"minecraft:custom_data":{AdvWorksWitness:"unit",Typed:2b},' + '"future:payload":{Value:9L}}}],Outside:3s}', extra=[dict(value=4)]) + frozen = copy.deepcopy(original) + derived = networks.compatibility_view(original) + self.assertEqual(frozen, original) + self.assertIsNot(derived, original) + raw, converted = nbtlib.parse_nbt(original["nbt"]), nbtlib.parse_nbt(derived["nbt"]) + self.assertEqual(raw["Outside"], converted["Outside"]) + self.assertEqual(raw["Items"][0]["components"], converted["Items"][0]["components"]) + self.assertEqual(raw["Items"][0]["count"], converted["Items"][0]["Count"]) + self.assertEqual(raw["Items"][0]["components"]["minecraft:custom_data"], converted["Items"][0]["tag"]) + + def test_native_inventory_rejects_legacy_ambiguous_and_badly_typed_stacks(self): + invalid = ['{Items:{}}', '{Items:[{count:1}]}', '{Items:[{id:"minecraft:dirt",Count:1b}]}', + '{Items:[{id:"minecraft:dirt",count:1,Count:1b}]}', + '{Items:[{id:"minecraft:dirt",count:1,tag:{}}]}', + '{Items:[{id:"minecraft:dirt",count:1,components:[]}]}', + '{Items:[{id:"minecraft:dirt",count:1,components:{"minecraft:custom_data":1}}]}'] + invalid += ['{Items:[{id:"minecraft:dirt",count:' + value + '}]}' + for value in ("0", "-1", "1b", "1s", "1L", "1.0f", '"1"')] + for nbt in invalid: + with self.subTest(nbt=nbt), self.assertRaises(AssertionError): + networks.compatibility_view(dict(nbt=nbt)) + + def test_native_snapshot_identity_and_clock_cannot_be_coerced(self): + good = dict(minecraft="26.2", forge="65.1.0", station="lifecycle", worldTime=0) + self.assertIs(good, networks.validate_snapshot(good)) + for key, value in (("minecraft", "1.21.11"), ("forge", "64.0.9"), ("station", "works"), + ("worldTime", True), ("worldTime", -1), ("worldTime", "0")): + wrong = dict(good, **{key: value}) + with self.assertRaises(AssertionError): + networks.validate_snapshot(wrong) + + def test_custody_rejects_changed_seed_and_oracle_input(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + seed = root / "seed" + seed.mkdir() + (seed / "library").write_bytes(b"sealed seed") + source = root / "oracle.py" + source.write_bytes(b"sealed oracle") + hashes = networks.oracle_closure() + hashes[source] = digest(source) + inputs = networks.Inputs((seed, {}, {}), root / "java", "unit", root / "output", hashes, fingerprint(seed)) + inputs.verify(False) + source.write_bytes(b"changed oracle") + with self.assertRaises(AssertionError): + inputs.verify(False) + source.write_bytes(b"sealed oracle") + (seed / "library").write_bytes(b"changed seed") + with self.assertRaises(AssertionError): + inputs.verify(False) + + def test_preflight_rejects_inherited_jvm_options_before_any_world_work(self): + from types import SimpleNamespace + for key in ("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS", "CLASSPATH"): + with patch.dict(networks.native.os.environ, {key: "unexpected"}), self.assertRaises(ValueError): + networks.prepare_inputs(SimpleNamespace()) + + def test_explicit_cpu_cap_preserves_the_real_pinned_command(self): + original = ["actual-java.exe", "-Xms512M", "-Xmx2G", "@native-win-args.txt", "nogui"] + actual = networks.capped_command(original) + self.assertEqual([original[0], "-XX:ActiveProcessorCount=4", *original[1:]], actual) + self.assertNotIn("-XX:ActiveProcessorCount=4", original) + self.assertEqual(1, actual.count("-XX:ActiveProcessorCount=4")) + for command in ([], ["java", "-version"], ["java", "-Xmx3G", "@args"], + ["java", "-Xmx2G", "@args", "-XX:ActiveProcessorCount=8"]): + with self.assertRaises(ValueError): + networks.capped_command(command) + + def test_finite_server_retains_the_actual_core_mro_and_local_constructor_binding(self): + self.assertTrue(issubclass(networks.Server, networks.native.Server)) + mro = networks.Server.__mro__ + self.assertLess(mro.index(networks.native.Server), mro.index(networks._CappedLaunch)) + self.assertLess(mro.index(networks._CappedLaunch), mro.index(networks.native.NativeLaunch)) + self.assertIs(networks.native.NativeLaunch.__init__.__globals__["subprocess"], networks.subprocess) + + def test_even_empty_inherited_options_are_rejected(self): + from types import SimpleNamespace + for key in ("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS", "CLASSPATH"): + with patch.dict(networks.native.os.environ, {key: ""}), self.assertRaises(ValueError): + networks.prepare_inputs(SimpleNamespace()) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_oil.py b/test-rig/migration/test_native262_oil.py new file mode 100644 index 00000000..0a9f42ea --- /dev/null +++ b/test-rig/migration/test_native262_oil.py @@ -0,0 +1,162 @@ +import copy +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from matrix import digest +from native117_oil import construction_commands +import native262_oil as oil +from test_native262_resources import level_fixture + + +def in_world_fixture(root): + world, _ = level_fixture(root, "unit-oil-world") + record = dict(file="unit-oil-world/level.dat", sha256=digest(world / "level.dat")) + + def pixels(name, broken=False): + a, b = root / (name + "-actual.rgba"), root / (name + "-reference.rgba") + a.write_bytes(bytes((1, 2, 3, 255)) * 4) + b.write_bytes(bytes((10 if broken else 1, 2, 3, 255)) * 4) + return dict(width=2, height=2, actualRgba=dict(file=a.name, sha256=digest(a)), + referenceRgba=dict(file=b.name, sha256=digest(b))) + + phases = [] + for index, phase in enumerate(("initial", "after-resource-reload")): + rows = [] + for number, (name, flowing) in enumerate((name, flowing) for name in oil.OILS for flowing in (False, True)): + sprite = "basepower:blocks/" + name + ("_flow" if flowing else "_still") + rows.append(dict(oil=name, flowing=flowing, worldBlock="basepower:" + name, modelLayer="TRANSLUCENT", + missingModel=False, modelSprite=sprite, atlasSprite=sprite, + modelLookup="IClientFluidTypeExtensions.getModel", modelSpriteIdentity=number + 1 + index * 100, + atlasSpriteIdentity=number + 1 + index * 100, modelIdentity=number + 10 + index * 100, + vertices=4, pos=[number, 80, 0], worldTime=100, pixels=pixels(phase + str(number)))) + phases.append(dict(phase=phase, atlasGeneration=index, reloadCompleted=bool(index), cases=rows)) + return dict(protocol="NATIVE65_IN_WORLD_OIL_ATLAS_V1", minecraft="26.2", forge="65.1.0", + profileSha256="A" * 64, worldLoaded=True, dimension="minecraft:overworld", syntheticWorld=False, + stoppedLevel=record, phases=phases, + negativeControls={key: pixels(key, True) for key in ("missing-model", "wrong-sprite", "stale-atlas")}) + + +class OilAdapterTest(unittest.TestCase): + def test_only_the_four_power_lanes_are_defined_without_a_new_mineralogy_pin(self): + definition = oil.fixtures({"jars": []}) + self.assertEqual(4, len(definition)) + self.assertEqual({"basepower:crude_oil", "basepower:refined_oil"}, {row["fluid"] for row in definition}) + self.assertEqual(oil.native.LAUNCHER, oil.OilRun.expected_launcher) + + def test_missing_tank_or_wrong_identity_cannot_be_defaulted_to_empty(self): + definition = oil.fixtures({"jars": []}) + original = dict(minecraft="26.2", forge="65.1.0", station="lifecycle", worldTime=0, cases={ + lane["id"]: [dict(pos=pos, loaded=True, nbt="{Tank:{Empty:1b}}") for pos in lane["witnesses"]] + for lane in definition}) + self.assertIs(original, oil.validate_snapshot(original, definition)) + for text in ("{}", '{Tank:{FluidName:"minecraft:water",Amount:1000}}', + '{Tank:{FluidName:"basepower:crude_oil",Amount:-1}}', '{Tank:{Empty:1}}', + '{Tank:{Empty:0b}}', '{Tank:{Empty:1b,Amount:0}}'): + wrong = copy.deepcopy(original) + lane = definition[0] + next(row for row in wrong["cases"][lane["id"]] if row["pos"] == lane["drain"])["nbt"] = text + with self.assertRaises((AssertionError, KeyError)): + oil.validate_snapshot(wrong, definition) + wrong = copy.deepcopy(original) + wrong["minecraft"] = "1.21.11" + with self.assertRaises(AssertionError): + oil.validate_snapshot(wrong, definition) + + def test_filled_oil_requires_native_int_amount_and_exact_fluid_payload(self): + definition = oil.fixtures({"jars": []}) + original = dict(minecraft="26.2", forge="65.1.0", station="lifecycle", worldTime=1, cases={ + lane["id"]: [dict(pos=pos, loaded=True, nbt='{Tank:{Amount:1000,FluidName:"' + lane["fluid"] + '"}}') + for pos in lane["witnesses"]] for lane in definition}) + self.assertIs(original, oil.validate_snapshot(original, definition)) + for amount in ("1000s", "1000L", "1000.0f", "1000.0d", '"1000"'): + wrong = copy.deepcopy(original) + row = wrong["cases"][definition[0]["id"]][1] + row["nbt"] = row["nbt"].replace("Amount:1000", "Amount:" + amount) + with self.assertRaises(AssertionError): + oil.validate_snapshot(wrong, definition) + + def test_only_exact_first_preconstruction_load_accepts_loaded_air(self): + definition = oil.fixtures({"jars": []}) + original = dict(minecraft="26.2", forge="65.1.0", station="lifecycle", worldTime=1, cases={ + lane["id"]: [dict(pos=pos, loaded=True, block="minecraft:air", state="Block{minecraft:air}") + for pos in lane["witnesses"]] for lane in definition}) + runner = object.__new__(oil.OilRun) + runner.definition = definition + runner.boots = ["oil-first-fill"] + runner.pending_construction = construction_commands(definition) + runner.construction_sealed = False + with patch.object(oil.NativeOilRun, "probe", return_value=original): + self.assertIs(original, runner.probe("load", "oil-loaded")) + for operation, label in (("snapshot", "oil-loaded"), ("load", "oil-restart-loaded")): + with self.assertRaises((AssertionError, KeyError)): + runner.probe(operation, label) + runner.pending_construction.pop(0) + with self.assertRaises((AssertionError, KeyError)): + runner.probe("load", "oil-loaded") + runner.pending_construction = construction_commands(definition) + for change in (lambda row: row.update(loaded=False), lambda row: row.update(block="basepower:fluid_drain"), + lambda row: row.update(state="Block{minecraft:stone}"), lambda row: row.update(nbt="{}")): + wrong = copy.deepcopy(original) + change(wrong["cases"][definition[0]["id"]][0]) + with patch.object(oil.NativeOilRun, "probe", return_value=wrong): + with self.assertRaises((AssertionError, KeyError)): + runner.probe("load", "oil-loaded") + + def test_worldless_oil_gauges_cannot_claim_actual_world_rendering(self): + with tempfile.TemporaryDirectory() as directory: + with self.assertRaises((AssertionError, KeyError)): + oil.validate_in_world_render(dict(tank_gauge_pixel_status="PASS", fluid_rendering=[]), Path(directory), "A" * 64) + + def test_actual_world_schema_requires_exact_native_model_and_reload_controls(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + original = in_world_fixture(root) + before = copy.deepcopy(original) + result = oil.validate_in_world_render(original, root, "A" * 64) + self.assertEqual(8, result["cases"]) + self.assertEqual(before, original) + changes = ( + lambda doc: doc.update(worldLoaded=False), + lambda doc: doc.update(syntheticWorld=True), + lambda doc: doc.update(profileSha256="B" * 64), + lambda doc: doc["phases"][1].update(reloadCompleted=False), + lambda doc: doc["phases"][1].update(atlasGeneration=0), + lambda doc: doc["phases"][1]["cases"][0].update(modelIdentity=10), + lambda doc: doc["phases"][1]["cases"][0].update(modelSpriteIdentity=1, atlasSpriteIdentity=1), + lambda doc: doc["phases"][0]["cases"][0].update(modelLayer="SOLID"), + lambda doc: doc["phases"][0]["cases"][0].update(missingModel=True), + lambda doc: doc["phases"][0]["cases"][0].update(modelSprite="minecraft:block/water_still"), + lambda doc: doc["phases"][0]["cases"][0].update(atlasSpriteIdentity=999), + lambda doc: doc["negativeControls"].pop("stale-atlas"), + lambda doc: doc["phases"][1]["cases"].pop(), + ) + for change in changes: + wrong = copy.deepcopy(original) + change(wrong) + with self.assertRaises((AssertionError, KeyError)): + oil.validate_in_world_render(wrong, root, "A" * 64) + + def test_pixel_mismatch_and_passing_broken_control_are_rejected_from_raw_bytes(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + document = in_world_fixture(root) + row = document["phases"][0]["cases"][0]["pixels"] + wrong = root / row["actualRgba"]["file"] + wrong.write_bytes(bytes((4, 5, 6, 255)) * 4) + row["actualRgba"]["sha256"] = digest(wrong) + with self.assertRaisesRegex(AssertionError, "pixel comparison"): + oil.validate_in_world_render(document, root, "A" * 64) + document = in_world_fixture(root) + broken = document["negativeControls"]["missing-model"] + a = root / broken["actualRgba"]["file"] + b = root / broken["referenceRgba"]["file"] + b.write_bytes(a.read_bytes()) + broken["referenceRgba"]["sha256"] = digest(b) + with self.assertRaisesRegex(AssertionError, "pixel comparison"): + oil.validate_in_world_render(document, root, "A" * 64) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_operations.py b/test-rig/migration/test_native262_operations.py new file mode 100644 index 00000000..d17f56a9 --- /dev/null +++ b/test-rig/migration/test_native262_operations.py @@ -0,0 +1,86 @@ +import copy +import io +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import nbtlib + +from matrix import WORLD +import native262_operations as native +import test_native12111_operations_actor_load as retained_controls +from native262_layout import region_file + + +class Native262ExpectedActorLoadTest(retained_controls.NativeExpectedActorLoadTest): + def compare(self, fixture, inspected): + original = copy.deepcopy((fixture, inspected)) + try: + return native.validate_restart(fixture, inspected) + finally: + self.assertEqual(original, (fixture, inspected)) + + def test_selected_native_sources_are_exact(self): + self.assertEqual(2, len(native.native_load_sources())) + + +class Native262StoppedMachineTest(unittest.TestCase): + def test_full_job_count_cannot_be_a_smaller_passing_catalog(self): + for count in (0, 131, 133): + with self.subTest(count=count), patch.object(native, "frozen_validate_operation", return_value=dict(status="PASS", cases=count)): + with self.assertRaisesRegex(AssertionError, "132"): + native.validate_operation({}, {}, "all") + with patch.object(native, "frozen_validate_operation", return_value=dict(status="FAIL", cases=132)): + self.assertEqual("FAIL", native.validate_operation({}, {}, "all")["status"]) + + def payload(self, version=4903): + tile = nbtlib.Compound(id=nbtlib.String("basepower:fluidtank"), x=nbtlib.Int(-1), + y=nbtlib.Int(80), z=nbtlib.Int(-1), keepPacked=nbtlib.Byte(0), + unknown=nbtlib.Compound(ordered=nbtlib.IntArray([3, 2, 1]))) + root = nbtlib.File(dict(DataVersion=nbtlib.Int(version), xPos=nbtlib.Int(-1), zPos=nbtlib.Int(-1), + sections=nbtlib.List[nbtlib.Compound]([nbtlib.Compound(Y=nbtlib.Byte(5), block_states=nbtlib.Compound( + palette=nbtlib.List[nbtlib.Compound]([nbtlib.Compound(Name=nbtlib.String("basepower:fluidtank"))])))]), + block_entities=nbtlib.List[nbtlib.Compound]([tile]), opaque=nbtlib.IntArray([9, 8, 7]))) + buffer = io.BytesIO() + root.write(buffer) + body = b"\x03" + buffer.getvalue() + return len(body).to_bytes(4, "big") + body, tile + + def test_old_data_version_and_wrong_type_are_not_coerced(self): + with self.assertRaises(ValueError): + native.modern_chunk(self.payload(4671)[0]) + payload, _ = self.payload() + native.modern_chunk(payload) + with self.assertRaises(ValueError): + native.modern_chunk(payload[:-1]) + + def test_all_dimensions_and_custom_negative_coordinates_use_explicit_paths(self): + for dimension in ("minecraft:overworld", "minecraft:the_nether", "minecraft:the_end", "example:deep/cave"): + with self.subTest(dimension=dimension), tempfile.TemporaryDirectory() as folder: + runtime = Path(folder) / "runtime" + evidence = Path(folder) / "evidence" + region = region_file(runtime / WORLD, dimension, -1, -1) + region.parent.mkdir(parents=True) + region.write_bytes(b"immutable native region fixture") + payload, tile = self.payload() + expected = copy.deepcopy(tile) + del expected["keepPacked"] + row = dict(x=-1, y=80, z=-1, block="basepower:fluidtank", + state="Block{basepower:fluidtank}", nbt=expected.snbt()) + fixture = dict(cases=[dict(id="typed", dimension=dimension, positions=[row])]) + with patch.object(native, "read_region", return_value=(None, {1023: payload})) as read: + result = native.audit_saved(runtime, fixture, evidence) + self.assertEqual("PASS", result["status"]) + read.assert_called_once_with(region) + self.assertEqual(payload, (evidence / "chunk-00000.bin").read_bytes()) + self.assertEqual(b"immutable native region fixture", region.read_bytes()) + changed = copy.deepcopy(fixture) + changed["cases"][0]["positions"][0]["nbt"] = expected.snbt().replace("[I; 3, 2, 1]", "[I; 1, 2, 3]") + self.assertNotEqual(fixture, changed) + with patch.object(native, "read_region", return_value=(None, {1023: payload})): + self.assertEqual("FAIL", native.audit_saved(runtime, changed, evidence.parent / "changed")["status"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_platform_diagnostics.py b/test-rig/migration/test_native262_platform_diagnostics.py new file mode 100644 index 00000000..5505bb44 --- /dev/null +++ b/test-rig/migration/test_native262_platform_diagnostics.py @@ -0,0 +1,90 @@ +from pathlib import Path +import unittest + +import native12111 +import native262_platform_diagnostics as native + + +class Native262PlatformDiagnosticsTest(unittest.TestCase): + @staticmethod + def host_text(rows=None): + rows = rows if rows is not None else native.host_fixture() + return "\n".join("[time] [" + row["thread"] + "/" + row["level"] + "] [" + row["logger"] + "]: " + + row["message"] + "\n" + "\n".join(row["body"]) for row in rows) + + def test_independent_native_host_failure_is_reported_not_silently_removed(self): + for file_appender in (False, True): + failures, diagnostics = native.classify(self.host_text(native.host_fixture(file_appender)), native12111.log_findings) + self.assertFalse(failures) + self.assertEqual(4, len(diagnostics)) + self.assertTrue(all(row["controlReceiptSha256"] == native.HOST_CONTROL_SHA for row in diagnostics)) + self.assertTrue(all(row["rawBlock"] for row in diagnostics)) + + def test_host_matching_never_waives_changed_or_incomplete_stacks(self): + import copy + rows = native.host_fixture() + for slot in range(4): + for key in ("thread", "level", "logger", "message", "body"): + changed = copy.deepcopy(rows) + if key == "body": + changed[slot][key].append("\tat zone.moddev.mc.basepower.Machine.tick(Machine.java:10)") + else: + changed[slot][key] += "changed" + with self.subTest(slot=slot, key=key): + self.assertTrue(native.classify(self.host_text(changed), native12111.log_findings)[0]) + with self.subTest(missing=slot): + self.assertTrue(native.classify(self.host_text(rows[:slot] + rows[slot+1:]), native12111.log_findings)[0]) + for suffix in ("\n[time] [Server thread/ERROR] [basepower/]: machine failed", + "\njava.lang.NullPointerException: unrelated failure"): + self.assertTrue(native.classify(self.host_text() + suffix, native12111.log_findings)[0]) + + def test_current_netty_fallback_requires_exact_sequence_and_successful_cleaner(self): + import copy + rows = native.netty_fixture() + failures, diagnostics = native.classify(self.host_text(rows), native12111.log_findings) + self.assertFalse(failures) + self.assertEqual(2, len(diagnostics)) + for slot in range(3): + for key in ("thread", "level", "logger", "message", "body"): + changed = copy.deepcopy(rows) + if key == "body": + changed[slot][key].append("java.lang.IllegalStateException: real failure") + else: + changed[slot][key] += "changed" + with self.subTest(slot=slot, key=key): + self.assertTrue(native.classify(self.host_text(changed), native12111.log_findings)[0]) + self.assertTrue(native.classify(self.host_text(rows[:slot] + rows[slot+1:]), native12111.log_findings)[0]) + mixed = self.host_text() + "\n" + self.host_text(rows) + self.assertEqual(6, len(native.classify(mixed, native12111.log_findings)[1])) + self.assertTrue(native.classify(mixed + "\njava.lang.IllegalArgumentException: real failure", native12111.log_findings)[0]) + + def test_predecessor_capability_exceptions_are_not_waived(self): + fixture = Path(__file__).with_name("fixtures") / "native2612-client-netty-java25.txt" + text = fixture.read_text(encoding="utf-8") + strict = native12111.log_findings(text) + self.assertTrue(strict[0]) + actual = native.classify(text, native12111.log_findings) + self.assertTrue(set(strict[0]) <= set(actual[0])) + self.assertEqual([], actual[1]) + + def test_new_runtime_failures_are_not_waived(self): + for text in ( + "[now] [Server thread/ERROR] [basepower/]: machine failure", + "java.lang.NoClassDefFoundError: MissingMachine", + "java.lang.UnsupportedOperationException: native access unavailable", + ): + with self.subTest(text=text): + self.assertTrue(native.classify(text, native12111.log_findings)[0]) + + def test_classifier_keeps_all_original_text_and_results(self): + seen = [] + expected = (["unknown failure"], [{"raw": "unaltered"}]) + def fallback(text): + seen.append(text) + return expected + self.assertIs(expected, native.classify("full raw log", fallback)) + self.assertEqual(["full raw log"], seen) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_resources.py b/test-rig/migration/test_native262_resources.py new file mode 100644 index 00000000..c7f04978 --- /dev/null +++ b/test-rig/migration/test_native262_resources.py @@ -0,0 +1,144 @@ +"""Synthetic adapter controls, never substitute campaign receipts.""" + +import copy +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import nbtlib + +from matrix import WORLD, digest +import native117_recipes as old +import native262_resources as resources + + +def level_fixture(root, world=WORLD): + target = Path(root) / world + target.mkdir(parents=True, exist_ok=True) + versions = {**resources.native.VERSIONS, "forge": "65.1.0", "minecraft": "26.2"} + data = nbtlib.Compound(DataVersion=nbtlib.Int(4903), LevelName=nbtlib.String(world), + Version=nbtlib.Compound(Name=nbtlib.String("26.2"))) + entries = nbtlib.List[nbtlib.Compound]([nbtlib.Compound(ModId=nbtlib.String(key), ModVersion=nbtlib.String(value)) + for key, value in versions.items()]) + registry = nbtlib.Compound(ids=nbtlib.List[nbtlib.Compound]([ + nbtlib.Compound(K=nbtlib.String("minecraft:stone"), V=nbtlib.Int(1))])) + result = nbtlib.File({"Data": data, "fml": nbtlib.Compound(LoadingModList=entries, Registries=nbtlib.Compound({ + "minecraft:item": copy.deepcopy(registry), "minecraft:block": copy.deepcopy(registry)}))}) + result.save(target / "level.dat", gzipped=True) + return target, result + + +class ResourcesAdapterTest(unittest.TestCase): + def test_native_loader_errors_abort_waits_without_becoming_allowed_diagnostics(self): + for line in ("[00:00:00] [modloading-worker-0/ERROR] [FML/]: Caught exception during event FMLCommonSetupEvent", + "[00:00:00] [main/FATAL] [FML/]: Failed to complete lifecycle event COMMON_SETUP", + "[00:00:00] [main/ERROR] [minecraft/Main]: Failed to start the minecraft server"): + self.assertTrue(resources.RecipeServer.unexpected_line(line)) + self.assertFalse(resources.RecipeServer.unexpected_line("[00:00:00] [main/DEBUG] [unit/]: Caught exception during event control")) + self.assertFalse(resources.RecipeServer.unexpected_line("Common setup complete")) + + def test_exact_engine_metadata_and_read_only_bindings(self): + self.assertEqual(("26.2", "65.1.0"), resources.Engine.version) + self.assertEqual({"min_format": [107, 1], "max_format": [107, 1], + "description": "Disposable loot-override witness"}, resources.Engine.pack_metadata) + self.assertIs(resources.unlock_campaign.__code__, old.unlock_campaign.__code__) + self.assertIs(resources.progression_campaign.__code__, old.progression_campaign.__code__) + self.assertIs(resources.unlock_campaign.__globals__["verify_level"], resources.verify_saved_level) + self.assertIs(old.unlock_campaign.__globals__["verify_level"], old.verify_level) + self.assertEqual({"engine": None}, resources.progression_campaign.__kwdefaults__) + + def test_new_engine_selects_native_pack_codec_without_changing_old_metadata(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def stage(runtime, *_): + runtime.mkdir() + return "NOT_RUN" + + with patch.object(resources.Engine, "stage", stage), patch.dict(resources.progression_campaign.__globals__, MODES=()): + resources.progression_campaign(root, None, None, None, 26520, {}, engine=resources.Engine) + pack = json.loads((root / "progression" / WORLD / "datapacks/progression-override/pack.mcmeta").read_text()) + self.assertEqual(resources.PACK_METADATA, pack["pack"]) + self.assertNotIn("pack_format", pack["pack"]) + self.assertEqual(("NORMAL", "TECH_PROGRESSION", "APOCALYPTIC"), old.MODES) + + def test_native_stopped_level_checks_exact_all_six_mods_and_registry_maps(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + world, saved = level_fixture(root) + entry = {"jars": [dict(id=key, version=value) for key, value in resources.native.VERSIONS.items()]} + result = resources.verify_saved_level(root, entry) + self.assertEqual(4903, result["dataVersion"]) + self.assertEqual(digest(world / "level.dat"), result["sha256"]) + for change in (lambda tag: tag["Data"].update(DataVersion=nbtlib.Int(4671)), + lambda tag: tag["fml"]["LoadingModList"].append(copy.deepcopy(tag["fml"]["LoadingModList"][0])), + lambda tag: tag["fml"]["Registries"].pop("minecraft:item"), + lambda tag: tag["fml"]["LoadingModList"].pop()): + wrong = copy.deepcopy(saved) + change(wrong) + wrong.save(world / "level.dat", gzipped=True) + with self.assertRaises((AssertionError, ValueError, KeyError)): + resources.verify_saved_level(root, entry) + + def test_packet_exports_require_every_mode_and_nonempty_hash_bound_item_tags(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for mode in ("normal", "tech", "apocalyptic"): + (root / f"recipes-{mode}-sweep.bin").write_bytes(b"synthetic-unit-packet") + tags = root / f"tags-{mode}.bin" + tags.write_bytes(b"synthetic-unit-tags") + manifest = dict(bookPacketType="ClientboundRecipeBookAddPacket", tagsPacketType="ClientboundUpdateTagsPacket", + itemTagCount=2, tagsPacketFile=tags.name, tagsPacketSha256=digest(tags)) + (root / f"recipes-{mode}-sweep.json").write_text(json.dumps(manifest)) + self.assertEqual(3, len(resources.verify_packet_exports(root))) + path = root / "recipes-normal-sweep.json" + original = json.loads(path.read_text()) + for key, value in (("tagsPacketFile", "../outside.bin"), ("itemTagCount", True), + ("tagsPacketSha256", "0" * 64), ("bookPacketType", "OldRecipePacket")): + path.write_text(json.dumps({**original, key: value})) + with self.assertRaises((AssertionError, KeyError)): + resources.verify_packet_exports(root) + path.write_text(json.dumps(original)) + (root / "recipes-apocalyptic-sweep.bin").write_bytes(b"") + with self.assertRaises(AssertionError): + resources.verify_packet_exports(root) + + def test_retained_pixels_are_replayed_not_accepted_from_a_pass_label(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + a, b = root / "a.rgba", root / "b.rgba" + a.write_bytes(bytes((1, 2, 3, 255)) * 4) + b.write_bytes(a.read_bytes()) + spec = dict(width=2, height=2, actualRgba=dict(file=a.name, sha256=digest(a)), + referenceRgba=dict(file=b.name, sha256=digest(b))) + self.assertEqual(0, resources.pixel_pair(root, spec)) + with self.assertRaises(AssertionError): + resources.pixel_pair(root, {**spec, "referenceRgba": spec["actualRgba"]}) + b.write_bytes(bytes((10, 2, 3, 255)) * 4) + spec["referenceRgba"]["sha256"] = digest(b) + with self.assertRaises(AssertionError): + resources.pixel_pair(root, spec) + self.assertEqual(4, resources.pixel_pair(root, spec, matches=False)) + spec["width"] = True + with self.assertRaises(AssertionError): + resources.pixel_pair(root, spec) + + def test_artifact_path_escape_hash_and_hardlink_fail_closed(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + path = root / "witness" + path.write_bytes(b"synthetic-unit-artifact") + self.assertEqual(path, resources.artifact(root, dict(file=path.name, sha256=digest(path)))) + for value in (dict(file="../witness", sha256=digest(path)), dict(file=path.name, sha256="0" * 64)): + with self.assertRaises(AssertionError): + resources.artifact(root, value) + linked = root / "hardlink" + linked.hardlink_to(path) + with self.assertRaises(AssertionError): + resources.artifact(root, dict(file=path.name, sha256=digest(path))) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_retained_worldgen.py b/test-rig/migration/test_native262_retained_worldgen.py new file mode 100644 index 00000000..f7c8b0e6 --- /dev/null +++ b/test-rig/migration/test_native262_retained_worldgen.py @@ -0,0 +1,145 @@ +import copy +from pathlib import Path +import tempfile +import unittest + +import native262_retained_worldgen as retained + + +def fixture(): + return dict(manage_vanilla_ores=False, place_fluid_deposits=True, suppress_all_ore_features=False, + history={"note": "electricadvantage:li_ore is literal history"}, ores={ + "electricadvantage:legacy/custom_lithium": dict(block="electricadvantage:li_ore", enabled=True, + source_provider="electricadvantage", opaque={"keep": [1, 2, 3], "block": "electricadvantage:li_ore"}, dimension_selectors={ + "orespawn:all_except_nether_end": dict(enabled=True, min_y=3, max_y=29, + frequency=0.375, min_quantity=5, max_quantity=11, pattern="default", + spread=7, vertical_spread=2, node_size=8, biome_ids=["minecraft:forest"], + excluded_biome_ids=["minecraft:desert"])})}, fluid_deposits={}) + + +class RetainedWorldgenContractTest(unittest.TestCase): + registry = {"baseelectric:li_ore", "baseelectric:sulfur_ore", "basepower:crude_oil", "minecraft:nether_quartz_ore"} + + def fixed(self, source): + result = copy.deepcopy(source) + rule = result["ores"]["electricadvantage:legacy/custom_lithium"] + rule["block"] = "baseelectric:li_ore" + return result + + def test_full_payload_and_rule_id_are_retained_without_rewriting_history(self): + source = fixture() + result = retained.audit_transition(source, self.fixed(source), self.registry) + self.assertEqual("PASS", result["status"]) + self.assertEqual("electricadvantage:li_ore is literal history", source["history"]["note"]) + + def test_provider_identity_and_opaque_block_text_are_not_output_references(self): + source = fixture() + actual = self.fixed(source) + self.assertEqual("PASS", retained.audit_transition(source, actual, self.registry)["status"]) + actual["ores"]["electricadvantage:legacy/custom_lithium"]["source_provider"] = "baseelectric" + self.assertEqual("FAIL", retained.audit_transition(source, actual, self.registry)["status"]) + + def test_zero_rate_saved_rule_cannot_be_reenabled_by_missing_default(self): + source = fixture() + source["ores"]["electricadvantage:legacy/custom_lithium"]["dimension_selectors"]["orespawn:all_except_nether_end"]["frequency"] = 0.0 + actual = self.fixed(source) + actual["ores"]["baseelectric:ore/lithium"] = dict(block="baseelectric:li_ore", enabled=True, + dimensions={"minecraft:overworld": {"enabled": True, "frequency": 0.125}}) + self.assertEqual("FAIL", retained.audit_transition(source, actual, self.registry)["status"]) + + def test_old_unknown_output_is_failure_even_if_valid_fallback_exists(self): + source = fixture() + actual = copy.deepcopy(source) + actual["ores"]["baseelectric:ore/lithium"] = dict(block="baseelectric:li_ore", enabled=True, + dimensions={"minecraft:overworld": {"enabled": True}}) + result = retained.audit_transition(source, actual, self.registry) + self.assertEqual("FAIL", result["status"]) + self.assertTrue(any(row["kind"] == "UNKNOWN_OUTPUT" for row in result["issues"])) + + def test_each_user_bound_geometry_filter_and_enabled_mutation_is_rejected(self): + source = fixture() + for key, value in dict(min_y=4, max_y=30, frequency=0.5, min_quantity=6, max_quantity=12, + spread=8, vertical_spread=4, node_size=4, enabled=False, + biome_ids=[], excluded_biome_ids=[]).items(): + with self.subTest(key=key): + actual = self.fixed(source) + actual["ores"]["electricadvantage:legacy/custom_lithium"]["dimension_selectors"][ + "orespawn:all_except_nether_end"][key] = value + self.assertEqual("FAIL", retained.audit_transition(source, actual, self.registry)["status"]) + + def test_dimension_selector_must_not_be_replaced_by_overworld_fallback(self): + source = fixture() + actual = self.fixed(source) + rule = actual["ores"]["electricadvantage:legacy/custom_lithium"] + rule["dimensions"] = {"minecraft:overworld": rule.pop("dimension_selectors")["orespawn:all_except_nether_end"]} + self.assertEqual("FAIL", retained.audit_transition(source, actual, self.registry)["status"]) + + def test_disabled_saved_rule_does_not_allow_new_enabled_default(self): + source = fixture() + source["ores"]["electricadvantage:legacy/custom_lithium"]["enabled"] = False + actual = self.fixed(source) + actual["ores"]["baseelectric:ore/lithium"] = dict(block="baseelectric:li_ore", enabled=True, + dimensions={"minecraft:overworld": {"enabled": True}}) + result = retained.audit_transition(source, actual, self.registry) + self.assertEqual("FAIL", result["status"]) + self.assertTrue(any(row["kind"] == "NEW_ACTIVE_DEFAULT" for row in result["issues"])) + + def test_enabled_saved_rule_also_rejects_new_duplicate_generation(self): + source = fixture() + actual = self.fixed(source) + actual["ores"]["baseelectric:ore/lithium"] = copy.deepcopy(actual["ores"]["electricadvantage:legacy/custom_lithium"]) + self.assertEqual("FAIL", retained.audit_transition(source, actual, self.registry)["status"]) + + def test_explicit_current_id_collision_is_not_replaced_with_old_default(self): + source = fixture() + source["ores"]["baseelectric:ore/lithium"] = dict(block="baseelectric:li_ore", enabled=False, + dimensions={"minecraft:overworld": dict(enabled=True, min_y=9, max_y=17, frequency=0.875)}) + actual = self.fixed(source) + self.assertEqual("PASS", retained.audit_transition(source, actual, self.registry)["status"]) + actual["ores"]["baseelectric:ore/lithium"]["enabled"] = True + self.assertEqual("FAIL", retained.audit_transition(source, actual, self.registry)["status"]) + + def test_rule_keys_must_not_silently_rename_or_disappear(self): + source = fixture() + actual = self.fixed(source) + actual["ores"]["baseelectric:legacy/custom_lithium"] = actual["ores"].pop("electricadvantage:legacy/custom_lithium") + self.assertEqual("FAIL", retained.audit_transition(source, actual, self.registry)["status"]) + + def test_opaque_rule_payload_and_root_history_cannot_be_dropped(self): + source = fixture() + for remove in ("opaque", "history"): + actual = self.fixed(source) + if remove == "opaque": del actual["ores"]["electricadvantage:legacy/custom_lithium"][remove] + else: del actual[remove] + self.assertEqual("FAIL", retained.audit_transition(source, actual, self.registry)["status"]) + + def test_source_controls_cannot_change_even_with_all_positive_sample_counts(self): + source = fixture() + for key in ("manage_vanilla_ores", "suppress_all_ore_features", "place_fluid_deposits"): + actual = self.fixed(source) + actual[key] = not source[key] + self.assertEqual("FAIL", retained.audit_transition(source, actual, self.registry)["status"]) + + def test_os1_oracle_uses_raw_source_rates_and_original_selector(self): + source = dict(dimensions=[dict(dimension="+", ores=[dict(blockID="electricadvantage:li_ore", + minHeight=3, maxHeight=30, frequency=0.375, size=8, variation=2, biomes=["FOREST"])])]) + expected = retained.os1_profile(source, "baseelectric", "electricadvantage") + row = expected["ores"]["baseelectric:legacy/baseelectric_li_ore_0"] + rule = row["dimension_selectors"]["orespawn:all_except_nether_end"] + self.assertEqual((3, 29, 0.375, 6, 9, 2, 8), tuple(rule[k] for k in + ("min_y", "max_y", "frequency", "min_quantity", "max_quantity", "vertical_spread", "node_size"))) + self.assertEqual(["FOREST"], rule["biome_dictionary"]) + + def test_new_chunk_custody_rejects_any_preexisting_region_file(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + retained.require_new_world(root) + region = root / "dimensions/minecraft/overworld/region/r.0.0.mca" + region.parent.mkdir(parents=True) + region.write_bytes(b"old-region") + with self.assertRaisesRegex(AssertionError, "preexisting"): + retained.require_new_world(root) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_strict_migration.py b/test-rig/migration/test_native262_strict_migration.py new file mode 100644 index 00000000..64251f6f --- /dev/null +++ b/test-rig/migration/test_native262_strict_migration.py @@ -0,0 +1,63 @@ +import copy +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import native262_strict_migration as target + + +class StrictMigrationTest(unittest.TestCase): + def test_every_previous_warning_is_a_failure_even_when_old_log_gate_accepts(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "console.log" + for text in target.REJECT: + path.write_text("[Server thread/WARN] " + text + " 'old:rule'", encoding="utf-8") + with patch.object(target.native, "require_clean_log", return_value=[]): + with self.assertRaisesRegex(AssertionError, "forbidden"): + target.clean_log(path) + + def test_clean_log_retains_native_platform_classifier(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "console.log" + path.write_text("native startup completed", encoding="utf-8") + with patch.object(target.native, "require_clean_log", return_value=["classified"]): + self.assertEqual(["classified"], target.clean_log(path)) + with patch.object(target.native, "require_clean_log", side_effect=AssertionError("native failure")): + with self.assertRaisesRegex(AssertionError, "native failure"): target.clean_log(path) + + def test_world_rates_disabled_choices_rule_ids_and_payloads_remain_authoritative(self): + source = {"place_fluid_deposits": False, "ores": {"pack:saved": {"block": "electricadvantage:li_ore", + "enabled": False, "dimensions": {"minecraft:overworld": {"frequency": 0.25, "private": "unchanged"}}}}} + actual = copy.deepcopy(source) + actual["ores"]["pack:saved"]["block"] = "baseelectric:li_ore" + self.assertEqual("PASS", target.contract(source, actual, {"baseelectric:li_ore"})["status"]) + for change in ("rate", "enable", "root", "rule", "payload"): + modified = copy.deepcopy(actual) + row = modified["ores"]["pack:saved"] + if change == "rate": row["dimensions"]["minecraft:overworld"]["frequency"] = 0.125 + elif change == "enable": row["enabled"] = True + elif change == "root": modified["place_fluid_deposits"] = True + elif change == "payload": row["dimensions"]["minecraft:overworld"]["private"] = "lost" + else: modified["ores"]["pack:renamed"] = modified["ores"].pop("pack:saved") + self.assertEqual("FAIL", target.contract(source, modified, {"baseelectric:li_ore"})["status"], change) + + def test_new_cross_section_active_fallback_cannot_hide_behind_positive_counts(self): + source = {"ores": {"poweradvantage:legacy/oil": {"block": "poweradvantage:crude_oil", "enabled": False}}} + actual = {"ores": {"poweradvantage:legacy/oil": {"block": "basepower:crude_oil", "enabled": False}}, + "fluid_deposits": {"basepower:default": {"block": "basepower:crude_oil", "enabled": True, + "dimensions": {"minecraft:overworld": {"frequency": 1}}}}} + self.assertEqual("FAIL", target.contract(source, actual, {"basepower:crude_oil"})["status"]) + actual["fluid_deposits"]["basepower:default"]["enabled"] = False + self.assertEqual("PASS", target.contract(source, actual, {"basepower:crude_oil"})["status"]) + + def test_unknown_primary_deep_and_weighted_outputs_cannot_pass_even_disabled(self): + for field in ("block", "deep_output", "outputs"): + actual = {"ores": {"test:ore": {"block": "baseelectric:li_ore", "enabled": False}}} + row = actual["ores"]["test:ore"] + row[field] = [{"block": "electricadvantage:li_ore", "weight": 1}] if field == "outputs" else "electricadvantage:li_ore" + self.assertEqual("FAIL", target.contract({}, actual, {"baseelectric:li_ore"})["status"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_transformer_order.py b/test-rig/migration/test_native262_transformer_order.py new file mode 100644 index 00000000..9093b934 --- /dev/null +++ b/test-rig/migration/test_native262_transformer_order.py @@ -0,0 +1,293 @@ +"""Order-proof contracts and strict template adaptation; no Minecraft process is started.""" + +import ast +import copy +import json +import os +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import Mock, patch +from zipfile import ZipFile + +from matrix import digest, fingerprint, write_json +import native262 as native +import native262_transformer_order as probe + + +def woven(reverse=False): + order = ["basemetals", "basepower"] if reverse else ["basepower", "basemetals"] + return dict(status="PASS", scope=probe.SCOPE, qualificationAllowed=False, + minecraft="26.2", forge="65.1.0", dataVersion=4903, upgradeDescriptor=probe.UPGRADE, + selectedWorldAfterLock=True, selectedWorldTarget=probe.SELECTED, rawReadDescriptor=probe.RAW_READ, + allHandlerBodiesMatchPinnedProduction=True, nativeReturnsCovered=2, exceptionEdgesChecked=True, + regionHeads=order[:], regionReturns=[order[:], order[:]], worldHeads=order[:], worldgenHeads=order[:]) + + +class Native262TransformerOrderTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name).resolve() + + def test_only_exact_four_vectors_reverse_and_remain_diagnostic(self): + result = probe.compare_orders(woven(), woven(True)) + self.assertEqual("PASS", result["status"]) + self.assertFalse(result["qualificationAllowed"]) + self.assertEqual(set(probe.VECTORS), set(result["permutations"])) + with self.assertRaisesRegex(AssertionError, "did not reverse"): + probe.compare_orders(woven(), woven()) + + def test_stale_61_abi_layout_and_selected_world_proofs_fail_closed(self): + for key, value in (("minecraft", "1.21.11"), ("forge", "61.1.0"), ("dataVersion", 4671), + ("upgradeDescriptor", probe.UPGRADE.replace(";I)", ";)")), + ("selectedWorldTarget", "net/minecraft/world/level/storage/LevelStorageSource"), + ("selectedWorldAfterLock", False), ("rawReadDescriptor", "old"), + ("qualificationAllowed", True), ("scope", "QUALIFIED"), ("status", "FAIL"), + ("allHandlerBodiesMatchPinnedProduction", False), ("exceptionEdgesChecked", False), + ("nativeReturnsCovered", 1), ("nativeReturnsCovered", 2.0), ("selectedWorldAfterLock", 1)): + actual = woven(True) + actual[key] = value + with self.subTest(key=key, value=value), self.assertRaises(AssertionError): + probe.compare_orders(woven(), actual) + for key in ("minecraft", "upgradeDescriptor", "selectedWorldAfterLock"): + actual = woven() + del actual[key] + with self.assertRaises(AssertionError): + probe.verify_woven(actual) + + def test_missing_duplicate_partial_and_unreversed_callbacks_cannot_pass(self): + for key in probe.VECTORS: + for value in ([], ["basepower"], ["basepower", "basepower"], ["basepower", "unknown"]): + actual = woven(True) + actual[key] = [value, value] if key == "regionReturns" else value + with self.subTest(key=key, value=value), self.assertRaises(AssertionError): + probe.compare_orders(woven(), actual) + actual = woven(True) + actual[key] = copy.deepcopy(woven()[key]) + with self.assertRaises(AssertionError): + probe.compare_orders(woven(), actual) + actual = woven(True) + actual["regionReturns"].pop() + with self.assertRaises(AssertionError): + probe.compare_orders(woven(), actual) + + def test_guarded_template_generation_preserves_templates_and_target_contract(self): + before = fingerprint(probe.TEMPLATES) + target = self.root / "sources" + generated = probe.prepare_sources(target) + self.assertEqual(before, fingerprint(probe.TEMPLATES)) + self.assertEqual(set(probe.TEMPLATE_HASHES), {name.replace("\\", "/") for name in generated}) + audit = (target / "src/zone/moddev/orderprobe/OrderProbeAudit.java").read_text() + self.assertIn(probe.SELECTED, audit) + self.assertIn('"(" + TAG + "I" + TAG + "I)" + TAG', audit) + self.assertIn("SelectedWorldStorageMixin", audit) + self.assertIn("dominates(selected, lockedAt, site)", audit) + self.assertIn("dominates(selected, site, rawAt)", audit) + self.assertIn("nativeSelected.tryCatchBlocks", audit) + self.assertNotIn("readLevelDataTagFixed", audit) + self.assertIn("allHandlerBodiesMatchPinnedProduction", audit) + self.assertIn("native exception-handler coverage changed", audit) + self.assertIn("native65-order-plugin-", (target / "src/zone/moddev/orderprobe/OrderProbePlugin.java").read_text()) + self.assertIn('@net.minecraftforge.fml.common.Mod("advantageorderprobe")', + (target / "src/zone/moddev/orderprobe/OrderProbeMod.java").read_text()) + metadata = (target / "resources/META-INF/mods.toml").read_text() + self.assertIn('loaderVersion="[65,)"', metadata) + self.assertIn("Native65", metadata) + self.assertNotIn("Native64", metadata) + self.assertIn('[65.1.0]', metadata) + self.assertIn('[26.2]', metadata) + config = json.loads((target / "resources/advantageorderprobe.mixins.json").read_text()) + self.assertEqual("JAVA_21", config["compatibilityLevel"]) + self.assertEqual([], config["mixins"]) + self.assertEqual({"defaultRequire": 1}, config["injectors"]) + with self.assertRaises(FileExistsError): + probe.prepare_sources(target) + + def test_unknown_or_duplicate_template_patterns_fail_before_output_creation(self): + output = self.root / "bad" + with patch.dict(probe.TEMPLATE_HASHES, {"resources/META-INF/mods.toml": "0" * 64}), self.assertRaises(AssertionError): + probe.prepare_sources(output) + self.assertFalse(output.exists()) + for text in ("not-reviewed", "x x"): + with self.assertRaises(AssertionError): + probe.replace_once(text, "x", "y") + + def test_all_21_original_mutations_and_two_positive_baselines_remain_executable(self): + target = self.root / "sources" + probe.prepare_sources(target) + self.assertEqual(10, len(probe.GUARD_CONTROLS)) + self.assertEqual(11, len(probe.WOVEN_CONTROLS)) + for filename, names, baseline in (("run_guard_controls.py", probe.GUARD_CONTROLS, "baseline"), + ("run_woven_controls.py", probe.WOVEN_CONTROLS, "unmodified")): + text = (target / "test" / filename).read_text() + tree = ast.parse(text) + constants = {node.value for node in ast.walk(tree) if isinstance(node, ast.Constant) and isinstance(node.value, str)} + self.assertTrue(set(names) <= constants) + self.assertIn(baseline, constants) + self.assertIn('"--release", "25"', text) + self.assertIn("java.lang.AssertionError: ORDER_PROBE:", text) + self.assertIn("subprocess.run", text) + self.assertNotIn("import native12111", text) + mutator = (target / "test/WovenNegativeControl.java").read_text() + self.assertIn(probe.UPGRADE, mutator) + self.assertIn("StandardOpenOption.CREATE_NEW", mutator) + self.assertIn("jar/existing-file writes forbidden", mutator) + + def test_sealed_diagnostic_launch_does_not_relax_default_options_policy(self): + clean = dict(JAVA_TOOL_OPTIONS="", JDK_JAVA_OPTIONS="", _JAVA_OPTIONS="", CLASSPATH="") + with patch.dict(os.environ, clean): + manifest, checksum = probe.diagnostic_manifest(self.root, "converted") + env = native.native_launch_environment(self.root, "converted", diagnostic_environment=manifest, + diagnostic_environment_sha256=checksum) + self.assertEqual("-Dmixin.debug.export=true -Dmixin.debug.export.decompile=false", env["JAVA_TOOL_OPTIONS"]) + receipt = native.read_json(self.root / "native65-launch-environment-converted.json") + self.assertEqual("DIAGNOSTIC_NOT_QUALIFIED", receipt["status"]) + self.assertFalse(receipt["qualificationAllowed"]) + for key in clean: + with patch.dict(os.environ, {**clean, key: "-javaagent:forbidden.jar"}), self.assertRaises(ValueError): + native.native_launch_environment() + + def test_diagnostic_manifest_cannot_be_reused_or_modified_without_seal(self): + clean = dict(JAVA_TOOL_OPTIONS="", JDK_JAVA_OPTIONS="", _JAVA_OPTIONS="", CLASSPATH="") + with patch.dict(os.environ, clean): + path, checksum = probe.diagnostic_manifest(self.root, "converted") + with self.assertRaises(FileExistsError): + probe.diagnostic_manifest(self.root, "converted") + with self.assertRaises(ValueError): + native.native_launch_environment(self.root, "other", diagnostic_environment=path, diagnostic_environment_sha256=checksum) + document = json.loads(path.read_text()) + document["properties"]["arbitrary.vm.property"] = True + path.write_text(json.dumps(document)) + with self.assertRaises(AssertionError): + native.native_launch_environment(self.root, "converted", diagnostic_environment=path, diagnostic_environment_sha256=checksum) + with self.assertRaises(ValueError): + native.native_launch_environment(self.root, "converted", diagnostic_environment=path, diagnostic_environment_sha256=digest(path)) + + def test_sidecar_staging_does_not_relax_six_mod_reader_or_rewrite_production_jars(self): + runtime = self.root / "runtime" + (runtime / "mods").mkdir(parents=True) + production = runtime / "mods" / "production.jar" + production.write_bytes(b"immutable-production") + sidecar = self.root / "sidecar.jar" + sidecar.write_bytes(b"diagnostic-only") + variant = dict(jar=str(sidecar), sha256=digest(sidecar)) + inputs = [dict(file=production.name, sha256=digest(production))] + write_json(runtime / "input-jars.json", inputs) + expected = {production.name: digest(production)} + core = SimpleNamespace(stage=Mock(return_value=expected.copy()), verify_runtime=Mock()) + mods = {name: ("v", Path(name), "s") for name in probe.MOD_IDS} + with patch.object(probe.upgrade, "engine", return_value=core): + result = probe.instrumented_stage(variant)(runtime, "seed", "inventory", mods, 26510) + self.assertEqual(set(probe.MOD_IDS), set(mods)) + self.assertEqual({**expected, sidecar.name: digest(sidecar)}, result) + self.assertEqual(b"immutable-production", production.read_bytes()) + receipt = native.read_json(runtime / "input-jars.json") + self.assertEqual(inputs, receipt[:-1]) + self.assertIs(False, receipt[-1]["qualificationAllowed"]) + core.verify_runtime.assert_called_once_with(runtime, "inventory", result) + sidecar.write_bytes(b"changed") + with patch.object(probe.upgrade, "engine", return_value=core), self.assertRaises(AssertionError): + probe.instrumented_stage(variant)(runtime, "seed", "inventory", mods, 26510) + self.assertEqual(1, core.stage.call_count) + + def test_plugin_identity_priority_and_registered_config_guards_are_retained(self): + pins = dict(mode="power-low", priorities={"basepower.mixins.json": 900, "basemetals.mixins.json": 1100}) + before = {"basepower.mixins.json": 1000, "basemetals.mixins.json": 1000, "other.json": 321} + proof = dict(status=probe.SCOPE, qualificationAllowed=False, mode="power-low", runtime=str(self.root), pid=123, + preparedBefore=False, before=before, after={**before, **pins["priorities"]}) + probe.verify_plugin(proof, pins, self.root, 123) + for field, value in (("pid", 124), ("preparedBefore", True), ("runtime", "other"), ("qualificationAllowed", True)): + with self.assertRaises(AssertionError): + probe.verify_plugin({**proof, field: value}, pins, self.root, 123) + bad = copy.deepcopy(proof) + bad["after"]["other.json"] = 322 + with self.assertRaises(AssertionError): + probe.verify_plugin(bad, pins, self.root, 123) + + def test_control_receipts_require_every_named_rejection_and_build_hash(self): + build_path = self.root / "build.json" + write_json(build_path, {}) + output = self.root / "controls" + output.mkdir() + receipt = dict(status="PASS", scope=probe.SCOPE, qualificationAllowed=False, buildReceiptSha256=digest(build_path), + controls={"baseline": "PASS", **dict.fromkeys(probe.GUARD_CONTROLS, "REJECTED")}) + path = output / "guard-controls.json" + write_json(path, receipt) + core = SimpleNamespace(native_launch_environment=lambda: {}) + with patch.object(probe.upgrade, "engine", return_value=core), patch.object(probe.subprocess, "run", return_value=SimpleNamespace(returncode=0, stdout="", stderr="")): + self.assertEqual("PASS", probe.controls(Path("java"), build_path, {"sourceTree": str(self.root)}, output)["status"]) + for key in probe.GUARD_CONTROLS: + bad = copy.deepcopy(receipt) + del bad["controls"][key] + write_json(path, bad) + with self.subTest(key=key), self.assertRaises(AssertionError): + probe.controls(Path("java"), build_path, {"sourceTree": str(self.root)}, output) + + def test_export_archive_preserves_complete_tree_and_clears_only_owned_stale_export_name(self): + runtime = self.root / "runtime" + source = runtime / ".mixin.out" + for name in probe.TARGETS: + path = source / "class" / (name + ".class") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(name.encode()) + (source / "audit-note.txt").write_text("retain complete export output") + before = fingerprint(source) + folder = runtime / "order-evidence/converted" + folder.mkdir(parents=True) + self.assertEqual(before, probe.archive_exports(runtime, folder)) + self.assertFalse(source.exists()) + self.assertEqual(before, fingerprint(folder / "mixin-output")) + self.assertEqual("retain complete export output", (folder / "mixin-output/audit-note.txt").read_text()) + for name in probe.TARGETS: + self.assertEqual(name.encode(), (folder / "export" / (name + ".class")).read_bytes()) + with self.assertRaises(FileNotFoundError): + probe.archive_exports(runtime, folder) + + def test_incomplete_export_tree_cannot_be_archived_as_current_proof(self): + runtime = self.root / "runtime" + (runtime / ".mixin.out/class").mkdir(parents=True) + folder = runtime / "order-evidence/failed" + folder.mkdir(parents=True) + with self.assertRaisesRegex(ValueError, "Missing"): + probe.archive_exports(runtime, folder) + self.assertTrue((runtime / ".mixin.out/class").is_dir()) + self.assertFalse((folder / "export").exists()) + + def test_json_string_custody_paths_are_converted_and_changed_bytes_are_rejected(self): + path = self.root / "classpath.jar" + path.write_bytes(b"pinned dependency") + hashes = json.loads(json.dumps({str(path): digest(path)})) + probe.verify_files(hashes) + path.write_bytes(b"changed dependency") + with self.assertRaisesRegex(AssertionError, "checksum differs"): + probe.verify_files(hashes) + path.unlink() + with self.assertRaises(FileNotFoundError): + probe.verify_files(hashes) + + def test_sidecar_rejects_missing_entrypoint_or_wrong_mod_identity_before_launch(self): + files = {"META-INF/mods.toml": f'[[mods]]\nmodId="{probe.MOD_ID}"\nversion="{probe.VERSION}"\n'.encode(), + "advantageorderprobe.mixins.json": b"{}", "order-probe-pins.json": b"{}"} + files.update({"zone/moddev/orderprobe/" + name + ".class": b"unit packaging fixture only" + for name in ("OrderProbeMod", "OrderProbeAudit", "OrderProbePlugin", "ProbeGuard")}) + for index, change in enumerate((None, "entrypoint", "identity", "duplicate")): + payloads = files.copy() + if change == "entrypoint": del payloads["zone/moddev/orderprobe/OrderProbeMod.class"] + elif change == "identity": payloads["META-INF/mods.toml"] = b'[[mods]]\nmodId="unknown"\nversion="1"\n' + path = self.root / (str(index) + ".jar") + with ZipFile(path, "x") as archive: + for name, data in payloads.items(): archive.writestr(name, data) + if change == "duplicate": + with self.assertWarns(UserWarning): + archive.writestr("zone/moddev/orderprobe/OrderProbeMod.class", b"duplicate fixture") + if change is None: + probe.verify_probe_package(path) + else: + with self.subTest(change=change), self.assertRaises(AssertionError): + probe.verify_probe_package(path) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_upgrade.py b/test-rig/migration/test_native262_upgrade.py new file mode 100644 index 00000000..e5487b9d --- /dev/null +++ b/test-rig/migration/test_native262_upgrade.py @@ -0,0 +1,467 @@ +"""Real stopped-storage fixtures and fail-closed migration adapter contracts, no JVM launch.""" + +import base64 +import copy +import gzip +import io +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import Mock, patch +import uuid +import zlib + +import nbtlib as n + +from matrix import WORLD, digest, fingerprint, write_json +from native117_actor_io import bind +import native262_upgrade as upgrade + + +def encoded(tag): + stream = io.BytesIO() + n.File(tag).write(stream) + return stream.getvalue() + + +def region(tag, cx=-1, cz=-2): + payload = b"\x02" + zlib.compress(encoded(tag)) + record = len(payload).to_bytes(4, "big") + payload + sectors = (len(record) + 4095) // 4096 + header = bytearray(8192) + slot = (cx & 31) + ((cz & 31) << 5) + header[slot * 4:slot * 4 + 4] = ((2 << 8) | sectors).to_bytes(4, "big") + return bytes(header) + record.ljust(sectors * 4096, b"\0") + + +def reject_operation(*args): + raise AssertionError("Required ordinary operation executed and failed") + + +class Native262UpgradeTest(unittest.TestCase): + def test_native2612_source_retains_complete_component_payloads(self): + value = dict(id="baseelectric:lithium_battery", count=1, + components={"minecraft:custom_data": {"charge": 517, "Private": [1, 2, 3]}, + "minecraft:enchantments": {"basesteam:musket_damage": 2}}) + before = copy.deepcopy(value) + self.assertEqual(4790, upgrade.SOURCE_VERSIONS["26.1.2"]) + self.assertEqual(value, upgrade.expected_components(value, source_version="26.1.2")) + self.assertEqual(before, value) + for broken in (dict(value, Count=1), dict(value, count=True), dict(value, count=0)): + with self.assertRaises(AssertionError): + upgrade.expected_components(broken, source_version="26.1.2") + + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.runtime = self.root / "runtime" + self.world = self.runtime / WORLD + self.world.mkdir(parents=True) + self.identity = uuid.UUID("12345678-1234-5678-9abc-def012345678") + + def save(self, path, tag): + path.parent.mkdir(parents=True, exist_ok=True) + n.File(tag, gzipped=True).save(path) + + def words(self, identity=None): + value = (identity or self.identity).int + return n.IntArray([((value >> shift) & 0xffffffff) - + (0x100000000 if (value >> shift) & 0x80000000 else 0) for shift in (96, 64, 32, 0)]) + + def storage(self): + self.save(self.world / "level.dat", n.Compound(Data=n.Compound(DataVersion=n.Int(4903)))) + self.save(upgrade.layout.shared_file(self.world, "minecraft:world_gen_settings"), + n.Compound(DataVersion=n.Int(4903), data=n.Compound(seed=n.Long(-17), + unknown=n.Compound(retain=n.ByteArray([1, -2, 3]))))) + tile = n.Compound(id=n.String("minecraft:chest"), x=n.Int(-1), y=n.Int(64), z=n.Int(-17), + Items=n.List[n.Compound]([]), components=n.Compound(), Unknown=n.Long(718)) + chunk = n.Compound(DataVersion=n.Int(4903), xPos=n.Int(-1), zPos=n.Int(-2), + sections=n.List[n.Compound]([n.Compound(Y=n.Byte(4), block_states=n.Compound( + palette=n.List[n.Compound]([n.Compound(Name=n.String("minecraft:chest"))])))]), + block_entities=n.List[n.Compound]([copy.deepcopy(tile)])) + path = upgrade.layout.region_file(self.world, "minecraft:overworld", -1, -2) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(region(chunk)) + row = dict(name="chest", x=-1, y=64, z=-17, actualName="minecraft:chest", properties={}, + nbtBase64=base64.b64encode(gzip.compress(encoded(tile))).decode()) + return path, chunk, row + + def test_native_stopped_capture_reads_only_relocated_region_and_shared_worldgen(self): + path, chunk, row = self.storage() + (self.world / "region").mkdir() + (self.world / "region" / path.name).write_bytes(b"stale legacy decoy") + original = fingerprint(self.world) + receipt = upgrade.capture(self.runtime, "converted", [row]) + self.assertEqual("PASS", receipt["status"]) + self.assertEqual(4903, receipt["nativeDataVersion"]) + self.assertEqual([1, -2, 3], receipt["worldGenSettings"]["data"]["unknown"]["retain"]) + self.assertEqual("NO_SINGLEPLAYER_OWNER", receipt["playerAuthority"]["status"]) + self.assertEqual(original, fingerprint(self.world)) + self.assertEqual(path.read_bytes(), (self.runtime / "native262-save/converted" / path.name).read_bytes()) + + def test_raw_chunk_mutations_cannot_be_hidden_by_live_projection(self): + path, baseline, row = self.storage() + for index, mutation in enumerate(("version", "version-type", "coordinate", "coordinate-type", "historical", + "block", "tile", "extra-tile-field", "duplicate-tile", "duplicate-section", "packed")): + chunk = copy.deepcopy(baseline) + if mutation == "version": chunk["DataVersion"] = n.Int(4671) + elif mutation == "version-type": chunk["DataVersion"] = n.Long(4903) + elif mutation == "coordinate": chunk["xPos"] = n.Int(0) + elif mutation == "coordinate-type": chunk["xPos"] = n.String("-1") + elif mutation == "historical": chunk["Level"] = n.Compound() + elif mutation == "block": chunk["sections"][0]["block_states"]["palette"][0]["Name"] = n.String("minecraft:air") + elif mutation == "tile": chunk["block_entities"][0]["Unknown"] = n.Long(719) + elif mutation == "extra-tile-field": chunk["block_entities"][0]["Unexpected"] = n.Int(1) + elif mutation == "duplicate-tile": chunk["block_entities"].append(copy.deepcopy(chunk["block_entities"][0])) + elif mutation == "duplicate-section": chunk["sections"].append(copy.deepcopy(chunk["sections"][0])) + elif mutation == "packed": chunk["block_entities"][0]["keepPacked"] = n.Byte(1) + path.write_bytes(region(chunk)) + with self.subTest(mutation=mutation), self.assertRaises((AssertionError, ValueError)): + upgrade.capture(self.runtime, "mutant-" + str(index), [row]) + + def test_only_reviewed_zero_keep_packed_storage_field_is_projected(self): + path, chunk, row = self.storage() + chunk["block_entities"][0]["keepPacked"] = n.Byte(0) + path.write_bytes(region(chunk)) + self.assertEqual("PASS", upgrade.capture(self.runtime, "zero-packed", [row])["status"]) + + def test_empty_duplicate_and_misaddressed_live_witnesses_cannot_pass(self): + _, _, row = self.storage() + for index, rows in enumerate(([], [row, row], [{**row, "x": True}], [{**row, "x": 16}])): + with self.subTest(rows=rows), self.assertRaises((AssertionError, ValueError)): + upgrade.capture(self.runtime, "invalid-" + str(index), rows) + + def test_archive_damage_and_missing_native_path_do_not_use_legacy_fallback(self): + path, chunk, row = self.storage() + native = path.read_bytes() + legacy = self.world / "region" / path.name + legacy.parent.mkdir() + legacy.write_bytes(native) + for index, raw in enumerate((b"", native[:-1], b"invalid", bytes(8192))): + path.write_bytes(raw) + with self.subTest(raw=len(raw)), self.assertRaises((ValueError, AssertionError)): + upgrade.capture(self.runtime, "damaged-" + str(index), [row]) + path.unlink() + with self.assertRaisesRegex(ValueError, "Missing"): + upgrade.capture(self.runtime, "missing", [row]) + + def test_host_uuid_owns_native_player_path_not_legacy_or_embedded_decoys(self): + self.storage() + self.save(self.world / "level.dat", n.Compound(Data=n.Compound(DataVersion=n.Int(4903), singleplayer_uuid=self.words()))) + player = n.Compound(DataVersion=n.Int(4903), UUID=self.words(), Unknown=n.Long(88)) + path = upgrade.layout.player_file(self.world, self.identity) + self.save(path, player) + self.save(self.world / "playerdata" / path.name, n.Compound(UUID=self.words(uuid.uuid4()))) + result = upgrade.player_authority(self.world) + self.assertEqual("NATIVE_PLAYER", result["status"]) + self.assertEqual("players/data/" + path.name, result["file"].replace("\\", "/")) + self.assertFalse(result["playerConversionQualified"]) + player["DataVersion"] = n.Int(4671) + self.save(path, player) + self.assertEqual("RETAINED_UNOPENED_SOURCE_PLAYER", upgrade.player_authority(self.world)["status"]) + path.unlink() + with self.assertRaises(FileNotFoundError): + upgrade.player_authority(self.world) + + def test_host_uuid_version_and_embedded_conflicts_fail_closed(self): + self.storage() + self.save(self.world / "level.dat", n.Compound(Data=n.Compound(DataVersion=n.Int(4903), singleplayer_uuid=self.words()))) + path = upgrade.layout.player_file(self.world, self.identity) + for change in (dict(UUID=self.words(uuid.uuid4())), dict(DataVersion=n.Int(4904)), + dict(DataVersion=n.String("4903")), dict(DataVersion=n.Int(0))): + tag = n.Compound(DataVersion=n.Int(4903), UUID=self.words(), **{}) + tag.update(change) + self.save(path, tag) + with self.subTest(change=change), self.assertRaises(AssertionError): + upgrade.player_authority(self.world) + self.save(self.world / "level.dat", n.Compound(Data=n.Compound(DataVersion=n.Int(4903), + singleplayer_uuid=self.words(), Player=n.Compound(UUID=self.words())))) + with self.assertRaises(ValueError): + upgrade.player_authority(self.world) + + def test_turbine_native_entity_region_preserves_uuid_parent_and_position(self): + self.storage() + actor = n.Compound(id=n.String("baseelectric:hydroturbine"), UUID=self.words(), parentPos=n.IntArray([-1, 64, -17]), + Pos=n.List[n.Double]([-0.5, 64.0, -16.5])) + tag = n.Compound(DataVersion=n.Int(4903), Position=n.IntArray([-1, -2]), Entities=n.List[n.Compound]([actor])) + path = upgrade.layout.region_file(self.world, "minecraft:overworld", -1, -2, "entities") + path.parent.mkdir(parents=True) + path.write_bytes(region(tag)) + result = upgrade.turbine(self.world, [-1, 64, -17]) + self.assertEqual([-1, 64, -17], result["parent"]) + self.assertEqual([-0.5, 64.0, -16.5], result["position"]) + for version in (4671, 4791): + tag["DataVersion"] = n.Int(version) + path.write_bytes(region(tag)) + with self.assertRaises((AssertionError, ValueError)): + upgrade.turbine(self.world, [-1, 64, -17]) + + def source_receipt(self, version="1.21.11", data_version=4671, route=None): + source = self.root / "source" + source.mkdir(exist_ok=True) + write_json(source / "migration-restarted.json", []) + receipt = dict(status="PASS", minecraft=version, sourceUnchanged=True, phases={"restarted": dict( + status="PASS", snapshotSha256=digest(source / "migration-restarted.json"), serialized=dict(nativeDataVersion=data_version))}) + path = source / "receipt.json" + write_json(path, receipt) + return SimpleNamespace(source=source, legacy_reference=None, source_receipt_name=path.name, + receipt_sha256=digest(path), source_route=route or version), receipt + + def test_each_retained_intermediate_and_12111_route_requires_exact_sealed_version(self): + for version, data_version in upgrade.SOURCE_VERSIONS.items(): + args, _ = self.source_receipt(version, data_version) + self.assertEqual(version, upgrade.read_source(args)[4]) + self.assertEqual(("original110", "released110", "maintained112", "released112"), upgrade.LEGACY_ROUTES) + + def test_source_receipt_cannot_lie_about_version_status_route_or_snapshot(self): + for version, dv, route in (("1.21.11", 3955, "1.21.11"), (None, 4671, "1.21.11"), + ({}, 4671, "1.21.11"), ("1.21.11", "4671", "1.21.11"), + ("1.21.11", 4671, "1.21.1"), ("1.21.11", 4671, "original110")): + args, _ = self.source_receipt(version, dv, route) + with self.subTest(version=version, dv=dv, route=route), self.assertRaises(AssertionError): + upgrade.read_source(args) + args, receipt = self.source_receipt() + receipt["status"] = "FAIL" + write_json(args.source / "receipt.json", receipt) + args.receipt_sha256 = digest(args.source / "receipt.json") + with self.assertRaisesRegex(AssertionError, "passing"): + upgrade.read_source(args) + args, _ = self.source_receipt() + (args.source / "migration-restarted.json").write_text("[1]") + with self.assertRaisesRegex(AssertionError, "checksum"): + upgrade.read_source(args) + + def test_raw_historical_routes_require_independent_exact_source_fingerprint(self): + for route in upgrade.LEGACY_ROUTES: + args, receipt = self.source_receipt("1.20.1", 3465, route) + original = self.root / route + original.mkdir() + (original / "immutable").write_bytes(b"retained historic input") + self.save(original / WORLD / "level.dat", n.Compound(Data=n.Compound(DataVersion=n.Int(upgrade.LEGACY_SOURCE_VERSIONS[route])))) + receipt.update(source=str(original), sourceFingerprint=fingerprint(original)) + write_json(args.source / "receipt.json", receipt) + args.legacy_reference, args.source = args.source, original + args.receipt_sha256 = digest(args.legacy_reference / "receipt.json") + self.assertEqual("1.20.1", upgrade.read_source(args)[4]) + (original / "immutable").write_bytes(b"changed") + with self.subTest(route=route), self.assertRaisesRegex(AssertionError, "no longer matches"): + upgrade.read_source(args) + + def test_pre_1201_intermediates_can_use_their_sealed_1201_bridge_without_mislabeling_format(self): + for route, version in upgrade.SOURCE_VERSIONS.items(): + if version >= 3465: + continue + args, receipt = self.source_receipt("1.20.1", 3465, route) + original = self.root / ("bridge-" + route) + self.save(original / WORLD / "level.dat", n.Compound(Data=n.Compound(DataVersion=n.Int(version)))) + receipt.update(source=str(original), sourceFingerprint=fingerprint(original)) + write_json(args.source / "receipt.json", receipt) + args.legacy_reference, args.source = args.source, original + args.receipt_sha256 = digest(args.legacy_reference / "receipt.json") + self.assertEqual("1.20.1", upgrade.read_source(args)[4]) + args.source_route = "1.21.11" + with self.assertRaisesRegex(AssertionError, "Historical bridge"): + upgrade.read_source(args) + + def test_expected_components_preserve_legacy_adapters_and_native_opaque_payloads(self): + legacy = dict(id="basesteam:musket", Count=1, tag=dict(Damage=7, loaded=1, + Enchantments=[dict(id="basesteam:powderless", lvl=1)])) + for version, data_version in upgrade.SOURCE_VERSIONS.items(): + if data_version < 3465 or version == "1.20.1": + self.assertEqual(upgrade.previous.components(legacy), upgrade.expected_components(legacy, source_version=version)) + native = dict(id="basesteam:musket", count=1, components={"minecraft:damage": 7, + "minecraft:enchantments": {"basesteam:powderless": 1}, + "other:opaque": {"id": "not:an_item", "count": False, "components": {"levels": "opaque"}}}) + original = copy.deepcopy(native) + adapted = upgrade.expected_components(native, source_version="1.21.11") + self.assertEqual(original, adapted) + adapted["components"]["other:opaque"]["count"] = 3 + self.assertEqual(original, native) + + def test_native_expected_component_types_and_wrappers_are_not_coerced(self): + for version in (None, {}, True, "26.2"): + with self.assertRaises(AssertionError): + upgrade.expected_components({}, source_version=version) + base = dict(id="basesteam:musket", count=1, components={"minecraft:enchantments": {"basesteam:powderless": 1}}) + for field, value in (("count", True), ("count", 0), ("count", "1"), ("id", "bad id"), ("components", None), ("Count", 1)): + with self.subTest(field=field, value=value), self.assertRaises(AssertionError): + upgrade.expected_components({**base, field: value}, source_version="1.21.11") + for levels in ({"levels": {}}, {"basesteam:powderless": True}, {"basesteam:powderless": 256}, {7: 1}, []): + with self.subTest(levels=levels), self.assertRaises(AssertionError): + upgrade.expected_components({**base, "components": {"minecraft:enchantments": levels}}, source_version="1.21.11") + + def test_native_observation_extra_unknown_wear_enchantment_and_count_changes_stay_failures(self): + tag = {"Items": [dict(id="basesteam:musket", count=1, components={"minecraft:damage": 7, + "minecraft:enchantments": {"basesteam:powderless": 1}, "other:opaque": {"retain": [1, 2]}})], "components": {}} + row = dict(name="chest", x=1, y=2, z=3, actualName="minecraft:chest", properties={}, nbtBase64="fixture") + for change in (None, "wear", "enchantment", "count", "opaque", "extra", "missing"): + observed = copy.deepcopy(tag) + stack = observed["Items"][0] + if change == "wear": stack["components"]["minecraft:damage"] = 8 + elif change == "enchantment": stack["components"]["minecraft:enchantments"]["basesteam:powderless"] = 2 + elif change == "count": stack["count"] = 2 + elif change == "opaque": stack["components"]["other:opaque"]["retain"].append(3) + elif change == "extra": stack["components"]["other:added"] = 9 + elif change == "missing": del stack["components"]["other:opaque"] + with patch.object(upgrade.previous, "parse_nbt", side_effect=[tag, observed]): + actual = upgrade.comparison([row], [row], modern=True, source_version="1.21.11") + self.assertEqual("PASS" if change is None else "FAIL", actual["status"]) + with patch.object(upgrade.previous, "parse_nbt", side_effect=[tag, tag]): + self.assertEqual("PASS", upgrade.comparison([row], [row], modern=False)["status"]) + + def test_stopped_protocol_freezes_before_snapshot_flushes_then_closes(self): + events = [] + server = Mock() + server.send.side_effect = lambda command: events.append(command) + server.snapshot.side_effect = lambda label: (events.append("snapshot:" + label), write_json(self.runtime / ("migration-" + label + ".json"), [])) + server.close.side_effect = lambda: events.append("close") + core = SimpleNamespace(Server=Mock(return_value=server), LAUNCHER="official-args") + with patch.object(upgrade, "engine", return_value=core): + self.assertEqual([], upgrade.with_server(self.runtime, "java", "converted", lambda run: events.append("action"))) + self.assertEqual(["action", "tick freeze", "snapshot:converted", "save-all flush", "close"], events) + self.assertEqual([("The game is frozen", 30), ("Saved the game", 30)], [call.args for call in server.wait.call_args_list]) + + def test_action_and_stop_failures_both_remain_visible_and_no_snapshot_passes(self): + server = Mock() + server.close.side_effect = AssertionError("stop log failure") + core = SimpleNamespace(Server=Mock(return_value=server), LAUNCHER="official-args") + original = AssertionError("action failed") + with patch.object(upgrade, "engine", return_value=core), self.assertRaisesRegex(AssertionError, "action failed") as failure: + upgrade.with_server(self.runtime, "java", "converted", Mock(side_effect=original)) + self.assertIn("stop log failure", failure.exception.__notes__[0]) + server.snapshot.assert_not_called() + server.close.assert_called_once() + + def test_operation_readiness_uses_target_identity_and_all_distinct_chunks(self): + server = Mock() + ready = Mock(return_value={"status": "PASS"}) + load = bind(upgrade.load_operating_chunks, await_ready=ready) + self.assertEqual(2, len(load(server, self.runtime, {"a": (-1, 64, 32), "b": (16, 64, -1), "same": (31, 5, -8)}))) + self.assertTrue(all(call.kwargs["minecraft"] == "26.2" for call in ready.call_args_list)) + self.assertEqual(["forceload add -16 32", "forceload add 16 -16"], [call.args[0] for call in server.send.call_args_list]) + ready.side_effect = AssertionError("not ticking") + with self.assertRaisesRegex(AssertionError, "not ticking"): + load(server, self.runtime, {"a": (0, 5, 0)}) + + def test_historical_world_copy_is_exact_and_only_loader_config_collision_is_explicit(self): + source = self.root / "copy-source" + (source / WORLD).mkdir(parents=True) + (source / WORLD / "retain").write_bytes(b"immutable") + (source / "config").mkdir() + (source / "config/fml.toml").write_text("old-loader") + (source / "config/mod.cfg").write_text("old-mod") + target = self.root / "copy-target" + (target / "config").mkdir(parents=True) + (target / "config/fml.toml").write_text("native-loader") + before = fingerprint(source) + upgrade.copy_source(source, target) + self.assertEqual(before, fingerprint(source)) + self.assertEqual(fingerprint(source / WORLD), fingerprint(target / WORLD)) + receipt = upgrade.read_json(target / "historical-config-custody.json") + self.assertEqual("NATIVE_TARGET_LOADER", receipt["fml.toml"]["authority"]) + self.assertEqual("native-loader", (target / "config/fml.toml").read_text()) + bad = self.root / "bad-copy" + (bad / "config").mkdir(parents=True) + (bad / "config/mod.cfg").write_text("unreviewed") + with self.assertRaisesRegex(AssertionError, "collision"): + upgrade.copy_source(source, bad) + + def test_staging_failure_retains_fail_receipt_and_oracle_input_hashes_without_launch(self): + args, _ = self.source_receipt() + args.profile, args.inventory = self.root / "profile.json", self.root / "inventory.json" + write_json(args.profile, {}) + write_json(args.inventory, {}) + args.seed = self.root / "seed" + args.seed.mkdir() + args.java = self.root / "java.exe" + args.java.write_bytes(b"unit fake, never executed") + args.output, args.port, args.inventory_sha256 = self.root / "failed-output", 26510, digest(args.inventory) + mods = {} + for name in upgrade.MOD_IDS: + jar = self.root / (name + ".jar") + jar.write_bytes(name.encode()) + mods[name] = "unit", jar, digest(jar) + core = SimpleNamespace(read_family_target=Mock(return_value=(args.seed, {}, mods)), + java25=Mock(return_value=(args.java, "unit")), + stage=Mock(side_effect=AssertionError("owned staging failed")), verify_runtime=Mock()) + with patch.object(upgrade, "engine", return_value=core), patch.object(upgrade, "output_path", return_value=args.output), \ + self.assertRaisesRegex(AssertionError, "owned staging failed"): + upgrade.run(args) + receipt = upgrade.read_json(args.output / "native262-upgrade-results.json") + self.assertEqual("FAIL", receipt["status"]) + self.assertIn("owned staging failed", receipt["traceback"]) + self.assertTrue(receipt["sourceUnchanged"]) + self.assertTrue(receipt["inputsUnchanged"]) + self.assertTrue(receipt["referenceUnchanged"]) + self.assertTrue(any(path.endswith("native12111_upgrade.py") for path in receipt["inputHashes"])) + self.assertTrue(all(str(path) in receipt["inputHashes"] for _, path, _ in mods.values())) + + def test_hook_only_scope_never_invents_operation_pass_and_ordinary_operation_remains_required(self): + args, prior = self.source_receipt() + rows = [dict(name="hydroGenerator", x=1, y=5, z=1, actualName="baseelectric:hydrogenerator", properties={})] + write_json(args.source / "migration-restarted.json", rows) + write_json(args.source / "migration-fixture.json", rows) + prior["phases"]["restarted"].update(snapshotSha256=digest(args.source / "migration-restarted.json"), + turbine=dict(id="baseelectric:hydroturbine", uuid=[1, 2])) + write_json(args.source / "receipt.json", prior) + args.receipt_sha256 = digest(args.source / "receipt.json") + (args.source / WORLD).mkdir() + args.profile, args.inventory = self.root / "profile.json", self.root / "inventory.json" + write_json(args.profile, {}) + write_json(args.inventory, {}) + args.seed = self.root / "seed" + args.seed.mkdir() + args.java = self.root / "java.exe" + args.java.write_bytes(b"unit fake, never executed") + args.port, args.inventory_sha256 = 26560, digest(args.inventory) + mods = {} + for name in upgrade.MOD_IDS: + path = self.root / (name + ".jar") + path.write_bytes(name.encode()) + mods[name] = "unit", path, digest(path) + + def stage(runtime, *values): + runtime.mkdir() + return {} + + def snapshot(runtime, java, label, action, **options): + write_json(runtime / ("migration-" + label + ".json"), rows) + return rows + + core = SimpleNamespace(read_family_target=Mock(return_value=(args.seed, {}, mods)), + java25=Mock(return_value=(args.java, "unit")), stage=stage, verify_runtime=Mock()) + for hook_only in (True, False): + args.output = self.root / ("hook-only" if hook_only else "ordinary") + with patch.object(upgrade, "engine", return_value=core), patch.object(upgrade, "output_path", return_value=args.output), \ + patch.object(upgrade, "with_server", side_effect=snapshot) as boots, \ + patch.object(upgrade, "capture", return_value=dict(status="PASS", worldGenSettings={"seed": 17})), \ + patch.object(upgrade, "turbine", return_value=prior["phases"]["restarted"]["turbine"]), \ + patch.object(upgrade.previous, "operate", reject_operation): + if hook_only: + self.assertEqual(0, upgrade.run(args, hook_only=True)) + else: + with self.assertRaisesRegex(AssertionError, "Required ordinary operation"): + upgrade.run(args) + self.assertEqual(2, boots.call_count) + result = upgrade.read_json(args.output / "native262-upgrade-results.json") + self.assertNotIn("operation", result["phases"]) + if hook_only: + self.assertEqual("PASS", result["status"]) + self.assertEqual(upgrade.HOOK_SCOPE, result["scope"]) + self.assertIs(False, result["qualificationAllowed"]) + self.assertEqual("NOT_RUN", result["operationNotRun"]["status"]) + else: + self.assertEqual("FAIL", result["status"]) + self.assertEqual(upgrade.SCOPE, result["scope"]) + self.assertNotIn("operationNotRun", result) + + def test_hook_scope_rejects_implicit_truthy_mode_values(self): + for value in ("true", 1, None, []): + with self.subTest(value=value), self.assertRaisesRegex(ValueError, "explicitly boolean"): + upgrade.run(None, hook_only=value) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native262_worldgen.py b/test-rig/migration/test_native262_worldgen.py new file mode 100644 index 00000000..d82b1fc8 --- /dev/null +++ b/test-rig/migration/test_native262_worldgen.py @@ -0,0 +1,86 @@ +import copy +from pathlib import Path +import tempfile +import unittest + +import nbtlib + +import native12111_worldgen as old +import native262_worldgen as worldgen +from test_native262_resources import level_fixture + + +class WorldgenAdapterTest(unittest.TestCase): + def test_existing_assertion_bodies_and_templates_are_not_rewritten(self): + self.assertIs(worldgen.validate_profile, old.validate_profile) + self.assertIs(worldgen.validate_sample.__code__, old.validate_sample.__code__) + self.assertIs(worldgen.validate_sample.__globals__["native"], worldgen.native) + self.assertIsNot(old.validate_sample.__globals__["native"], worldgen.native) + spec = worldgen.campaign_spec("advantage") + source = worldgen.native.read_json(spec["template"]) + for key in ("worldgenExpectations", "firstProfileAssertions", "restartProfileAssertions"): + self.assertEqual(source[key], spec[key]) + with self.assertRaisesRegex(AssertionError, "provider pin"): + worldgen.campaign_spec("mineralogy") + + def test_all_six_jars_remain_pinned_and_extra_providers_are_not_smuggled_in(self): + spec = worldgen.campaign_spec("advantage") + mods = dict.fromkeys(worldgen.FAMILY, "synthetic-unit-pin") + self.assertEqual(mods, worldgen.select_mods(mods, spec)) + for wrong in ({**mods, "mineralogy": "unapproved"}, {k: v for k, v in mods.items() if k != "basesteam"}): + with self.assertRaises(AssertionError): + worldgen.select_mods(wrong, spec) + + def test_separate_native_saved_data_is_lossless_and_no_flat_or_legacy_fallback(self): + with tempfile.TemporaryDirectory() as directory: + world, level = level_fixture(directory, worldgen.WORLD) + settings_path = worldgen.layout.shared_file(world, "minecraft:world_gen_settings") + settings_path.parent.mkdir(parents=True) + settings = nbtlib.File({"DataVersion": nbtlib.Int(4903), "data": nbtlib.Compound( + seed=nbtlib.Long(1234), unknown_retained=nbtlib.String("not-projected-away"), dimensions=nbtlib.Compound({ + "minecraft:overworld": nbtlib.Compound(generator=nbtlib.Compound(type=nbtlib.String("minecraft:noise"), + settings=nbtlib.String("minecraft:overworld")))}))}) + settings.save(settings_path, gzipped=True) + before = settings_path.read_bytes() + actual = worldgen.normal_world(world) + self.assertEqual(settings, actual) + self.assertEqual(before, settings_path.read_bytes()) + self.assertEqual("not-projected-away", actual["data"]["unknown_retained"]) + for wrong in (nbtlib.Int(4671), nbtlib.String("4903")): + changed = copy.deepcopy(settings) + changed["DataVersion"] = wrong + changed.save(settings_path, gzipped=True) + with self.assertRaises(ValueError): + worldgen.normal_world(world) + settings["data"]["dimensions"]["minecraft:overworld"]["generator"]["type"] = nbtlib.String("minecraft:flat") + settings.save(settings_path, gzipped=True) + with self.assertRaisesRegex(AssertionError, "normal terrain"): + worldgen.normal_world(world) + + def test_native_restart_does_not_inherit_old_oil_exclusion_as_a_waiver(self): + sample = dict(centers={"DESERT": "0,0"}, chunksScanned=1, + counts={"basepower:crude_oil": 5}, biomeViolations={"basepower:crude_oil": 0}, + biomeBoundaryBlocks={"basepower:crude_oil": 0}) + spec = dict(name="worldgen-legacy-import", stableWorldgenCountExclusions=["basepower:crude_oil"]) + worldgen.validate_restart(sample, copy.deepcopy(sample), spec) + changed = copy.deepcopy(sample) + changed["counts"]["basepower:crude_oil"] += 1 + old.validate_restart(sample, changed, spec) + with self.assertRaisesRegex(AssertionError, "no historical exclusion"): + worldgen.validate_restart(sample, changed, spec) + + def test_stage_changes_only_disposable_normal_world_properties(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + settings = root / "server.properties" + settings.write_text("level-type=minecraft:flat\ngenerator-settings=flat\nserver-ip=127.0.0.1\n") + worldgen.configure_normal_world(root, worldgen.campaign_spec("advantage"), 26520) + data = dict(line.split("=", 1) for line in settings.read_text().splitlines()) + self.assertEqual("minecraft:normal", data["level-type"]) + self.assertEqual("", data["generator-settings"]) + self.assertEqual("-1", data["pause-when-empty-seconds"]) + self.assertEqual("127.0.0.1", data["server-ip"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/platform-controls/NativePlatformSupport.java b/test-rig/platform-controls/NativePlatformSupport.java index 9a230690..903a1470 100644 --- a/test-rig/platform-controls/NativePlatformSupport.java +++ b/test-rig/platform-controls/NativePlatformSupport.java @@ -136,9 +136,9 @@ static void sourceEvidence(Map proof, Class... types) throws proof.put("nativeSourceSha256", hashes); Map controls = new LinkedHashMap<>(); for (String source : List.of("NativePlatformSupport.java", "NativeLeashKnotPlatformControl.java")) { - controls.put(source, sha(Files.readAllBytes(Path.of("platform-controls", source)))); + controls.put("platform-controls/" + source, sha(Files.readAllBytes(Path.of("platform-controls", source)))); } - controls.put("NativeEmptyEntityTracker.java", sha(Files.readAllBytes(Path.of( + controls.put("unit-support/zone/moddev/mc/advantageworks/unit/NativeEmptyEntityTracker.java", sha(Files.readAllBytes(Path.of( "unit-support/zone/moddev/mc/advantageworks/unit/NativeEmptyEntityTracker.java")))); proof.put("controlSourceSha256", controls); } diff --git a/test-rig/platform-controls/NativeSystemReportPlatformControl.java b/test-rig/platform-controls/NativeSystemReportPlatformControl.java new file mode 100644 index 00000000..63cc272e --- /dev/null +++ b/test-rig/platform-controls/NativeSystemReportPlatformControl.java @@ -0,0 +1,63 @@ +import com.google.gson.Gson; +import java.io.File; +import java.lang.management.ManagementFactory; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import net.minecraft.SharedConstants; +import net.minecraft.SystemReport; + +/** Reproduces host diagnostics without Advantage, a world, a server or entity ticks. */ +public final class NativeSystemReportPlatformControl { + public static void main(String[] args) throws Exception { + for (String name : List.of("zone.moddev.mc.basepower.PowerAdvantage", + "zone.moddev.mc.advantageworks.NativeActorIo")) { + try { + Class.forName(name); + throw new AssertionError("Advantage code is present: " + name); + } catch (ClassNotFoundException expected) { } + } + for (String key : List.of("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS", "CLASSPATH")) + if (!System.getenv().getOrDefault(key, "").isBlank()) + throw new AssertionError("Inherited native control option " + key); + SharedConstants.tryDetectVersion(); + Path root = Path.of(args[0]); + Files.createDirectory(root); + String report = new SystemReport().toLineSeparatedString(); + Files.writeString(root.resolve("native-system-report.txt"), report); + boolean missing = new oshi.SystemInfo().getOperatingSystem().getCurrentProcess() == null; + Map proof = new LinkedHashMap<>(); + proof.put("scope", "INDEPENDENT_NATIVE_HOST_DIAGNOSTICS_NOT_GAMEPLAY"); + proof.put("minecraftVersion", SharedConstants.getCurrentVersion().name()); + proof.put("targetDataVersion", SharedConstants.getCurrentVersion().dataVersion().version()); + proof.put("mainClass", "NativeSystemReportPlatformControl"); + proof.put("currentProcessMissing", missing); + proof.put("noPower", true); + proof.put("noHarness", true); + proof.put("noServer", true); + proof.put("noWorld", true); + proof.put("noTicks", true); + proof.put("inheritedJvmEnvironmentAbsent", true); + proof.put("jvmArguments", ManagementFactory.getRuntimeMXBean().getInputArguments()); + proof.put("nativeClassSha256", NativePlatformSupport.classHashes(SystemReport.class, oshi.SystemInfo.class)); + proof.put("controlSourceSha256", NativePlatformSupport.sha(Files.readAllBytes( + Path.of("platform-controls/NativeSystemReportPlatformControl.java")))); + List> archives = new ArrayList<>(); + for (String entry : System.getProperty("java.class.path").split(Pattern.quote(File.pathSeparator))) { + Path path = Path.of(entry).toAbsolutePath().normalize(); + if (Files.isRegularFile(path)) archives.add(Map.of("path", path.toString(), "sha256", + NativePlatformSupport.sha(Files.readAllBytes(path)))); + } + proof.put("classpath", archives); + Files.writeString(root.resolve("receipt.json"), new Gson().toJson(proof) + "\n"); + if (!missing || !report.contains("Process Loads: ~~ERROR~~ NullPointerException") + || !report.contains("Process Virtual Size (MiB): ~~ERROR~~ NullPointerException") + || !report.contains("Process Resident Size (MiB): ~~ERROR~~ NullPointerException")) + throw new AssertionError("Host failure was not independently reproduced"); + System.out.println("NATIVE_SYSTEM_REPORT_CONTROL_DONE no_mods=true no_server=true no_ticks=true"); + } +} diff --git a/test-rig/platform-controls/NativeWolfPlatformControl.java b/test-rig/platform-controls/NativeWolfPlatformControl.java index 9d7f626e..0eb2b267 100644 --- a/test-rig/platform-controls/NativeWolfPlatformControl.java +++ b/test-rig/platform-controls/NativeWolfPlatformControl.java @@ -1,9 +1,14 @@ import com.google.gson.Gson; import java.lang.reflect.Field; +import java.lang.management.ManagementFactory; +import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.LinkedHashMap; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; import java.util.Map; import java.util.UUID; import net.minecraft.SharedConstants; @@ -23,6 +28,49 @@ public final class NativeWolfPlatformControl { private static final UUID OWNER = UUID.fromString("a614e658-6270-33c3-9297-7ce80908964f"); private static final UUID ID = UUID.fromString("a6ea8abd-6ab3-4e66-8ef5-d713aabbccdd"); + private static void runtimeEvidence(Path root) throws Exception { + for (String key : List.of("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS", "CLASSPATH")) { + if (!System.getenv().getOrDefault(key, "").isBlank()) + throw new AssertionError("Native control inherited " + key); + } + Map proof = new LinkedHashMap<>(); + proof.put("scope", "WORLDLESS_INDEPENDENT_NATIVE_WOLF_CONTROL_NOT_GAMEPLAY"); + proof.put("minecraftVersion", SharedConstants.getCurrentVersion().name()); + proof.put("targetDataVersion", SharedConstants.getCurrentVersion().dataVersion().version()); + Path java = Path.of(System.getProperty("java.home"), "bin", "java.exe"); + if (!Files.isRegularFile(java)) java = java.resolveSibling("java"); + proof.put("java", java.toString()); + proof.put("javaSha256", NativePlatformSupport.sha(Files.readAllBytes(java))); + proof.put("jvmArguments", ManagementFactory.getRuntimeMXBean().getInputArguments()); + proof.put("mainClass", "NativeWolfPlatformControl"); + proof.put("inheritedJvmEnvironmentAbsent", true); + List> paths = new ArrayList<>(); + for (String entry : System.getProperty("java.class.path").split(Pattern.quote(File.pathSeparator))) { + Path path = Path.of(entry).toAbsolutePath().normalize(); + Map row = new LinkedHashMap<>(); + row.put("path", path.toString()); + if (Files.isRegularFile(path)) row.put("sha256", NativePlatformSupport.sha(Files.readAllBytes(path))); + else if (Files.isDirectory(path)) { + Map files = new LinkedHashMap<>(); + try (var stream = Files.walk(path)) { + for (Path file : stream.filter(Files::isRegularFile).sorted().toList()) + files.put(path.relativize(file).toString(), NativePlatformSupport.sha(Files.readAllBytes(file))); + } + row.put("files", files); + } else throw new AssertionError("Missing control classpath: " + path); + paths.add(row); + } + proof.put("classpath", paths); + proof.put("nativeClassSha256", NativePlatformSupport.classHashes(Wolf.class, net.minecraft.world.entity.Entity.class)); + Map source = new LinkedHashMap<>(); + for (String name : List.of("platform-controls/NativeWolfPlatformControl.java", + "platform-controls/NativePlatformSupport.java", + "unit-support/zone/moddev/mc/advantageworks/unit/NativeEmptyEntityTracker.java")) + source.put(name, NativePlatformSupport.sha(Files.readAllBytes(Path.of(name)))); + proof.put("controlSourceSha256", source); + Files.writeString(root.resolve("control-runtime.json"), new Gson().toJson(proof) + "\n"); + } + private static ServerLevel context() throws Exception { ServerLevel world = NativePlatformSupport.context(true); // Native navigation now consults debug subscribers even before any entity ticks. @@ -124,6 +172,7 @@ public static void main(String[] args) throws Exception { ServerLevel world = context(); control(root, world, true, 8); control(root, world, true, 20); control(root, world, false, 8); legacyControl(root, world, 512); legacyControl(root, world, 1343); + runtimeEvidence(root); System.out.println("WOLF_CONTROL_DONE no_server_started=true no_ticks=true native_entity_methods_unmodified=true"); } } diff --git a/test-rig/platform-controls/NettyJava25PlatformControl.java b/test-rig/platform-controls/NettyJava25PlatformControl.java new file mode 100644 index 00000000..eedebd20 --- /dev/null +++ b/test-rig/platform-controls/NettyJava25PlatformControl.java @@ -0,0 +1,63 @@ +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.logging.InternalLoggerFactory; +import io.netty.util.internal.logging.JdkLoggerFactory; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +public final class NettyJava25PlatformControl { + public static void main(String[] args) throws Exception { + for (String name : List.of("zone.moddev.mc.basepower.PowerAdvantage", + "zone.moddev.mc.advantageworks.NativeActorIo", "net.minecraft.server.MinecraftServer")) { + try { + Class.forName(name, false, NettyJava25PlatformControl.class.getClassLoader()); + throw new AssertionError("Family/Minecraft class leaked into isolated Netty control: " + name); + } catch (ClassNotFoundException expected) { } + } + List captured = new ArrayList<>(); + Logger logger = Logger.getLogger("io.netty"); + logger.setLevel(Level.ALL); + Handler capture = new Handler() { + public void publish(LogRecord record) { captured.add(record); } + public void flush() {} + public void close() {} + }; + capture.setLevel(Level.ALL); + logger.addHandler(capture); + for (Handler handler : Logger.getLogger("").getHandlers()) handler.setLevel(Level.ALL); + InternalLoggerFactory.setDefaultFactory(JdkLoggerFactory.INSTANCE); + if (PlatformDependent.javaVersion() != 25 || PlatformDependent.hasUnsafe() + || !"io.netty.common".equals(PlatformDependent.class.getModule().getName())) + throw new AssertionError("Wrong native Java25/default Unsafe/module context"); + var buffer = PlatformDependent.allocateDirect(32); + buffer.buffer().putInt(0, 0x5A31C0DE); + if (buffer.buffer().getInt(0) != 0x5A31C0DE) throw new AssertionError("Direct allocation round trip failed"); + buffer.clean(); + String[] names = {"CleanerJava9", "CleanerJava24Linker", "CleanerJava25"}; + String[] messages = {"java.nio.ByteBuffer.cleaner(): unavailable", "java.nio.ByteBuffer.cleaner(): unavailable", + "java.nio.ByteBuffer.cleaner(): available"}; + String[] reasons = {"sun.misc.Unsafe unavailable", + "Native access (restricted methods) is not enabled for the io.netty.common module.", null}; + int prior = -1; + for (int i = 0; i < names.length; i++) { + final String name = "io.netty.util.internal." + names[i]; + var records = captured.stream().filter(record -> name.equals(record.getLoggerName())).toList(); + if (records.size() != 1) throw new AssertionError("Wrong exact probe count for " + name); + LogRecord record = records.getFirst(); + int index = captured.indexOf(record); + if (index <= prior || !record.getLevel().equals(Level.FINE) || !messages[i].equals(record.getMessage())) + throw new AssertionError("Wrong native probe order/level/message: " + name); + if (reasons[i] == null ? record.getThrown() != null + : !(record.getThrown() instanceof UnsupportedOperationException) + || !reasons[i].equals(record.getThrown().getMessage())) + throw new AssertionError("Wrong native capability exception: " + name); + prior = index; + System.out.println("NATIVE_PROBE " + name + " " + record.getMessage() + " reason=" + reasons[i]); + } + System.out.println("NETTY_JAVA25_CONTROL_PASS noPower=true noHarness=true noMinecraft=true namedModule=io.netty.common" + + " unsafe=false directAllocationRoundTrip=true cleaner25=true newJvmFlags=false"); + } +} diff --git a/test-rig/profiles/native262/dev-core.json b/test-rig/profiles/native262/dev-core.json index d2fea194..837fb51d 100644 --- a/test-rig/profiles/native262/dev-core.json +++ b/test-rig/profiles/native262/dev-core.json @@ -9,12 +9,12 @@ { "id": "basesteam", "version": "2.2.18.2602001", - "sha256": "0000000000000000000000000000000000000000000000000000000000000000" + "sha256": "EDD10D6ED4A623F6EAA364D21480FDA3B777FB15C2195D437882DB09C0F415AB" }, { "id": "baseelectric", "version": "2.2.18.2602001", - "sha256": "0000000000000000000000000000000000000000000000000000000000000000" + "sha256": "A7D12A66CA91A6BACCFEFD2CED9D3EC962F5FEB73BF632E1683F99084384F938" } ] } diff --git a/test-rig/profiles/native262/legacy-visible-alternatives.json b/test-rig/profiles/native262/legacy-visible-alternatives.json new file mode 100644 index 00000000..dee289df --- /dev/null +++ b/test-rig/profiles/native262/legacy-visible-alternatives.json @@ -0,0 +1,11 @@ +{ + "protocol": "NATIVE26_LEGACY_VISIBLE_DISABLED_ALTERNATIVES_V1", + "reason": "Retain the imported placement rules and show complete disabled modern alternatives. Presence is not duplicate generation; enabling a second overlapping source is.", + "replacesOnly": [ + {"id": "modern-oil-not-duplicated", "section": "fluid_deposits", "entry": "basepower:fluid_deposit/desert_crude_oil", "block": "basepower:crude_oil"}, + {"id": "modern-lithium-not-duplicated", "section": "ores", "entry": "baseelectric:ore/lithium", "block": "baseelectric:li_ore"}, + {"id": "modern-sulfur-not-duplicated", "section": "ores", "entry": "baseelectric:ore/sulfur", "block": "baseelectric:sulfur_ore"} + ], + "replacement": {"present": true, "field": "enabled", "expectedBoolean": false}, + "historicalAssertionsAndReceiptsRemainUnchanged": true +} 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 7d367c97..856cae43 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 @@ -368,9 +368,9 @@ private static JsonObject respawn(MinecraftServer server, ServerLevel world, Jso ServerLevel destination = server.getLevel(Level.OVERWORLD); platform(destination); if (bedMode.equals("bed")) { - destination.setBlock(SPAWN, Blocks.RED_BED.defaultBlockState().setValue(BedBlock.FACING, Direction.EAST) + destination.setBlock(SPAWN, Blocks.BED.red().defaultBlockState().setValue(BedBlock.FACING, Direction.EAST) .setValue(BedBlock.PART, net.minecraft.world.level.block.state.properties.BedPart.FOOT), 2); - destination.setBlock(SPAWN.east(), Blocks.RED_BED.defaultBlockState().setValue(BedBlock.FACING, Direction.EAST) + destination.setBlock(SPAWN.east(), Blocks.BED.red().defaultBlockState().setValue(BedBlock.FACING, Direction.EAST) .setValue(BedBlock.PART, net.minecraft.world.level.block.state.properties.BedPart.HEAD), 2); } player.setRespawnPosition(new ServerPlayer.RespawnConfig( 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 98ee2939..1b1d05de 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 @@ -584,7 +584,7 @@ private static void turret(List cases) { if (mode != 1) c.power(8, 1, Z, ELECTRICITY, mode == 2 ? 999 : 1500); if (mode == 3) for (int y = 1; y <= 4; y++) for (int z = Z - 2; z <= Z + 2; z++) c.block(12, y, z, "minecraft:stone"); - Husk husk = EntityType.HUSK.create(c.world, net.minecraft.world.entity.EntitySpawnReason.COMMAND); + Husk husk = net.minecraft.world.entity.EntityTypes.HUSK.create(c.world, net.minecraft.world.entity.EntitySpawnReason.COMMAND); c.require(husk != null, "Created a real hostile target"); husk.snapTo(c.at(16, 1, Z).getX() + 0.5, c.at(16, 1, Z).getY(), c.at(16, 1, Z).getZ() + 0.5, 0, 0); husk.setNoAi(true); @@ -639,7 +639,7 @@ private static void turret(List cases) { c.world.getScoreboard().addPlayerToTeam(player.getScoreboardName(), team[0]); c.world.addNewPlayer(player); c.invoke(8, 1, Z, "setOwner", new Class[] {Player.class}, player); - Husk husk = EntityType.HUSK.create(c.world, net.minecraft.world.entity.EntitySpawnReason.COMMAND); + Husk husk = net.minecraft.world.entity.EntityTypes.HUSK.create(c.world, net.minecraft.world.entity.EntitySpawnReason.COMMAND); c.require(husk != null, "Created ownership target"); victim[0] = husk; husk.snapTo(c.at(16, 1, Z).getX() + 0.5, c.at(16, 1, Z).getY(), c.at(16, 1, Z).getZ() + 0.5, 0, 0); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2.java index cc68c409..82f2c855 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2.java @@ -37,8 +37,8 @@ /** Versioned stop-boundary observation. Native no-save helpers are never saved-state witnesses. */ final class Native117StoppedEntitiesSnapshotV2 { - static final String CONTRACT = "native2612-helper-contract.json"; - static final String POLICY = "native2612-leash-knot-nosave-v1"; + static final String CONTRACT = "native262-helper-contract.json"; + static final String POLICY = "native262-leash-knot-nosave-v1"; private static final Map BOOTS = Collections.synchronizedMap(new WeakHashMap<>()); private Native117StoppedEntitiesSnapshotV2() {} @@ -64,7 +64,7 @@ static void validateContract(byte[] bytes) throws IOException { if (control.get("format").getAsInt() != 1 || !POLICY.equals(control.get("policy").getAsString()) || !SharedConstants.getCurrentVersion().name().equals(control.get("minecraftVersion").getAsString()) || SharedConstants.getCurrentVersion().dataVersion().version() != control.get("dataVersion").getAsInt() - || !"64.1.5".equals(control.get("forgeVersion").getAsString())) { + || !"65.1.0".equals(control.get("forgeVersion").getAsString())) { throw new IOException("Native helper control does not describe this pinned engine"); } } catch (RuntimeException error) { @@ -158,7 +158,7 @@ static JsonObject partition(ServerLevel world, Iterable available, Set cases) { static net.minecraft.nbt.CompoundTag nativeChestLock(net.minecraft.core.HolderLookup.Provider registries) { net.minecraft.world.LockCode lock = new net.minecraft.world.LockCode( - net.minecraft.advancements.criterion.ItemPredicate.Builder.item().of( + net.minecraft.advancements.predicates.ItemPredicate.Builder.item().of( registries.lookupOrThrow(net.minecraft.core.registries.Registries.ITEM), Items.STICK).build()); net.minecraft.nbt.CompoundTag patch = new net.minecraft.nbt.CompoundTag(); patch.put("lock", net.minecraft.world.LockCode.CODEC.encodeStart( 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 ef76d4e9..3341af01 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 @@ -21,7 +21,7 @@ import java.util.Set; import java.util.UUID; import net.minecraft.advancements.AdvancementHolder; -import net.minecraft.advancements.CriteriaTriggers; +import net.minecraft.advancements.triggers.CriteriaTriggers; import net.minecraft.commands.Commands; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.inventory.AbstractContainerMenu; 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 3a2310f5..463e0c4a 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 @@ -17,8 +17,8 @@ import java.util.Map; import java.util.UUID; import net.minecraft.advancements.AdvancementHolder; -import net.minecraft.advancements.CriteriaTriggers; -import net.minecraft.advancements.criterion.InventoryChangeTrigger; +import net.minecraft.advancements.triggers.CriteriaTriggers; +import net.minecraft.advancements.triggers.InventoryChangeTrigger; import net.minecraft.commands.Commands; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.item.Item; 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 d9aadb5f..b6172dc7 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 @@ -143,7 +143,7 @@ private static void musket(List cases) { shot.player.getInventory().setItem(1, new ItemStack(cartridge)); } shot.expectedAmmo = mode.equals("infinity") ? 1 : 0; - shot.target = EntityType.COW.create(c.world, net.minecraft.world.entity.EntitySpawnReason.COMMAND); + shot.target = net.minecraft.world.entity.EntityTypes.COW.create(c.world, net.minecraft.world.entity.EntitySpawnReason.COMMAND); c.require(shot.target != null, "Native living target created"); shot.target.getAttribute(Attributes.MAX_HEALTH).setBaseValue(1000); shot.target.setHealth(1000); 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 acd3f417..2aeab3aa 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 @@ -195,7 +195,7 @@ private static void prepareShot(MachineContext c, Shot shot, String id, boolean } private static Cow target(MachineContext c, double x, double y, double z) { - Cow target = EntityType.COW.create(c.world, net.minecraft.world.entity.EntitySpawnReason.COMMAND); + Cow target = net.minecraft.world.entity.EntityTypes.COW.create(c.world, net.minecraft.world.entity.EntitySpawnReason.COMMAND); c.require(target != null, "Native living target is available"); BlockPos origin = c.at(0, 0, 0); target.getAttribute(Attributes.MAX_HEALTH).setBaseValue(1000); diff --git a/test-rig/src/main/resources/META-INF/mods.toml b/test-rig/src/main/resources/META-INF/mods.toml index 843cccb2..dab26fdc 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="[64,65)" +loaderVersion="[65,66)" license="LGPL-3.0-only" [[mods]] @@ -13,20 +13,20 @@ description='''Server-side commissioning harness for the Advantage family. Not f [[dependencies.advantageworkstest]] modId="basepower" mandatory=true -versionRange="[2.4.16.2601021,3.0.0)" +versionRange="[2.4.17.2602001,3.0.0)" ordering="AFTER" side="SERVER" [[dependencies.advantageworkstest]] modId="forge" mandatory=true -versionRange="[64.1.5,65)" +versionRange="[65.1.0,66)" ordering="NONE" side="SERVER" [[dependencies.advantageworkstest]] modId="minecraft" mandatory=true -versionRange="[26.1.2]" +versionRange="[26.2]" ordering="NONE" side="SERVER" diff --git a/test-rig/src/main/resources/assets/advantageworkstest/native262-helper-contract.json b/test-rig/src/main/resources/assets/advantageworkstest/native262-helper-contract.json new file mode 100644 index 00000000..1bebf065 --- /dev/null +++ b/test-rig/src/main/resources/assets/advantageworkstest/native262-helper-contract.json @@ -0,0 +1,55 @@ +{ + "format": 1, + "policy": "native262-leash-knot-nosave-v1", + "minecraftVersion": "26.2", + "dataVersion": 4903, + "forgeVersion": "65.1.0", + "nativeClass": "net.minecraft.world.entity.decoration.LeashFenceKnotEntity", + "registryId": "minecraft:leash_knot", + "canSerialize": false, + "storageSelectorAccepts": true, + "encodedIdPresent": false, + "rootWriterAccepted": false, + "passengerWriterAccepted": false, + "refusedWritersLeaveNoPayload": true, + "observationalUuidPreserved": true, + "observationalStateUnchanged": true, + "observationalTagKeys": [ + "Air", + "CanUpdate", + "Fire", + "Invulnerable", + "Motion", + "OnGround", + "PortalCooldown", + "Pos", + "Rotation", + "UUID", + "fall_distance" + ], + "noPower": true, + "powerAccessTransformerAbsent": true, + "noHarness": true, + "noServer": true, + "noTicks": true, + "nativeMethodsUnmodified": true, + "nativeClassSha256": { + "net.minecraft.world.entity.Entity": "377FCD4776D74C71369F2E695D608BB74D5B349594C499108E5255E8F32F0678", + "net.minecraft.world.entity.EntityType": "629BD57170F29614E4E9217B8B056C7BCC5E951696ECF3BA07AA015D5559ED47", + "net.minecraft.world.entity.decoration.LeashFenceKnotEntity": "E71C9728C1520FA923FFB3FE07B62622F02D22013F5C4475AF24508A273C316A", + "net.minecraft.util.datafix.fixes.BlockStateData": "3192DA9F2107E79270B73FB742CBDB1CB6BCD2B5228C579757E3C899A5C8E51A" + }, + "engineJarSha256": "7C324FAF36362EDB1E1879EA11536CEA7F6E58BE3A0E783C13607BE7D7364DD6", + "engineSourcesSha256": "D06D76658C43DC91D8EB5BF0C1A519E261B84AAC17940E64E7050449D9DA86D9", + "nativeSourceSha256": { + "net/minecraft/world/entity/Entity.java": "D0451E3BDF7F730064C80071391EA14E1B7919030D12B00E845B748236F29C8F", + "net/minecraft/world/entity/EntityType.java": "6FDA0DB78417461E043400E5BB863B3EAC155D6711F65B8C0D41DFB8A14ADB83", + "net/minecraft/world/entity/decoration/LeashFenceKnotEntity.java": "B482036CCD277BC69229A4D89C33C3F36895D9138F231C445DD6DAE1BCE94DB2", + "net/minecraft/util/datafix/fixes/BlockStateData.java": "6196F9039890D5F67B59CB372EC06DA987E8A5EA80B49FD8BEE9EECA0A1784A4" + }, + "controlSourceSha256": { + "platform-controls/NativePlatformSupport.java": "C3A2E65822218805CCC090955334523B78BED1A225AC5ACC5C9625CA95BA50DE", + "platform-controls/NativeLeashKnotPlatformControl.java": "3A86FC03838D1ED1B27F88AAB36054004793B7851B5AE1742EC03C92E9B54276", + "unit-support/zone/moddev/mc/advantageworks/unit/NativeEmptyEntityTracker.java": "CEB0DFC585351DAC03FA2BDD0ED119152CCA00F341FBC7AE3FD0B1C6C2C40C06" + } +} 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 fbffc8d4..8edb6867 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 @@ -65,6 +65,26 @@ public final class WorksRunner { private WorksRunner() { } + private record CalendarTarget(String minecraft, String forge, String suffix, String id, + String shimHash, String installerHash, String loggingHash) { + String shim() { return "forge-" + forge + "-shim.jar"; } + String loggingPrefix() { return "forge" + forge.split("-")[1].split("\\.")[0]; } + } + + private static final List CALENDAR_TARGETS = List.of( + new CalendarTarget("26.1.2", "26.1.2-64.1.5", ".2601021", "native2612", + "0C74CB2A8E7A71AEB7055396C1E25864584A2D837D7DD42136A06D0B41ADD545", + "6E01323837051176762BD466340A601DA122F179FE6A00AC616E05429AE07648", + "7299C7AF7AE427C44C9AE83A92508F7BB09764DC30D21A4E8E86C7FFF2B0C4BC"), + new CalendarTarget("26.2", "26.2-65.1.0", ".2602001", "native262", + "8D113D3DFFF0F0978A1989E22A8040FAD0FE5E332EF766B3702EB29687C9F4D2", + "813FBBFECC3086A492783B9F5023AE15007737B42E17AAB75F48BA77403314DF", + "7D1A733519C86FE2665110BF660DCF4B7358B97F416975444906E5CC77F163BE")); + + private static CalendarTarget calendarTarget(String minecraft) { + return CALENDAR_TARGETS.stream().filter(target -> target.minecraft.equals(minecraft)).findFirst().orElse(null); + } + public static void main(String[] args) throws Exception { Arguments arguments = Arguments.parse(args); Profile profile = readRunnerProfile(arguments.profile); @@ -80,10 +100,11 @@ public static void main(String[] args) throws Exception { if ("packaged".equals(profile.mode)) { runtimeJavaHome(profile); if (profile.preparedSeed != null) { - if ("26.1.2".equals(profile.minecraftVersion)) { + if (calendarTarget(profile.minecraftVersion) != null) { requireModularLauncher(profile.preparedSeed.root.toFile(), serverLauncherPath(profile)); - native2612LoggingBytes(profile.preparedSeed.root.resolve( - "libraries/net/minecraftforge/fmlloader/26.1.2-64.1.5/fmlloader-26.1.2-64.1.5.jar").toFile()); + nativeLoggingBytes(profile.preparedSeed.root.resolve( + "libraries/net/minecraftforge/fmlloader/" + profile.forge.version + + "/fmlloader-" + profile.forge.version + ".jar").toFile(), calendarTarget(profile.minecraftVersion)); } else packagedCommand(profile, profile.preparedSeed.root.toFile()); } } @@ -316,8 +337,8 @@ static List packagedCommand(Profile profile, File runtime) throws Except try (ZipFile jar = new ZipFile(loggingJar)) { if (jar.getEntry("log4j2.xml") == null) throw new IllegalStateException("Forge logging configuration missing"); } - String logging = "26.1.2".equals(profile.minecraftVersion) - ? native2612LoggingConfiguration(runtime, loggingJar) + String logging = calendarTarget(profile.minecraftVersion) != null + ? nativeLoggingConfiguration(runtime, loggingJar, calendarTarget(profile.minecraftVersion)) : "jar:" + loggingJar.toURI().toASCIIString() + "!/log4j2.xml"; return Arrays.asList(java.getAbsolutePath(), "-Xms512M", "-Xmx2G", "--add-opens", "java.base/java.lang.invoke=ALL-UNNAMED", @@ -326,7 +347,11 @@ static List packagedCommand(Profile profile, File runtime) throws Except } static byte[] native2612LoggingBytes(File loggingJar) throws Exception { - String sourceJarSha = "7299C7AF7AE427C44C9AE83A92508F7BB09764DC30D21A4E8E86C7FFF2B0C4BC"; + return nativeLoggingBytes(loggingJar, calendarTarget("26.1.2")); + } + + private static byte[] nativeLoggingBytes(File loggingJar, CalendarTarget target) throws Exception { + String sourceJarSha = target.loggingHash; String sourceXmlSha = "9680BAF9F936C8CD009CD3A8D75253B476FCA9C316F8D5B5C51700053BEAE440"; if (!sourceJarSha.equals(sha256(loggingJar))) throw new IllegalArgumentException("Forge 64 logging jar changed"); byte[] original; @@ -341,11 +366,19 @@ static byte[] native2612LoggingBytes(File loggingJar) throws Exception { } static String native2612LoggingConfiguration(File runtime, File loggingJar) throws Exception { - byte[] modified = native2612LoggingBytes(loggingJar); - Path config = runtime.toPath().resolve("forge64-log4j2-plain-stacktraces.xml"); - Path custody = runtime.toPath().resolve("forge64-logging-custody.json"); + return nativeLoggingConfiguration(runtime, loggingJar, calendarTarget("26.1.2")); + } + + static String native262LoggingConfiguration(File runtime, File loggingJar) throws Exception { + return nativeLoggingConfiguration(runtime, loggingJar, calendarTarget("26.2")); + } + + private static String nativeLoggingConfiguration(File runtime, File loggingJar, CalendarTarget target) throws Exception { + byte[] modified = nativeLoggingBytes(loggingJar, target); + Path config = runtime.toPath().resolve(target.loggingPrefix() + "-log4j2-plain-stacktraces.xml"); + Path custody = runtime.toPath().resolve(target.loggingPrefix() + "-logging-custody.json"); Map receipt = new LinkedHashMap<>(); - receipt.put("sourceJarSha256", "7299C7AF7AE427C44C9AE83A92508F7BB09764DC30D21A4E8E86C7FFF2B0C4BC"); + receipt.put("sourceJarSha256", target.loggingHash); receipt.put("sourceXmlSha256", "9680BAF9F936C8CD009CD3A8D75253B476FCA9C316F8D5B5C51700053BEAE440"); receipt.put("configSha256", java.util.HexFormat.of().withUpperCase().formatHex(MessageDigest.getInstance("SHA-256").digest(modified))); receipt.put("modification", "Exactly two %xEx renderers to %ex; all other source bytes retained"); @@ -367,12 +400,14 @@ static void requireModularLauncher(File runtime, String launcher) throws Excepti Path args = root.resolve(launcher); PreparedServerSeed.requirePlain(args); boolean forge61 = launcher.startsWith("libraries/net/minecraftforge/forge/1.21.11-61.1.0/"); - if (launcher.startsWith("libraries/net/minecraftforge/forge/26.1.2-64.1.5/")) { - Path jar = root.resolve("forge-26.1.2-64.1.5-shim.jar"); + CalendarTarget calendar = CALENDAR_TARGETS.stream().filter(target -> + launcher.startsWith("libraries/net/minecraftforge/forge/" + target.forge + "/")).findFirst().orElse(null); + if (calendar != null) { + Path jar = root.resolve(calendar.shim()); PreparedServerSeed.requirePlain(jar); if (!Files.readString(args, StandardCharsets.UTF_8).strip().equals( - "-Djava.net.preferIPv6Addresses=system -XX:+UseCompactObjectHeaders -jar forge-26.1.2-64.1.5-shim.jar") - || !sha256(jar.toFile()).equals("0C74CB2A8E7A71AEB7055396C1E25864584A2D837D7DD42136A06D0B41ADD545")) + "-Djava.net.preferIPv6Addresses=system -XX:+UseCompactObjectHeaders -jar " + calendar.shim()) + || !sha256(jar.toFile()).equals(calendar.shimHash)) throw new IllegalArgumentException("Unexpected selected Forge 64 shim command"); return; } @@ -866,8 +901,8 @@ static PreparedServerSeed preparedServerSeed(File testRig, Profile profile, Argu } return new PreparedServerSeed(preparedPath(testRig, arguments.preparedServerSeed), preparedPath(testRig, arguments.preparedServerSeedInventory), arguments.preparedServerSeedSha256, - serverLauncherPath(profile), "26.1.2".equals(profile.minecraftVersion) - ? "forge-26.1.2-64.1.5-shim.jar" + serverLauncherPath(profile), calendarTarget(profile.minecraftVersion) != null + ? calendarTarget(profile.minecraftVersion).shim() : "1.21.11".equals(profile.minecraftVersion) ? "forge-1.21.11-61.1.0-shim.jar" : "1.21.1".equals(profile.minecraftVersion) @@ -922,13 +957,15 @@ static final class PreparedServerSeed { final Set directories = new LinkedHashSet<>(); final List processes = new ArrayList<>(); final boolean forge50, forge52, forge61, forge64; + final CalendarTarget calendar; PreparedServerSeed(Path root, Path inventory, String expectedHash, String launcher, String minecraft) throws Exception { forge50 = "forge-1.20.6-50.2.0-shim.jar".equals(minecraft); forge52 = "forge-1.21.1-52.1.0-shim.jar".equals(minecraft); forge61 = "forge-1.21.11-61.1.0-shim.jar".equals(minecraft); - forge64 = "forge-26.1.2-64.1.5-shim.jar".equals(minecraft); + calendar = CALENDAR_TARGETS.stream().filter(target -> target.shim().equals(minecraft)).findFirst().orElse(null); + forge64 = calendar != null; this.root = root.toAbsolutePath().normalize(); this.inventory = inventory.toAbsolutePath().normalize(); if (expectedHash == null || !expectedHash.matches("[0-9A-F]{64}")) { @@ -998,8 +1035,8 @@ static final class PreparedServerSeed { reader.endObject(); if (forge64) { Map expected = new LinkedHashMap<>(); - expected.put("forge", "26.1.2-64.1.5"); - expected.put("installerSha256", "6E01323837051176762BD466340A601DA122F179FE6A00AC616E05429AE07648"); + expected.put("forge", calendar.forge); + expected.put("installerSha256", calendar.installerHash); expected.put("javaSha256", "D5F1B9E03E298EBFB04D2326ECC1E2AF3E6817F9B4D7F9F151FE856D56975650"); expected.put("javaVersion", "25.0.3+9"); for (var entry : expected.entrySet()) { @@ -1223,12 +1260,12 @@ private static boolean modularForge(Profile profile) { return "1.17.1".equals(profile.minecraftVersion) || "1.18.2".equals(profile.minecraftVersion) || "1.20.1".equals(profile.minecraftVersion) || "1.20.6".equals(profile.minecraftVersion) || "1.21.1".equals(profile.minecraftVersion) || "1.21.11".equals(profile.minecraftVersion) - || "26.1.2".equals(profile.minecraftVersion); + || calendarTarget(profile.minecraftVersion) != null; } private static File runtimeJavaHome(Profile profile) throws Exception { if (!modularForge(profile)) return java8Home(profile); - if ("26.1.2".equals(profile.minecraftVersion)) { + if (calendarTarget(profile.minecraftVersion) != null) { requireNative2612Environment(System.getenv()); String configured = profile.java25Home; if (configured == null || configured.isBlank()) configured = System.getenv("ADVANTAGE_WORKS_JAVA25_HOME"); @@ -1287,6 +1324,10 @@ static void requireNative12111Environment(Map environment) { } } + static void requireNative262Environment(Map environment) { + requireNative2612Environment(environment); + } + static void requireNative2612Environment(Map environment) { for (String key : List.of("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS", "CLASSPATH")) { if (environment.get(key) != null && !environment.get(key).isBlank()) @@ -1382,7 +1423,7 @@ static void writeRuntimeFiles(File runtime, Profile profile) throws Exception { + "spawn-npcs=false\n" + "spawn-protection=0\n" + "view-distance=10\n"; - if ("1.21.11".equals(profile.minecraftVersion) || "26.1.2".equals(profile.minecraftVersion)) { + if ("1.21.11".equals(profile.minecraftVersion) || calendarTarget(profile.minecraftVersion) != null) { properties += "pause-when-empty-seconds=-1\n"; } write(new File(runtime, "server.properties"), properties); @@ -1470,6 +1511,9 @@ static Profile readNative12111Profile(File file) throws Exception { static Profile readRunnerProfile(File file) throws Exception { try (InputStreamReader reader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) { JsonObject metadata = GSON.fromJson(reader, JsonObject.class); + if (metadata != null && metadata.has("minecraftVersion") + && "26.2".equals(metadata.get("minecraftVersion").getAsString())) + return readNative262Profile(file); if (metadata != null && metadata.has("minecraftVersion") && "26.1.2".equals(metadata.get("minecraftVersion").getAsString())) return readNative2612Profile(file); @@ -1492,6 +1536,17 @@ static Profile readNative2612Profile(File file) throws Exception { return profile; } + static Profile readNative262Profile(File file) throws Exception { + Profile profile = readNativeShimProfile(file, "26.2", "26.2-65.1.0", ".2602001"); + if (!"build/works-native262".equals(profile.runtimeBase)) + throw new IllegalArgumentException("Forge 65 must use isolated build/works-native262 runtimes"); + Set ids = new LinkedHashSet<>(); + profile.mods.forEach(input -> ids.add(input.id)); + if (!ids.containsAll(Set.of("basepower", "advantageworkstest", "basemetals", "orespawn"))) + throw new IllegalArgumentException("Forge 65 requires the complete pinned dependency family"); + return profile; + } + private static Profile readNativeShimProfile(File file, String minecraft, String forge, String suffix) throws Exception { Profile profile = readProfile(file, minecraft, forge); if (!"EXPLORATORY_UNQUALIFIED".equals(profile.qualificationStatus) || !"packaged".equals(profile.mode)) { 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 cb4d13a1..2e57ebb6 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 @@ -88,6 +88,7 @@ static ServerLevel world(ResourceKey key) throws Exception { Field field = unsafe.getDeclaredField("theUnsafe"); field.setAccessible(true); ServerLevel world = (ServerLevel) unsafe.getMethod("allocateInstance", Class.class) .invoke(field.get(null), EntityFixtureWorld.class); + zone.moddev.mc.advantageworks.unit.NativeEmptyEntityTracker.install(world); set(world, "dimension", key); set(world, "random", net.minecraft.util.RandomSource.create(0)); Field ticks = ServerLevel.class.getDeclaredField("entityTickList"); ticks.setAccessible(true); @@ -96,7 +97,7 @@ static ServerLevel world(ResourceKey key) throws Exception { } static Boat boat(ServerLevel world) { - return new Boat(EntityType.OAK_BOAT, world, () -> net.minecraft.world.item.Items.OAK_BOAT); + return new Boat(net.minecraft.world.entity.EntityTypes.OAK_BOAT, world, () -> net.minecraft.world.item.Items.OAK_BOAT); } private static void set(ServerLevel world, String name, Object value) throws Exception { 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 228cb4f8..a347447f 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 @@ -95,7 +95,8 @@ public class Native117StoppedEntitiesSnapshotTest { ItemEntity entity = new ItemEntity(world, -0.25, 80, 8.5, new ItemStack(Items.DIRT, 7)); entity.getPersistentData().putInt("Private", 42); CompoundTag saved = Native117StoppedEntitiesSnapshot.serialized(entity); - Entity loaded = EntityType.loadEntityRecursive(saved.copy(), world, EntitySpawnReason.LOAD, value -> value); + Entity loaded = EntityType.loadEntityRecursive(saved.copy(), world, + new net.minecraft.world.entity.EntitySpawnRequest(EntitySpawnReason.LOAD, false), value -> value); assertNotNull(loaded); assertEquals(saved, Native117StoppedEntitiesSnapshot.serialized(loaded)); } @@ -124,7 +125,7 @@ public class Native117StoppedEntitiesSnapshotTest { @Test public void nativeWriterRefusalIsAFailureNotASilentSkippedEntity() throws Exception { ServerLevel world = Native117EntityContextTest.world(Level.OVERWORLD); - LightningBolt transientEntity = new LightningBolt(EntityType.LIGHTNING_BOLT, world); + LightningBolt transientEntity = new LightningBolt(net.minecraft.world.entity.EntityTypes.LIGHTNING_BOLT, world); try { Native117StoppedEntitiesSnapshot.entities(world, List.of(transientEntity), new HashSet<>()); fail("Native writer refusal was hidden"); diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2Test.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2Test.java index 80b1c5f9..a0d13d16 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2Test.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native117StoppedEntitiesSnapshotV2Test.java @@ -48,7 +48,7 @@ public class Native117StoppedEntitiesSnapshotV2Test { @Test public void unexpectedNoSaveTypeFailsWithActualNativeIdentity() throws Exception { ServerLevel world = Native117EntityContextTest.world(Level.END); - LightningBolt bolt = new LightningBolt(EntityType.LIGHTNING_BOLT, world); + LightningBolt bolt = new LightningBolt(net.minecraft.world.entity.EntityTypes.LIGHTNING_BOLT, world); try { Native117StoppedEntitiesSnapshotV2.partition(world, List.of(bolt), new HashSet<>()); fail("Unproven no-save type was skipped"); diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native12111HelperContractTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native12111HelperContractTest.java index b3ebaad8..f90bc301 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native12111HelperContractTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native12111HelperContractTest.java @@ -24,7 +24,7 @@ import org.junit.Test; public class Native12111HelperContractTest { - private static final String SHA = "C2407DA9E3785032E26FCFB739133B2F4DEA8F1B537AD81F8AC562E2A61A59E8"; + private static final String SHA = "272085E5730D6C95525017714BC496BC6AF358F35ED6FDF8678A428F740D4EF5"; @BeforeClass public static void bootstrap() { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); } @@ -32,10 +32,10 @@ public class Native12111HelperContractTest { byte[] bytes = Native117StoppedEntitiesSnapshotV2.contract(); assertEquals(SHA, Native117StoppedEntitiesSnapshotV2.sha(bytes)); JsonObject control = JsonParser.parseString(new String(bytes, StandardCharsets.UTF_8)).getAsJsonObject(); - assertEquals("native2612-leash-knot-nosave-v1", control.get("policy").getAsString()); + assertEquals("native262-leash-knot-nosave-v1", control.get("policy").getAsString()); assertEquals(SharedConstants.getCurrentVersion().name(), control.get("minecraftVersion").getAsString()); assertEquals(SharedConstants.getCurrentVersion().dataVersion().version(), control.get("dataVersion").getAsInt()); - assertEquals("64.1.5", control.get("forgeVersion").getAsString()); + assertEquals("65.1.0", control.get("forgeVersion").getAsString()); for (String key : List.of("noPower", "powerAccessTransformerAbsent", "noHarness", "noServer", "noTicks", "nativeMethodsUnmodified", "observationalUuidPreserved", "observationalStateUnchanged", "refusedWritersLeaveNoPayload")) { assertTrue(key, control.get(key).getAsBoolean()); @@ -67,7 +67,7 @@ public class Native12111HelperContractTest { } } for (var entry : control.getAsJsonObject("controlSourceSha256").entrySet()) { - assertEquals(entry.getKey(), entry.getValue().getAsString(), sha(Files.readAllBytes(Path.of("platform-controls", entry.getKey())))); + assertEquals(entry.getKey(), entry.getValue().getAsString(), sha(Files.readAllBytes(Path.of(entry.getKey())))); } } @@ -81,7 +81,7 @@ public class Native12111HelperContractTest { var knot = new LeashFenceKnotEntity(world, new BlockPos(154, 100, 8)); var before = NativeActorIo.saveWithoutId(knot).copy(); assertTrue(knot.shouldBeSaved()); - assertFalse(EntityType.LEASH_KNOT.canSerialize()); + assertFalse(net.minecraft.world.entity.EntityTypes.LEASH_KNOT.canSerialize()); assertNull(knot.getEncodeId()); TagValueOutput root = TagValueOutput.createWithContext(NativeActorIo.reporter("root control"), world.registryAccess()); TagValueOutput passenger = TagValueOutput.createWithContext(NativeActorIo.reporter("passenger control"), world.registryAccess()); diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeActorIoTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeActorIoTest.java index 1421854f..19601320 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeActorIoTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeActorIoTest.java @@ -30,7 +30,7 @@ public class NativeActorIoTest { assertEquals("minecraft:item", saved.getString("id").orElseThrow()); assertEquals(original.getUUID(), saved.read("UUID", UUIDUtil.CODEC).orElseThrow()); assertFalse(NativeActorIo.saveWithoutId(original).contains("id")); - ItemEntity loaded = new ItemEntity(EntityType.ITEM, world); + ItemEntity loaded = new ItemEntity(net.minecraft.world.entity.EntityTypes.ITEM, world); NativeActorIo.load(loaded, saved); assertEquals(saved, NativeActorIo.save(loaded)); assertEquals(7, loaded.getItem().getCount()); @@ -48,7 +48,7 @@ public class NativeActorIoTest { @Test public void codecEncodeProblemThrowsWithItsNestedPath() throws Exception { ServerLevel world = Native117EntityContextTest.world(Level.OVERWORLD); - ItemEntity broken = new ItemEntity(EntityType.ITEM, world) { + ItemEntity broken = new ItemEntity(net.minecraft.world.entity.EntityTypes.ITEM, world) { @Override public void saveWithoutId(ValueOutput output) { output.child("outer").store("broken", Codec.intRange(0, 1), 2); } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeEntityReadSelectorsTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeEntityReadSelectorsTest.java index 54254c06..ae927a28 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeEntityReadSelectorsTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeEntityReadSelectorsTest.java @@ -87,7 +87,7 @@ public NativeEntityReadSelectorsTest(String selector, String scenario) { private static final class ObservedBoat extends Boat { int persistentReads; int serializations; - ObservedBoat(Level world) { super(EntityType.OAK_BOAT, world, () -> net.minecraft.world.item.Items.OAK_BOAT); } + ObservedBoat(Level world) { super(net.minecraft.world.entity.EntityTypes.OAK_BOAT, world, () -> net.minecraft.world.item.Items.OAK_BOAT); } @Override public CompoundTag getPersistentData() { persistentReads++; return super.getPersistentData(); } @Override public void saveWithoutId(ValueOutput output) { serializations++; super.saveWithoutId(output); } } diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native262ProfileTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native262ProfileTest.java new file mode 100644 index 00000000..44aa9ede --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native262ProfileTest.java @@ -0,0 +1,86 @@ +package zone.moddev.mc.advantageworks.runner; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonArray; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import static org.junit.Assert.*; + +public class Native262ProfileTest { + @Rule public TemporaryFolder temporary = new TemporaryFolder(); + + private JsonObject profile() { + JsonObject result = new Gson().fromJson("{\"name\":\"native262-test\",\"mode\":\"packaged\"," + + "\"minecraftVersion\":\"26.2\",\"runtimeBase\":\"build/works-native262\"," + + "\"portStatus\":\"PORTED_UNQUALIFIED\",\"qualificationStatus\":\"EXPLORATORY_UNQUALIFIED\"," + + "\"forge\":{\"version\":\"26.2-65.1.0\"}}", JsonObject.class); + JsonArray mods = new JsonArray(); + result.add("mods", mods); + for (var entry : Map.of("basepower", "2.4.17.2602001", "advantageworkstest", "1.1.7.2602001", + "basemetals", "3.1.2.2602001", "orespawn", "4.1.1.2602001").entrySet()) { + JsonObject input = new JsonObject(); + input.addProperty("id", entry.getKey()); + input.addProperty("version", entry.getValue()); + input.addProperty("file", entry.getKey() + "-" + entry.getValue() + ".jar"); + mods.add(input); + } + return result; + } + + private Path save(JsonObject profile) throws Exception { + Path path = temporary.newFile().toPath(); + Files.writeString(path, new Gson().toJson(profile)); + return path; + } + + @Test public void routesExplicit26ProfileWithoutCoercingHistoricalProfile() throws Exception { + WorksRunner.Profile actual = WorksRunner.readRunnerProfile(save(profile()).toFile()); + assertEquals("26.2", actual.minecraftVersion); + assertEquals("build/works-native262", actual.runtimeBase); + assertTrue(WorksRunner.serverLauncherPath(actual).contains("26.2-65.1.0")); + var old = profile(); + old.addProperty("minecraftVersion", "1.21.11"); + assertThrows(IllegalArgumentException.class, () -> WorksRunner.readNative262Profile(save(old).toFile())); + } + + @Test public void selectedForgeAndIsolatedOutputCannotDrift() throws Exception { + var wrongForge = profile(); + wrongForge.getAsJsonObject("forge").addProperty("version", "26.2-65.1.5"); + assertThrows(IllegalArgumentException.class, () -> WorksRunner.readNative262Profile(save(wrongForge).toFile())); + var oldOutput = profile(); + oldOutput.addProperty("runtimeBase", "build/works-native12111"); + assertThrows(IllegalArgumentException.class, () -> WorksRunner.readNative262Profile(save(oldOutput).toFile())); + } + + @Test public void missingMandatoryBaseMetalsCannotBeAPowerOnlyPass() throws Exception { + var missing = profile(); + var mods = missing.getAsJsonArray("mods"); + for (int index = mods.size() - 1; index >= 0; index--) { + if (mods.get(index).getAsJsonObject().get("id").getAsString().equals("basemetals")) mods.remove(index); + } + assertThrows(IllegalArgumentException.class, () -> WorksRunner.readNative262Profile(save(missing).toFile())); + } + + @Test public void inheritedJvmOptionsAndClasspathAreRejected() { + WorksRunner.requireNative262Environment(Map.of()); + for (String key : new String[] {"JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS", "CLASSPATH"}) { + assertThrows(key, IllegalArgumentException.class, + () -> WorksRunner.requireNative262Environment(Map.of(key, "injected"))); + } + } + + @Test public void alteredLoggingSourceIsNotSilentlyAdapted() throws Exception { + Path runtime = temporary.newFolder().toPath(); + Path jar = temporary.newFile("altered-loader.jar").toPath(); + Files.write(jar, new byte[] {1, 2, 3}); + assertThrows(IllegalArgumentException.class, + () -> WorksRunner.native262LoggingConfiguration(runtime.toFile(), jar.toFile())); + assertFalse(Files.exists(runtime.resolve("forge64-log4j2-plain-stacktraces.xml"))); + assertFalse(Files.exists(runtime.resolve("forge64-logging-custody.json"))); + } +} diff --git a/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/NativeEmptyEntityTracker.java b/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/NativeEmptyEntityTracker.java index 183d3a32..8d3f002a 100644 --- a/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/NativeEmptyEntityTracker.java +++ b/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/NativeEmptyEntityTracker.java @@ -30,8 +30,14 @@ public static void install(ServerLevel world) throws ReflectiveOperationExceptio } private static void set(Object owner, String name, Object value) throws ReflectiveOperationException { - Field field = owner.getClass().getDeclaredField(name); - field.setAccessible(true); - field.set(owner, value); + for (Class type = owner.getClass(); type != null; type = type.getSuperclass()) { + try { + Field field = type.getDeclaredField(name); + field.setAccessible(true); + field.set(owner, value); + return; + } catch (NoSuchFieldException absent) { } + } + throw new NoSuchFieldException(name); } }