diff --git a/.gitattributes b/.gitattributes index 0fc4431..14ae407 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,6 @@ *.ogg binary *.jar binary *.zip binary +# Preserve the exact atlas bytes used by the frozen native client candidate. +src/main/resources/assets/minecraft/atlases/blocks.json -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol +src/main/resources/assets/minecraft/atlases/items.json -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol diff --git a/.github/verify-published-pins.sh b/.github/verify-published-pins.sh index 40fb118..eadb217 100644 --- a/.github/verify-published-pins.sh +++ b/.github/verify-published-pins.sh @@ -18,10 +18,10 @@ done require_merged() { local repo="$1" ref="$2" status [[ "$ref" =~ ^[a-f0-9]{40}$ ]] - status="$(gh api "repos/MinecraftModDevelopmentMods/$repo/compare/$ref...master-1.21.11" --jq '.status')" + status="$(gh api "repos/MinecraftModDevelopmentMods/$repo/compare/$ref...master-26.1.2" --jq '.status')" case "$status" in identical|ahead) ;; - *) echo "::error::$repo $ref is not on upstream master-1.21.11 ($status)."; exit 1 ;; + *) echo "::error::$repo $ref is not on upstream master-26.1.2 ($status)."; exit 1 ;; esac } @@ -48,7 +48,7 @@ require_merged basePower "$power_ref" require_merged BaseMetals "$(value basemetals_source_commit)" require_merged OreSpawn "$orespawn_ref" -inputs="${RUNNER_TEMP:?}/forge61-release-dependencies" +inputs="${RUNNER_TEMP:?}/forge64-release-dependencies" mkdir -p "$inputs" power="$inputs/basePower-$(value poweradvantage_version).jar" metals="$inputs/BaseMetals-$(value basemetals_version).jar" @@ -57,8 +57,8 @@ public_jar basePower 1780140 "$(value poweradvantage_public_file_id)" "$(value p public_jar BaseMetals 240967 "$(value basemetals_public_file_id)" "$(value basemetals_deobf_sha256)" "$metals" public_jar OreSpawn 245586 "$(value orespawn_public_file_id)" "$(value orespawn_deobf_sha256)" "$orespawn" -installer="$inputs/forge-1.21.11-61.1.0-installer.jar" -download 'https://maven.minecraftforge.net/net/minecraftforge/forge/1.21.11-61.1.0/forge-1.21.11-61.1.0-installer.jar' \ +installer="$inputs/forge-26.1.2-64.1.5-installer.jar" +download 'https://maven.minecraftforge.net/net/minecraftforge/forge/26.1.2-64.1.5/forge-26.1.2-64.1.5-installer.jar' \ "$(value forge_installer_sha256)" "$installer" { printf 'power_ref=%s\n' "$power_ref" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb7544f..fc08f8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,16 @@ -name: baseElectric 1.21.11 CI +name: baseElectric 26.1.2 CI on: push: - branches: [master-1.21.11, 'feature/**'] + branches: [master-26.1.2, 'feature/**'] pull_request: - branches: [master-1.21.11] + branches: [master-26.1.2] permissions: contents: read concurrency: - group: baseelectric-1.21.11-${{ github.workflow }}-${{ github.ref }} + group: baseelectric-26.1.2-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -40,29 +40,19 @@ jobs: repository: MinecraftModDevelopmentMods/basePower ref: ${{ steps.public-pins.outputs.power_ref }} path: .ci-dependencies/basePower - - name: Install Java 25 for Mavenizer + - name: Install Java 25 for all build and runtime roles uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '25.0.3+9.0.LTS' - - name: Install Java 8 for the launcher helper - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 - with: - distribution: temurin - java-version: '8.0.502+7' - - name: Install Java 21 for Gradle and production - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 - with: - distribution: temurin - java-version: '21.0.7+6.0.LTS' - name: Use pinned build toolchains run: >- - echo "MMD_GRADLE_TOOLCHAINS=-Porg.gradle.java.installations.paths=$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64 + echo "MMD_GRADLE_TOOLCHAINS=-Porg.gradle.java.installations.paths=$JAVA_HOME -Porg.gradle.java.installations.auto-detect=false -Porg.gradle.java.installations.auto-download=false" >> "$GITHUB_ENV" - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 - - name: Verify the Forge 61 build inputs + - name: Verify the Forge 64 build inputs shell: bash env: ORESPAWN_REF: ${{ steps.public-pins.outputs.orespawn_ref }} @@ -74,33 +64,34 @@ jobs: test "$(git -C "$orespawn" rev-parse HEAD)" = "$ORESPAWN_REF" test "$(git -C "$power" rev-parse HEAD)" = "$POWER_REF" mavenizer="$orespawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar" - printf '%s %s\n' 'B36CA046DB72E30F27BD38E02B4AE34274ADB51AE1D65BAF9E1D07763755682E' "$mavenizer" | sha256sum -c - + printf '%s %s\n' 'BB0DACB3DEB2A711CA67DD402D5FDAB5A9847C37C7B9B596DEC7CF37378FA085' "$mavenizer" | sha256sum -c - echo "MAVENIZER=$mavenizer" >> "$GITHUB_ENV" - name: Build and audit release artifacts shell: bash run: | ./gradlew $MMD_GRADLE_TOOLCHAINS clean check build javadoc verifyReleaseDependencies verifyReleaseArtifacts \ writeReleaseChecksums verifyEclipseProductionClasspath --no-build-cache --no-daemon --stacktrace \ - "-Pforge61MavenizerJar=$MAVENIZER" "-PpowerAdvantageDeobfJar=$POWER_PLAYER" \ + "-Pforge64MavenizerJar=$MAVENIZER" "-PpowerAdvantageDeobfJar=$POWER_PLAYER" \ + "-PpowerUnitSource=$GITHUB_WORKSPACE/.ci-dependencies/basePower" \ "-PbaseMetalsDeobfJar=$METALS_PLAYER" "-PoreSpawnDeobfJar=$ORESPAWN_PLAYER" \ "-PforgeInstallerJar=$FORGE_INSTALLER" - name: Upload audited release candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: baseElectric-1.21.11-${{ github.sha }} + name: baseElectric-26.1.2-${{ github.sha }} if-no-files-found: error retention-days: 30 path: | - build/libs/baseElectric-2.2.16.121111.jar - build/libs/baseElectric-2.2.16.121111-sources.jar - build/libs/baseElectric-2.2.16.121111-javadoc.jar + build/libs/baseElectric-2.2.17.2601021.jar + build/libs/baseElectric-2.2.17.2601021-sources.jar + build/libs/baseElectric-2.2.17.2601021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt - name: Upload diagnostics on failure if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: baseElectric-1.21.11-diagnostics-${{ github.sha }} + name: baseElectric-26.1.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 d0771d4..22f5431 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,9 +2,9 @@ name: CodeQL on: push: - branches: [master-1.21.11, 'feature/**'] + branches: [master-26.1.2, 'feature/**'] pull_request: - branches: [master-1.21.11] + branches: [master-26.1.2] permissions: contents: read @@ -12,7 +12,7 @@ permissions: security-events: write concurrency: - group: baseelectric-1.21.11-${{ github.workflow }}-${{ github.ref }} + group: baseelectric-26.1.2-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -42,29 +42,19 @@ jobs: repository: MinecraftModDevelopmentMods/basePower ref: ${{ steps.public-pins.outputs.power_ref }} path: .ci-dependencies/basePower - - name: Install Java 25 for Mavenizer + - name: Install Java 25 for all build and runtime roles uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '25.0.3+9.0.LTS' - - name: Install Java 8 for the launcher helper - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 - with: - distribution: temurin - java-version: '8.0.502+7' - - name: Install Java 21 for Gradle and production - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 - with: - distribution: temurin - java-version: '21.0.7+6.0.LTS' - name: Use pinned build toolchains run: >- - echo "MMD_GRADLE_TOOLCHAINS=-Porg.gradle.java.installations.paths=$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64 + echo "MMD_GRADLE_TOOLCHAINS=-Porg.gradle.java.installations.paths=$JAVA_HOME -Porg.gradle.java.installations.auto-detect=false -Porg.gradle.java.installations.auto-download=false" >> "$GITHUB_ENV" - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 - - name: Verify the Forge 61 build inputs + - name: Verify the Forge 64 build inputs shell: bash env: ORESPAWN_REF: ${{ steps.public-pins.outputs.orespawn_ref }} @@ -76,7 +66,7 @@ jobs: test "$(git -C "$orespawn" rev-parse HEAD)" = "$ORESPAWN_REF" test "$(git -C "$power" rev-parse HEAD)" = "$POWER_REF" mavenizer="$orespawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar" - printf '%s %s\n' 'B36CA046DB72E30F27BD38E02B4AE34274ADB51AE1D65BAF9E1D07763755682E' "$mavenizer" | sha256sum -c - + printf '%s %s\n' 'BB0DACB3DEB2A711CA67DD402D5FDAB5A9847C37C7B9B596DEC7CF37378FA085' "$mavenizer" | sha256sum -c - echo "MAVENIZER=$mavenizer" >> "$GITHUB_ENV" - name: Initialize CodeQL uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 @@ -87,7 +77,8 @@ jobs: shell: bash run: | ./gradlew $MMD_GRADLE_TOOLCHAINS classes verifyReleaseDependencies --rerun-tasks --no-build-cache --no-daemon --stacktrace \ - "-Pforge61MavenizerJar=$MAVENIZER" "-PpowerAdvantageDeobfJar=$POWER_PLAYER" \ + "-Pforge64MavenizerJar=$MAVENIZER" "-PpowerAdvantageDeobfJar=$POWER_PLAYER" \ + "-PpowerUnitSource=$GITHUB_WORKSPACE/.ci-dependencies/basePower" \ "-PbaseMetalsDeobfJar=$METALS_PLAYER" "-PoreSpawnDeobfJar=$ORESPAWN_PLAYER" - name: Analyze uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 diff --git a/.github/workflows/validate-gradle-build.yml b/.github/workflows/validate-gradle-build.yml index b37dfc9..531f53f 100644 --- a/.github/workflows/validate-gradle-build.yml +++ b/.github/workflows/validate-gradle-build.yml @@ -3,11 +3,11 @@ name: Validate Gradle Wrapper on: push: branches: - - master-1.21.11 + - master-26.1.2 - 'feature/**' pull_request: branches: - - master-1.21.11 + - master-26.1.2 permissions: contents: read diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7a0fea9..68912b2 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,13 @@ +baseElectric 2.2.17.2601021 (Minecraft 26.1.2 / Forge) + +- Bring baseElectric to Minecraft 26.1.2 with Forge 64.1.5 and Java 25. +- Preserve machine state, battery charge, old-world aliases, material ratios and recipe progression. +- Keep all six librarian offers and their original ranges and tiers in native trade data. +- Fix laser-beam texture loading and inherited battery models. +- Preserve saved OreSpawn choices before generation and stop on ambiguous migrations. +- Pin released basePower 2.4.16.2601021, Base Metals 3.1.2.2601021 and OreSpawn 4.1.1.2601021. +- Complete the local machine, upgrade, graphical, endurance and manual-acceptance checks. + baseElectric 2.2.16.121111 (Minecraft 1.21.11 / Forge) - Port to Forge 61.1.0 with basePower 2.4.15.121111 and Java 21. diff --git a/README.md b/README.md index b6689c0..bdfea9a 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,76 @@ # baseElectric +**Version 2.2.17.2601021 is accepted for release.** + baseElectric continues DrCyano's Electric Advantage. It adds electric machines, generation, storage, automation and defence to basePower. Its equipment appears in the shared basePower creative tab. -baseElectric **2.2.16.121111** runs on Minecraft **1.21.11**, Forge **61.1.0**, -official mappings with MCP **20251223.124241**, ForgeGradle **7.0.34**, Gradle -**9.6.1**, and Java **21**. It requires -[basePower **2.4.15.121111**](https://www.curseforge.com/minecraft/mc-mods/basepower/files/9566998). -[Base Metals **3.1.2.121111**](https://www.curseforge.com/minecraft/mc-mods/base-metals/files/9566738) -is also **REQUIRED** on both client and server; -Forge enforces this dependency. The coordinated OreSpawn version is -**4.1.0.121111**. This version has been accepted for release but is not yet published. -See the [1.21.11 notes](docs/NATIVE12111.md) for compatibility and verification status. - -Combined machine, upgrade, graphical and two-world 30-minute endurance checks -passed before the released dependency retarget. The full record and its manual-test limits are in -basePower's `docs/1.21.11/QUALIFICATION.md`. Those limits still apply. -Publication is waiting for the final combined restart/endurance checks and -baseSteam's public release. The released dependency pins are recorded in -`gradle.properties`. +This checkout targets Minecraft **26.1.2** and +Forge **64.1.5**, using official runtime names, MCP **20260409.101008**, +ForgeGradle **7.0.34**, Gradle **9.6.1**, and Temurin **25.0.3+9**. +The frozen profile12 family has completed automated qualification and manual +testing. See the +[family qualification record](../../PowerAdvantage/docs/26.1.2/QUALIFICATION.md) +for the exact tested jars, completed checks and scope limits. Manual acceptance +is complete. Publication is being prepared; this version is not public yet. + +basePower **2.4.16.2601021** and Base Metals **3.1.2.2601021** are required on +both client and server. Their public source and ordinary-jar checkpoints are +recorded in `gradle.properties`. +The build rejects old 1.21.11 inputs. OreSpawn +**4.1.1.2601021** is pinned to its public release (CurseForge 9572926). +See the [26.1.2 notes](docs/NATIVE2612.md). + +The public pins use Power source `a684e71e` / jar `23ABCC1F` (CurseForge 9576332) +and Base Metals source `5e708468` / jar `67862388` (9576007). Their public +downloads match the accepted dependency jars. Power is published first, then +Steam, then Electric. Electric's promoted jar changes only its manifest to +record the merged Power source; every other archive entry is byte-identical +to the accepted candidate. Restart and endurance qualification for those final +jar bytes is recorded separately from the earlier candidate's results. +The new clean release suite passes 117 unit tests and 28 native checks, and its +warm and fresh empty-cache path-with-spaces reproductions match all three +archives. Fresh ECJ passes all 117 tests with the complete compiled inventory, +and the standalone source audit passes all nine checks after its exact OreSpawn +constants were updated. The original stale-pin failures remain preserved. + +The completed [1.21.11 release](https://www.curseforge.com/minecraft/mc-mods/baseelectric/files/9567881) +and its [qualification notes](docs/NATIVE12111.md) are historical evidence, +not proof of this port. New gameplay, migration and graphical checks must use +the actual 26.1.2 jars. Back up your world and try the upgrade on a copy. Replace Electric Advantage and Power Advantage with baseElectric and basePower; do not install both sets. ## Building locally -Use Temurin 21.0.7+6 for Gradle and Minecraft. The pinned OreSpawn Mavenizer -fixture uses Java 25, and ForgeGradle's Slime Launcher tooling uses Java 8. Set -`-Porg.gradle.java.installations.paths=,,` -if Gradle cannot discover all three. +Use Temurin 25.0.3+9 for Gradle, Minecraft, compilation, tests, Mavenizer and +Slime Launcher. No Java 8 helper is required. Set +`-Porg.gradle.java.installations.paths=` if needed. +Use the isolated `D:/GradleCaches/baseelectric-2612-port` cache for local work. The independent Forge/resource checks do not require sibling jars: ```text -./gradlew --offline nativeResourceChecks compileNativeApiDiagnostics verifyRecipeTagGuards processResources +./gradlew nativeResourceChecks compileNativeApiDiagnostics verifyRecipeTagGuards processResources --max-workers=2 --no-daemon node tools/verify-native-port.js node tools/generate-recipe-unlocks.js --check ``` -For a full local build, use the exact dependency jars recorded in -`gradle.properties`. The sibling paths are the defaults and these flags are -optional path overrides: +For a full local build, use the exact local dependency pins in +`gradle.properties`. Path overrides never replace source or hash checks: ```text --PpowerAdvantageRuntimeJar=/path/to/basePower-2.4.15.121111.jar --PbaseMetalsRuntimeJar=/path/to/BaseMetals-3.1.2.121111.jar --PforgeInstallerJar=/path/to/forge-1.21.11-61.1.0-installer.jar +-PpowerAdvantageRuntimeJar=/path/to/basePower-2.4.16.2601021.jar +-PbaseMetalsRuntimeJar=/path/to/BaseMetals-3.1.2.2601021.jar +-PforgeInstallerJar=/path/to/forge-26.1.2-64.1.5-installer.jar ``` -Forge 61 uses the ordinary native jar for both play and development. The former +Forge 64 uses the ordinary native jar for both play and development. The former `*DeobfJar` path flags remain aliases for the same bytes. OreSpawn and the -Mavenizer fixture default to the sibling `MinecraftMineralogy/OS12111/OreSpawn` -checkout. Use `-PoreSpawnRuntimeJar` and `-Pforge61MavenizerJar` to override paths. +Mavenizer fixture default to the sibling `MinecraftMineralogy/OS2612/OreSpawn` +checkout. Use `-PoreSpawnRuntimeJar` and `-Pforge64MavenizerJar` to override paths. For the prepared cache, use `--offline` with `ORESPAWN_MAVENIZER_OFFLINE=true`. ```text @@ -60,14 +78,44 @@ For the prepared cache, use `--offline` with `ORESPAWN_MAVENIZER_OFFLINE=true`. ``` Use `gradlew.bat` on Windows. Output jars go to `build/libs`. Dependencies -remain separate mods. The 1.21.11 CI and CodeQL jobs require merged source pins +remain separate mods. The 26.1.2 CI and CodeQL jobs require merged source pins and checksum-matching public dependencies before checkout or compilation. Local jars do not satisfy that gate. -## Verification - -The current local checks pass 114 unit tests, 25 native resource tests and -114 tests from fresh Eclipse compiler output, with no failures or skips. +## Candidate09 Verification History + +The previous candidate09 passed 117 unit tests, 28 independent native resource +tests and 117 tests from fresh ECJ output, with no failures or skips. The ECJ +gate compares every compiled test method with the Gradle XML inventory. +Electric registers its pure JSON migration defaults during construction, after +the old OS1 importer and before lifecycle setup. Migration refusal or an I/O +failure stops startup; old provider identities and saved choices are not +silently replaced by fresh defaults. +Both independent reproduction builds match all three jars exactly; each passes +117 unit and 28 native checks. One reuses the previously cold-generated engine +cache, without claiming a new cold download. All four named source GameTests +also pass on these pins. Their first-boot warnings remain in the receipts; +this is not warning-free or manual gameplay proof. The current receipts are in +`build/port-evidence/candidate09-lifecycle-pin-01`. Candidate06's four +GameTests, reproductions and narrow packaged smoke remain historical proof, +not qualification of the new player jar. Combined gameplay, restart, migration +and actual client checks remain separate parent-owned gates. +The Power pin includes the parent's retained-profile, provider-schema and +profile-loading lifecycle repairs, without an OreSpawn patch or log waiver. +Candidate07's actual OS1 startup failure remains recorded separately, despite +its passing local build checks. Candidate08 retained-profile runs passed the +original OS1 route but failed both released-source first boots on the old quartz +output; passing restarts do not waive those failures. Further family runs are +held until the current retained-profile gates are resolved. Candidate09's saved +Forge alias is valid, but the old canonical-ID-only audit still records quartz +failures; a separate Base Metals duplicate-placement review is also unresolved. +Each qualification receipt retains its exact input hashes. In particular, +no old screenshot or earlier version's result qualifies this port. + +The atlas and abstract-battery definition repairs have native codec checks and +a real registered-item census. Their sources and packaged archives reject +global directories, foreign namespaces, missing sprites and duplicates across +atlases. The earlier diagnostic asset jar is not a release or final proof. Run `runGameTestServer` with the same pinned mod inputs, an existing accepted EULA passed as `-PelectricGameTestEula=`, and a new directory passed as diff --git a/build.gradle b/build.gradle index 9483094..9282a68 100644 --- a/build.gradle +++ b/build.gradle @@ -61,36 +61,50 @@ def pinnedFile = { String property, String expected, String fallback = null -> input } -apply from: 'gradle/forge61-mavenizer.gradle' +apply from: 'gradle/forge64-mavenizer.gradle' java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(25) vendor = JvmVendorSpec.ADOPTIUM } withSourcesJar() withJavadocJar() } +fgtools.configure('slimelauncher') { + javaLauncher.set(javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(25) + vendor = JvmVendorSpec.ADOPTIUM + }) +} + tasks.withType(JavaCompile).configureEach { javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(25) vendor = JvmVendorSpec.ADOPTIUM } - options.release = 21 + options.release = 25 options.encoding = 'UTF-8' options.compilerArgs.addAll(['-Xmaxerrs', '1000']) } +tasks.withType(Test).configureEach { + jvmArgs '-XX:ActiveProcessorCount=4' + environment = environment.findAll { key, value -> + !(key in ['JAVA_TOOL_OPTIONS', 'JDK_JAVA_OPTIONS', '_JAVA_OPTIONS']) + } +} + tasks.named('test', Test) { javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(25) vendor = JvmVendorSpec.ADOPTIUM } afterSuite { descriptor, result -> if (!descriptor.parent && filter.includePatterns.empty && filter.commandLineIncludePatterns.empty - && (result.testCount != 114 || result.skippedTestCount != 0)) { - throw new GradleException("Expected 114 tests with no skips, got ${result.testCount} tests and ${result.skippedTestCount} skips") + && (result.testCount != 117 || result.skippedTestCount != 0)) { + throw new GradleException("Expected 117 tests with no skips, got ${result.testCount} tests and ${result.skippedTestCount} skips") } } doFirst { @@ -154,6 +168,7 @@ minecraft { configureEach { workingDir.convention layout.projectDirectory.dir(configuredRunDirectory) jvmArgs '--add-opens', 'java.base/java.lang.invoke=ALL-UNNAMED' + jvmArgs '-XX:ActiveProcessorCount=4' systemProperty 'forge.logging.markers', 'REGISTRIES' systemProperty 'forge.logging.console.level', 'info' mods { baseelectric { source sourceSets.main } } @@ -166,6 +181,8 @@ minecraft { register('gameTestServer') { workingDir.set(electricGameTestRunDirectory) systemProperty 'forge.enabledGameTestNamespaces', project.mod_id + systemProperty 'baseelectric.gameTestRuntimeReceipt', + electricGameTestRunDirectory.file('runtime-receipt.json').asFile.absolutePath args '--tests', 'baseelectric:*', '--report', 'game-test-results.xml' mods { baseelectric { source sourceSets.gameTestValidation } } environment 'MOD_CLASSES', "${mod_id}%%${gameTestModOutput.get().asFile}" @@ -186,7 +203,7 @@ sourceSets.main.resources { def forgeRunModClassesDirectory = layout.buildDirectory.dir('forge-run-mod-classes/main') def prepareForgeRunModClasses = tasks.register('prepareForgeRunModClasses', Sync) { group = 'ide' - description = 'Merges classes and processed resources for Forge 1.21.11 launches.' + description = 'Merges classes and processed resources for Forge 26.1.2 launches.' dependsOn tasks.named('classes') from sourceSets.main.output into forgeRunModClassesDirectory @@ -228,6 +245,10 @@ def prepareElectricGameTestFixture = tasks.register('prepareElectricGameTestFixt } } tasks.withType(JavaExec).configureEach { + jvmArgs '-XX:ActiveProcessorCount=4' + environment = environment.findAll { key, value -> + !(key in ['JAVA_TOOL_OPTIONS', 'JDK_JAVA_OPTIONS', '_JAVA_OPTIONS']) + } if (name in ['runClient', 'runServer', 'runData']) { dependsOn prepareForgeRunModClasses } @@ -286,7 +307,8 @@ file('gradle.properties').withInputStream { committedPins.load(it) } def requireCommittedPins = { String prefix -> committedPins.stringPropertyNames().findAll { key -> key.startsWith(prefix) && (key.endsWith('_sha256') || key.endsWith('_ref') - || key.endsWith('_commit') || key.endsWith('_pin_state') || key.endsWith('_version')) + || key.endsWith('_commit') || key.endsWith('_pin_state') || key.endsWith('_version') + || key.endsWith('_built_on')) }.each { key -> if (project.property(key) != committedPins.getProperty(key)) { throw new GradleException("Cannot override the recorded release pin ${key}.") @@ -299,12 +321,12 @@ def requirePowerPin = { || !(project.poweradvantage_source_ref ==~ /[a-f0-9]{40}/) || ![project.poweradvantage_deobf_sha256, project.poweradvantage_runtime_sha256] .every { it ==~ /[A-F0-9]{64}/ }) { - throw new GradleException('The owner-approved basePower 1.21.11 source and artifact pins are required.') + throw new GradleException('The owner-approved basePower 26.1.2 source and artifact pins are required.') } } // Resolve and check only the dependencies required by the selected tasks. def powerDefaultJar = "../../PowerAdvantage/build/libs/basePower-${project.poweradvantage_version}.jar" -def metalsDefaultJar = "../../../../BaseMetals/.verification-basemetals12111/Advantage shared hook/build/libs/BaseMetals-${project.basemetals_version}.jar" +def metalsDefaultJar = "../../../../BaseMetals/BaseMetals 2612/BaseMetals/build/libs/BaseMetals-${project.basemetals_version}.jar" def powerAdvantageDeobfJar = providers.provider { requirePowerPin() pinnedFile('powerAdvantageDeobfJar', project.poweradvantage_deobf_sha256, @@ -315,7 +337,7 @@ def baseMetalsDeobfJar = providers.provider { pinnedFile('baseMetalsDeobfJar', project.basemetals_deobf_sha256, providers.gradleProperty('baseMetalsRuntimeJar').getOrElse(metalsDefaultJar)) } -def oreSpawnProject = '../../../../MinecraftMineralogy/OS12111/OreSpawn' +def oreSpawnProject = '../../../../MinecraftMineralogy/OS2612/OreSpawn' def oreSpawnDeobfJar = providers.provider { requireCommittedPins('orespawn_') pinnedFile('oreSpawnDeobfJar', project.orespawn_deobf_sha256, @@ -323,6 +345,52 @@ def oreSpawnDeobfJar = providers.provider { "${oreSpawnProject}/build/libs/OreSpawn-${project.orespawn_version}.jar")) } +def powerUnitSource = providers.gradleProperty('powerUnitSource').getOrElse('../../PowerAdvantage') +def unitBootstrapJava = providers.provider { + requireCommittedPins('unit_') + pinnedFile('unitBootstrapJava', project.unit_bootstrap_java_sha256, + "${powerUnitSource}/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java") +} +def unitRegistriesJava = providers.provider { + requireCommittedPins('unit_') + pinnedFile('unitRegistriesJava', project.unit_registries_java_sha256, + "${powerUnitSource}/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/ForgeUnitRegistries.java") +} +def unitLoaderJava = providers.provider { + requireCommittedPins('unit_') + pinnedFile('unitLoaderJava', project.unit_loader_java_sha256, + "${powerUnitSource}/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/Forge64UnitLoader.java") +} +def pinnedUnitSupport = layout.buildDirectory.dir('generated-unit-support') +def preparePinnedUnitSupport = tasks.register('preparePinnedUnitSupport', Sync) { + from unitRegistriesJava, unitLoaderJava + into pinnedUnitSupport.map { it.dir('zone/moddev/mc/advantageworks/unit') } +} +sourceSets.test.java.srcDir pinnedUnitSupport +tasks.named('compileTestJava') { dependsOn preparePinnedUnitSupport } +def compileUnitBootstrapJava = tasks.register('compileUnitBootstrapJava', JavaCompile) { + source unitBootstrapJava + classpath = files() + destinationDirectory = layout.buildDirectory.dir('unit-bootstrap/classes') +} +def electricUnitBootstrapJar = tasks.register('electricUnitBootstrapJar', Jar) { + dependsOn compileUnitBootstrapJava + archiveFileName = 'forge-native-unit-bootstrap.jar' + destinationDirectory = layout.buildDirectory.dir('unit-bootstrap') + from compileUnitBootstrapJava.flatMap { it.destinationDirectory } + manifest.attributes('Premain-Class': 'zone.moddev.mc.advantageworks.unit.ForgeCapabilityBootstrap') + doLast { + requireCommittedPins('unit_') + if (sha256Of(archiveFile.get().asFile) != project.unit_bootstrap_sha256) { + throw new GradleException('Rebuilt unit agent differs from the frozen helper bytes') + } + } +} +tasks.named('test', Test) { + dependsOn electricUnitBootstrapJar + doFirst { jvmArgs "-javaagent:${electricUnitBootstrapJar.get().archiveFile.get().asFile}" } +} + def nativeForge = minecraft.dependency( "net.minecraftforge:forge:${project.minecraft_version}-${project.forge_version}") def forgeInstallerConfiguration = configurations.create('forgeInstallerArtifact') { @@ -382,21 +450,67 @@ def compileNativeResourceChecks = tasks.register('compileNativeResourceChecks', '**/RecipeRegistrationThreadTest.java', '**/NativeTestBootstrap.java', '**/LegacyBatteryDataTest.java', '**/NativePackRangeContractTest.java', '**/NativePackResourceProof.java', '**/BuildWorkflowContractTest.java', '**/ShaderResourceContractTest.java', '**/DatapackLayoutTest.java', - '**/RecipeDeconstructorTest.java' + '**/RecipeDeconstructorTest.java', '**/VillagesTest.java' } classpath = nativeResourceCheckClasspath.get() + files(layout.buildDirectory.dir('classes/native-api-diagnostics')) destinationDirectory = nativeResourceCheckClasses } + +tasks.register('compileNativeProductionDiagnostics', JavaCompile) { + group = 'verification' + description = 'Compiles all production sources against explicitly hashed provisional native inputs; never produces a release jar.' + source sourceSets.main.java + classpath = nativeResourceCheckClasspath.get() + files(providers.provider { + ['powerAdvantage', 'baseMetals'].collect { name -> + pinnedFile("${name}DiagnosticJar", providers.gradleProperty("${name}DiagnosticSha256").getOrElse('')) + } + }, oreSpawnDeobfJar) + destinationDirectory = layout.buildDirectory.dir('classes/native-production-diagnostics') +} + +def diagnosticMain = tasks.named('compileNativeProductionDiagnostics', JavaCompile) +def diagnosticTests = tasks.register('compileNativeTestDiagnostics', JavaCompile) { + group = 'verification' + description = 'Compiles retained unit tests against provisional inputs, isolated from release outputs.' + dependsOn diagnosticMain, preparePinnedUnitSupport + source sourceSets.test.java + classpath = files(diagnosticMain.flatMap { it.destinationDirectory }) + diagnosticMain.get().classpath + destinationDirectory = layout.buildDirectory.dir('classes/native-test-diagnostics') +} + +tasks.register('nativeUnitDiagnostics', Test) { + group = 'verification' + description = 'Runs worldless unit diagnostics with provisional native inputs; not a release qualification.' + dependsOn diagnosticTests, tasks.named('processResources'), tasks.named('processTestResources'), electricUnitBootstrapJar + useJUnit() + testClassesDirs = files(diagnosticTests.flatMap { it.destinationDirectory }) + classpath = testClassesDirs + files(diagnosticMain.flatMap { it.destinationDirectory }, + layout.buildDirectory.dir('resources/main'), layout.buildDirectory.dir('resources/test')) + diagnosticMain.get().classpath + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(25) + vendor = JvmVendorSpec.ADOPTIUM + } + afterSuite { descriptor, result -> + if (!descriptor.parent && filter.includePatterns.empty && filter.commandLineIncludePatterns.empty + && (result.testCount != 117 || result.skippedTestCount != 0)) { + throw new GradleException("Expected 117 provisional unit diagnostics without skips, got ${result.testCount}/${result.skippedTestCount}") + } + } + doFirst { + systemProperty 'baseelectric.nativePackClasspath', classpath.asPath + jvmArgs "-javaagent:${electricUnitBootstrapJar.get().archiveFile.get().asFile}" + } +} tasks.register('nativeResourceChecks', Test) { group = 'verification' - description = 'Checks Electric resources against native Forge 61 without sibling mod jars.' + description = 'Checks Electric resources against native Forge 64 without sibling mod jars.' dependsOn compileNativeResourceChecks, tasks.named('processResources') useJUnit() testClassesDirs = files(nativeResourceCheckClasses) classpath = testClassesDirs + nativeResourceCheckClasspath.get() + files(layout.buildDirectory.dir('classes/native-api-diagnostics')) afterSuite { descriptor, result -> - if (!descriptor.parent && (result.testCount != 25 || result.skippedTestCount != 0)) { - throw new GradleException("Expected 25 native resource tests without skips, got ${result.testCount}/${result.skippedTestCount}") + if (!descriptor.parent && (result.testCount != 28 || result.skippedTestCount != 0)) { + throw new GradleException("Expected 28 native resource tests without skips, got ${result.testCount}/${result.skippedTestCount}") } } doFirst { @@ -476,13 +590,13 @@ def releaseJar = tasks.named('jar', Jar) tasks.register('releaseJar') { group = 'build' - description = 'Builds the native Forge 61 player jar.' + description = 'Builds the native Forge 64 player jar.' dependsOn releaseJar } tasks.register('deobfJar') { group = 'build' - description = 'Builds the same native Forge 61 jar used for development and play.' + description = 'Builds the same native Forge 64 jar used for development and play.' dependsOn tasks.named('jar') } @@ -557,29 +671,29 @@ tasks.register('verifyReleaseConfiguration') { requireCommittedPins('orespawn_') requireCommittedPins('forge_') def pack = new JsonSlurper().parse(file('src/main/resources/pack.mcmeta')).pack - if (pack.min_format != [94, 1] || pack.max_format != [94, 1] + if (pack.min_format != [101, 1] || pack.max_format != [101, 1] || pack.containsKey('pack_format') || pack.containsKey('supported_formats') || !(file('src/main/resources/META-INF/mods.toml').text =~ /(?m)^showAsResourcePack=false$/)) { - throw new GradleException('Root metadata must declare data 94.1 with the native hidden Forge client pack') + throw new GradleException('Root metadata must declare data 101.1 with the native hidden Forge client pack') } - if (project.mod_version != '2.2.16.121111') { + if (project.mod_version != '2.2.17.2601021') { throw new GradleException("Unexpected baseElectric release version: ${project.mod_version}") } - if (project.minecraft_version != '1.21.11' - || project.minecraft_version_range != '[1.21.11]' - || project.forge_version != '61.1.0' - || project.forge_version_range != '[61.1.0,62)' - || project.loader_version_range != '[61,)' + if (project.minecraft_version != '26.1.2' + || project.minecraft_version_range != '[26.1.2]' + || project.forge_version != '64.1.5' + || project.forge_version_range != '[64.1.5,65)' + || project.loader_version_range != '[64,65)' || project.mapping_channel != 'official' - || project.mapping_version != '1.21.11' - || project.mcp_version != '20251223.124241') { + || project.mapping_version != '26.1.2' + || project.mcp_version != '20260409.101008') { throw new GradleException('Unexpected Minecraft, Forge, or mappings target') } if (project.loader_name != 'forge' || project.loader_code != '1' - || project.java_version != '21' - || project.java_toolchain_version != '21.0.7+6' - || project.java_setup_version != '21.0.7+6.0.LTS' - || project.gradle_java_version != '21') { + || project.java_version != '25' + || project.java_toolchain_version != '25.0.3+9' + || project.java_setup_version != '25.0.3+9.0.LTS' + || project.gradle_java_version != '25') { throw new GradleException('Unexpected release dispatcher target metadata') } if (project.mod_id != 'baseelectric' @@ -592,15 +706,17 @@ tasks.register('verifyReleaseConfiguration') { } if (project.cf_requirements != 'basepower,base-metals' || project.cf_optionals != '' - || project.cf_versions != '1.21.11') { + || project.cf_versions != '26.1.2') { throw new GradleException('Unexpected CurseForge dependency or game-version metadata') } - if (project.poweradvantage_version != '2.4.15.121111' + if (project.poweradvantage_version != '2.4.16.2601021' || project.poweradvantage_source_repository != 'MinecraftModDevelopmentMods/basePower') { throw new GradleException('Unexpected basePower compile dependency identity') } - if (!(project.basemetals_version ==~ /3\.\d+\.\d+\.121111/) - || project.orespawn_version != '4.1.0.121111' + if (!(project.basemetals_version ==~ /3\.\d+\.\d+\.2601021/) + || project.orespawn_version != '4.1.1.2601021' + || project.basemetals_built_on != '26.1.2-64.0.9' + || project.orespawn_built_on != '26.1.2-64.0.9' || !(project.basemetals_source_commit ==~ /[a-f0-9]{40}/) || !(project.basemetals_deobf_sha256 ==~ /[A-F0-9]{64}/) || !(project.basemetals_runtime_sha256 ==~ /[A-F0-9]{64}/) @@ -608,18 +724,18 @@ tasks.register('verifyReleaseConfiguration') { || project.orespawn_deobf_sha256 != project.orespawn_runtime_sha256 || project.basemetals_deobf_sha256 != project.basemetals_runtime_sha256 || project.poweradvantage_deobf_sha256 != project.poweradvantage_runtime_sha256 - || project.forge_installer_sha256 != '8BD1724EAEAF26F51DACFB7FDC46D79E9B640705B70D5F0019ED38907145EB48') { + || project.forge_installer_sha256 != '6E01323837051176762BD466340A601DA122F179FE6A00AC616E05429AE07648') { throw new GradleException('Unexpected native dependency pins') } String mainSource = file( 'src/main/java/zone/moddev/mc/baseelectric/ElectricAdvantage.java').getText('UTF-8') if (!mainSource.contains('MODID = "baseelectric"') - || !mainSource.contains('VERSION = "2.2.16.121111"')) { + || !mainSource.contains('VERSION = "2.2.17.2601021"')) { throw new GradleException('baseElectric source identity changed') } String metadata = file('src/main/resources/META-INF/mods.toml').getText('UTF-8').replace('\r\n', '\n') ['modId="baseelectric"', 'displayName="baseElectric"', 'modId="basepower"', 'modId="basemetals"', - 'versionRange="[2.4.15.121111,3.0.0)"', 'versionRange="[3.1.2.121111,4.0.0)"'].each { marker -> + 'versionRange="[2.4.16.2601021,3.0.0)"', 'versionRange="[3.1.2.2601021,4.0.0)"'].each { marker -> if (!metadata.contains(marker)) { throw new GradleException("Source mods.toml is missing ${marker}") } @@ -637,9 +753,11 @@ def auditModInput = { File jarFile, String modId, String version, List r } def manifest = new Manifest(zip.getInputStream(zip.getEntry('META-INF/MANIFEST.MF'))).mainAttributes String metadata = zip.getInputStream(zip.getEntry('META-INF/mods.toml')).getText('UTF-8') + String builtOn = [orespawn: project.orespawn_built_on, basemetals: project.basemetals_built_on] + .getOrDefault(modId, "${project.minecraft_version}-${project.forge_version}".toString()) if (manifest.getValue('Implementation-Version') != version - || manifest.getValue('Built-On') != "${project.minecraft_version}-${project.forge_version}" - || manifest.getValue('Built-On-Java') != '21' + || manifest.getValue('Built-On') != builtOn + || manifest.getValue('Built-On-Java') != '25' || !metadata.contains("modId=\"${modId}\"") || !metadata.contains("version=\"${version}\"")) { throw new GradleException("${jarFile.name} has unexpected ${modId} version or target metadata") @@ -648,10 +766,24 @@ def auditModInput = { File jarFile, String modId, String version, List r } } +def verifyOreSpawnNativeInputs = tasks.register('verifyOreSpawnNativeInputs') { + group = 'verification' + description = 'Verifies the unchanged public OreSpawn jar and its own exact native build provenance.' + doLast { + requireCommittedPins('orespawn_') + auditModInput(oreSpawnDeobfJar.get(), 'orespawn', project.orespawn_version, []) + auditModInput(pinnedFile('oreSpawnRuntimeJar', project.orespawn_runtime_sha256, + providers.gradleProperty('oreSpawnDeobfJar').getOrElse( + "${oreSpawnProject}/build/libs/OreSpawn-${project.orespawn_version}.jar")), + 'orespawn', project.orespawn_version, []) + } +} + tasks.register('verifyLocalNativeInputs') { group = 'verification' - description = 'Verifies explicit Base Metals and OreSpawn 1.21.11 development and player JARs.' + description = 'Verifies explicit Base Metals and OreSpawn 26.1.2 development and player JARs.' dependsOn tasks.named('verifyReleaseConfiguration') + dependsOn verifyOreSpawnNativeInputs doLast { auditModInput(baseMetalsDeobfJar.get(), 'basemetals', project.basemetals_version, ['zone/moddev/mc/basemetals/api/migration/Legacy112MigrationExtension.class', @@ -659,11 +791,6 @@ tasks.register('verifyLocalNativeInputs') { auditModInput(pinnedFile('baseMetalsRuntimeJar', project.basemetals_runtime_sha256, providers.gradleProperty('baseMetalsDeobfJar').getOrElse(metalsDefaultJar)), 'basemetals', project.basemetals_version, []) - auditModInput(oreSpawnDeobfJar.get(), 'orespawn', project.orespawn_version, []) - auditModInput(pinnedFile('oreSpawnRuntimeJar', project.orespawn_runtime_sha256, - providers.gradleProperty('oreSpawnDeobfJar').getOrElse( - "${oreSpawnProject}/build/libs/OreSpawn-${project.orespawn_version}.jar")), - 'orespawn', project.orespawn_version, []) } } @@ -710,8 +837,30 @@ tasks.register('verifyReleaseDependencies') { if (!runtimeFiles.containsAll([powerJar.canonicalFile, metalsJar.canonicalFile, oreJar.canonicalFile])) { throw new GradleException('A verified development mod is missing from runtimeClasspath') } - if (!compileFiles.any { it.name == "mappings_official-${project.minecraft_version}-${project.mcp_version}.zip" }) { - throw new GradleException('The resolved Forge official mappings do not match the MCP pin') + def nativeMappings = new JsonSlurper().parse(file('.gradle/mavenizer/dependencies/default.json')) + String mcpCoordinate = "${project.minecraft_version}-${project.mcp_version}" + if (nativeMappings['mc.version'] != project.minecraft_version + || nativeMappings['mappings.channel'] != 'official' + || nativeMappings['mappings.version'] != project.mapping_version + || nativeMappings['mcp.version'] != mcpCoordinate + || nativeMappings['mcp.artifact'] != "de.oceanlabs.mcp:mcp_config:${mcpCoordinate}@zip" + || nativeMappings['mappings.csv.artifact'] != "net.minecraft:mappings_official:${mcpCoordinate}@zip" + || !compileFiles.any { it.name == "forge-${project.minecraft_version}-${project.forge_version}.jar" } + || compileFiles.any { it.name.startsWith('mappings_') } + || project.reobf != 'false') { + throw new GradleException('The native official-name engine or MCP metadata does not match the target pin') + } + File emptyMappings = file(nativeMappings['mappings.csv.file']) + new ZipFile(emptyMappings).withCloseable { zip -> + if (zip.entries().collect { it.name }.sort() != ['fields.csv', 'methods.csv']) { + throw new GradleException('Unexpected official-name mapping entries') + } + ['fields.csv', 'methods.csv'].each { name -> + String csv = zip.getInputStream(zip.getEntry(name)).getText('UTF-8').replace('\r\n', '\n') + if (csv != 'searge,name,side,desc') { + throw new GradleException('Forge 64 official runtime mappings must be empty header-only CSVs') + } + } } List forbiddenRuntimeMods = runtimeFiles.collect { it.name.toLowerCase(Locale.ROOT) } .findAll { name -> @@ -725,17 +874,42 @@ tasks.register('verifyReleaseDependencies') { } def blocksAtlasPath = 'assets/minecraft/atlases/blocks.json' -def electricBlocksAtlas = [sources: [[type: 'single', resource: 'baseelectric:entity/laser_turret']]] +def itemsAtlasPath = 'assets/minecraft/atlases/items.json' +def validateOwnedAtlas = { Object atlas, String owner, Set sprites, Closure exists, String context -> + if (!(atlas instanceof Map) || atlas.keySet() != (['sources'] as Set) + || !(atlas.sources instanceof List) || atlas.sources.isEmpty()) { + throw new GradleException("${context} must declare nonempty explicit sprite sources") + } + atlas.sources.each { source -> + if (!(source instanceof Map) || source.keySet() != (['type', 'resource'] as Set) + || source.type != 'minecraft:single' || !(source.resource instanceof String) + || !source.resource.startsWith(owner + ':')) { + throw new GradleException("${context} must only register ${owner}'s exact native single sprites") + } + String sprite = source.resource + String path = sprite.substring(sprite.indexOf(':') + 1) + if (!(path ==~ '[a-z0-9_./-]+') || path.tokenize('/').any { it == '..' } + || !exists("assets/${owner}/textures/${path}.png".toString())) { + throw new GradleException("${context} registers a missing or unsafe sprite ${sprite}") + } + if (!sprites.add(sprite)) throw new GradleException("Duplicate sprite across native atlases: ${sprite}") + } +} def verifyPackagedBlocksAtlas = { ZipFile zip -> def overrides = zip.entries().findAll { !it.isDirectory() && it.name.startsWith('assets/minecraft/') }.collect { it.name } - if (overrides != [blocksAtlasPath]) { - throw new GradleException("Expected only ${blocksAtlasPath} in the Minecraft asset namespace, found ${overrides}") + if (overrides.sort() != [blocksAtlasPath, itemsAtlasPath].sort()) { + throw new GradleException("Expected exactly the two native atlases in the Minecraft asset namespace, found ${overrides}") } - zip.getInputStream(zip.getEntry(blocksAtlasPath)).withCloseable { input -> - if (new JsonSlurper().parse(input, 'UTF-8') != electricBlocksAtlas) { - throw new GradleException('Packaged blocks atlas must add only the turret item sprite') + Set sprites = [] as Set + [blocksAtlasPath, itemsAtlasPath].each { path -> + zip.getInputStream(zip.getEntry(path)).withCloseable { input -> + def atlas = new JsonSlurper().parse(input, 'UTF-8') + validateOwnedAtlas(atlas, project.mod_id, sprites, { resource -> zip.getEntry(resource) != null }, path) + if (atlas != new JsonSlurper().parse(file('src/main/resources/' + path))) { + throw new GradleException("Packaged ${path} differs from the validated source atlas") + } } } } @@ -787,9 +961,20 @@ tasks.register('verifyReleaseArtifacts') { 'zone/moddev/mc/baseelectric/machines/HydroelectricGeneratorTileEntity.class', 'zone/moddev/mc/baseelectric/migration/ElectricLegacyMigrations.class', 'zone/moddev/mc/baseelectric/init/OreSpawnWorldGen.class', - 'zone/moddev/mc/baseelectric/init/Villages.class', + 'data/baseelectric/villager_trade/librarian/electric_conduit.json', + 'data/baseelectric/villager_trade/librarian/solder.json', + 'data/baseelectric/villager_trade/librarian/integrated_circuit.json', + 'data/baseelectric/villager_trade/librarian/blank_circuit_board.json', + 'data/baseelectric/villager_trade/librarian/petrolplastic_ingot.json', + 'data/baseelectric/villager_trade/librarian/psu.json', + 'data/minecraft/tags/villager_trade/librarian/level_1.json', + 'data/minecraft/tags/villager_trade/librarian/level_2.json', 'zone/moddev/mc/baseelectric/recipe/RecipeConditions$ModeCondition.class', 'assets/baseelectric/lang/en_us.json', + 'assets/minecraft/atlases/blocks.json', 'assets/minecraft/atlases/items.json', + 'assets/baseelectric/models/item/battery_base.json', + 'assets/baseelectric/items/alkaline_battery.json', 'assets/baseelectric/items/lead_acid_battery.json', + 'assets/baseelectric/items/lithium_battery.json', 'assets/baseelectric/items/nickel_hydride_battery.json', 'data/baseelectric/loot_table/blocks/electric_drill.json', 'data/baseelectric/loot_table/blocks/electric_conduit.json', 'data/baseelectric/damage_type/laser.json', @@ -802,6 +987,8 @@ tasks.register('verifyReleaseArtifacts') { } } ['agent-notes/', '.codex/', 'src/test/', 'zone/moddev/mc/baseelectric/gametest/', + 'assets/baseelectric/items/battery_base.json', + 'zone/moddev/mc/advantageworks/unit/', 'data/baseelectric/structure/empty.nbt', 'fernflower_abstract_parameter_names.txt', 'cyano/poweradvantage/', 'zone/moddev/mc/basepower/', 'zone/moddev/mc/poweradvantage/', 'zone/moddev/mc/electricadvantage/', @@ -839,8 +1026,8 @@ tasks.register('verifyReleaseArtifacts') { } } int major = ((header[6] & 0xff) << 8) | (header[7] & 0xff) - if (major != 65) { - throw new GradleException("${entry.name} uses Java class major ${major}, expected 65") + if (major != 69) { + throw new GradleException("${entry.name} uses Java class major ${major}, expected 69") } } } @@ -856,7 +1043,7 @@ tasks.register('verifyReleaseArtifacts') { throw new GradleException('Javadoc jar is missing index.html') } ['copy.svg', 'link.svg'].each { name -> - def entry = javadocZip.getEntry(name) + def entry = javadocZip.getEntry("resource-files/${name}") if (entry == null || javadocZip.getInputStream(entry).getText('UTF-8').contains('\r')) { throw new GradleException("Javadoc resource ${name} must use canonical LF line endings") } @@ -994,7 +1181,7 @@ tasks.register('validateMavenReleaseCredentials') { group = 'publishing' description = 'Rejects remote publication from this local port.' doLast { - throw new GradleException('Remote Maven publication is disabled on the local 1.21.11 port.') + throw new GradleException('Remote Maven publication is disabled on the local 26.1.2 port.') } } @@ -1042,7 +1229,7 @@ eclipse { synchronizationTasks 'isolateEclipseProductionRuns' } -tasks.named('eclipseClasspath') { dependsOn tasks.named('processResources') } +tasks.named('eclipseClasspath') { dependsOn tasks.named('processResources'), preparePinnedUnitSupport } tasks.register('configureEclipseBuildship') { group = 'ide' @@ -1053,6 +1240,8 @@ tasks.register('configureEclipseBuildship') { [ 'eclipse.preferences.version' : '1', 'connection.gradle.distribution': 'GRADLE_DISTRIBUTION(WRAPPER)', + 'connection.java.home' : javaToolchains.compilerFor(java.toolchain).get() + .metadata.installationPath.asFile.canonicalPath, 'connection.gradle.user.home' : gradle.gradleUserHomeDir.canonicalPath, 'connection.project.dir' : '', 'gradle.user.home' : gradle.gradleUserHomeDir.canonicalPath, @@ -1067,10 +1256,12 @@ tasks.register('configureEclipseBuildship') { } def quoteEclipseLaunchPaths = { String name, String arguments -> + if (arguments.contains('--to-srg') || arguments.contains('--to-obf')) { + throw new GradleException("${name} must not remap the official Forge 64 runtime") + } // ForgeGradle flattens path arguments without quoting them for Eclipse. Map nextOptions = [ - '--cache': '--metadata', '--metadata': '--to-srg', - '--to-srg': '--to-obf', '--to-obf': '--' + '--cache': '--metadata', '--metadata': '--' ] if (name == 'runData.launch') { nextOptions.put('--output', '--existing') @@ -1095,6 +1286,58 @@ def quoteEclipseLaunchPaths = { String name, String arguments -> arguments } +def nativeEclipseAssets = { + File nativeJar = nativeResourceCheckClasspath.get().files.find { + it.name == "forge-${project.minecraft_version}-${project.forge_version}.jar" + } + if (nativeJar == null) throw new GradleException('Missing native client metadata input') + File metadata = new File(nativeJar.parentFile, + "forge-${project.minecraft_version}-${project.forge_version}-metadata.zip") + def version + new ZipFile(metadata).withCloseable { zip -> + def entry = zip.getEntry('minecraft/version.json') + if (entry == null) throw new GradleException('Native metadata has no Minecraft version descriptor') + version = new JsonSlurper().parse(zip.getInputStream(entry)) + } + if (version.id != project.minecraft_version || !(version.assetIndex.sha1 ==~ /[a-f0-9]{40}/)) { + throw new GradleException('Unexpected native Minecraft asset metadata') + } + File assets = file(providers.gradleProperty('minecraftAssetsDir').getOrElse( + new File(System.getenv('APPDATA') ?: System.getProperty('user.home'), '.minecraft/assets').path)) + [id: version.assetIndex.id.toString(), sha1: version.assetIndex.sha1.toString(), root: assets.canonicalPath] +} + +def resolveEclipseClientAssets = { String arguments -> + def assets = nativeEclipseAssets() + arguments.replace('{asset_index}', assets.id).replace('{assets_root}', '"' + assets.root + '"') +} + +tasks.register('verifyEclipseLaunchArguments') { + group = 'verification' + description = 'Checks official-name launch paths and actual native asset metadata without starting Minecraft.' + doLast { + String sample = '--main native --cache D:/cache with spaces --metadata D:/metadata with spaces -- --launchTarget forge_userdev_client' + String expected = '--main native --cache "D:/cache with spaces" --metadata "D:/metadata with spaces" -- --launchTarget forge_userdev_client' + if (quoteEclipseLaunchPaths('runClient.launch', sample) != expected + || quoteEclipseLaunchPaths('runClient.launch', expected) != expected) { + throw new GradleException('Native Eclipse paths must be quoted once') + } + ['--to-srg', '--to-obf'].each { flag -> + boolean rejected = false + try { quoteEclipseLaunchPaths('runClient.launch', sample + " ${flag} obsolete") } + catch (GradleException expectedFailure) { rejected = true } + if (!rejected) throw new GradleException("Accepted obsolete remapping flag ${flag}") + } + def assets = nativeEclipseAssets() + String resolved = resolveEclipseClientAssets('--assetsDir {assets_root} --assetIndex {asset_index}') + if (resolved != "--assetsDir \"${assets.root}\" --assetIndex ${assets.id}" + || resolved.contains('{asset_index}') || resolved.contains('{assets_root}')) { + throw new GradleException('Native client assets must resolve to literal arguments') + } + logger.lifecycle("Verified native Eclipse assets ${assets.id} SHA-1 ${assets.sha1}; metadata generation downloaded no assets") + } +} + tasks.register('isolateEclipseProductionRuns') { group = 'ide' description = 'Normalizes ForgeGradle Eclipse launches for production-only execution.' @@ -1166,6 +1409,9 @@ tasks.register('isolateEclipseProductionRuns') { throw new GradleException("${launch.name} is missing its program arguments") } programArguments.@value = quoteEclipseLaunchPaths(launch.name, programArguments.@value) + if (launch.name == 'runClient.launch') { + programArguments.@value = resolveEclipseClientAssets(programArguments.@value) + } } launch.setText(groovy.xml.XmlUtil.serialize(launchXml).replace('\r\n', '\n'), 'UTF-8') } @@ -1260,6 +1506,10 @@ tasks.register('verifyEclipseProductionClasspath') { != gradle.gradleUserHomeDir.canonicalFile) { throw new GradleException('Eclipse Buildship does not use this wrapper and Gradle home') } + File javaHome = javaToolchains.compilerFor(java.toolchain).get().metadata.installationPath.asFile + if (file(preferences.getProperty('connection.java.home', '')).canonicalFile != javaHome.canonicalFile) { + throw new GradleException('Eclipse Buildship must use the pinned Java 25 installation') + } def eclipseProject = new groovy.xml.XmlSlurper().parse(file('.project')) if (project.name != 'ElectricAdvantage' || eclipseProject.name.text() != project.name) { throw new GradleException('The Eclipse and Gradle project names must both be ElectricAdvantage') @@ -1302,8 +1552,8 @@ tasks.register('verifyEclipseProductionClasspath') { def productionJre = eclipseClasspath.classpathentry.findAll { it.@kind.text() == 'con' && it.@path.text().startsWith('org.eclipse.jdt.launching.JRE_CONTAINER/') } - if (productionJre.size() != 1 || productionJre[0].@path.text().tokenize('/').last() != 'JavaSE-21') { - throw new GradleException('Eclipse production JRE must be JavaSE-21') + if (productionJre.size() != 1 || productionJre[0].@path.text().tokenize('/').last() != 'JavaSE-25') { + throw new GradleException('Eclipse production JRE must be JavaSE-25') } def processedResources = eclipseClasspath.classpathentry.findAll { it.@kind.text() == 'src' && it.@path.text() == 'build/resources/main' @@ -1344,7 +1594,8 @@ tasks.register('verifyEclipseProductionClasspath') { if (launch.booleanAttribute.find { it.@key.text() == 'org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE' }.@value.text() != 'true' - || contents.contains('src/test') || contents.contains('bin/test')) { + || contents.contains('src/test') || contents.contains('bin/test') + || contents.contains('generated-unit-support') || contents.contains('forge-native-unit-bootstrap')) { throw new GradleException("${name} does not isolate production code") } def runtimeEntries = launch.listAttribute.find { @@ -1371,13 +1622,13 @@ tasks.register('verifyEclipseProductionClasspath') { } List bootstrapArtifacts = [ "forge-${project.minecraft_version}-${project.forge_version}", - "client-extra-${project.minecraft_version}", - "mappings_${project.mapping_channel}-${project.minecraft_version}" + "client-extra-${project.minecraft_version}" ] List bootstrapPositions = bootstrapArtifacts.collect { artifact -> runtimeArchives.findIndexOf { it.name.contains(artifact) } } - if (bootstrapPositions.any { it < 0 } + if (runtimeArchives.any { it.name.startsWith('mappings_') } + || bootstrapPositions.any { it < 0 } || bootstrapPositions != bootstrapPositions.sort(false)) { throw new GradleException( "${name} does not preserve the Forge bootstrap classpath order") @@ -1392,14 +1643,19 @@ tasks.register('verifyEclipseProductionClasspath') { it.@key.text() == 'org.eclipse.jdt.launching.MAIN_TYPE' }.@value.text() String target = name == 'runClient.launch' ? 'forge_userdev_client' : 'forge_userdev_server' + if (programArguments.contains('--to-srg') || programArguments.contains('--to-obf') + || programArguments.contains('{asset_index}') || programArguments.contains('{assets_root}') + || !contents.contains('StandardVMType/JavaSE-25')) { + throw new GradleException("${name} has obsolete mappings, unresolved assets or the wrong Java runtime") + } if (contents.contains('${MC_VERSION}') || mainType != 'net.minecraftforge.launcher.Main' || !programArguments.startsWith('--main net.minecraftforge.bootstrap.ForgeBootstrap ') - || !programArguments.contains(" -- --launchTarget ${target} ") + || !(programArguments =~ /(?:^|\s)--launchTarget\s+${target}(?:\s|$)/).find() || !vmArguments.contains('--add-opens java.base/java.lang.invoke=ALL-UNNAMED')) { - throw new GradleException("${name} does not define the Forge 61 bootstrap and native launch target") + throw new GradleException("${name} does not define the Forge 64 bootstrap and native launch target") } - ['--cache', '--metadata', '--to-srg', '--to-obf'].each { option -> + ['--cache', '--metadata'].each { option -> def argument = programArguments =~ /(?:^|\s)${option}\s+(?:"([^"]+)"|([^"\s]+))(?=\s+--|$)/ if (!argument.find()) { throw new GradleException("${name} must keep ${option} as one Slime Launcher argument") @@ -1425,6 +1681,7 @@ tasks.register('verifyModelAssets') { inputs.dir(assetRoot) inputs.dir(new File(resourceRoot, 'data')) inputs.files(atlasFile) + inputs.file(new File(resourceRoot, 'assets/minecraft/atlases/items.json')) inputs.file(powerAdvantageDeobfJar) doLast { @@ -1435,24 +1692,48 @@ tasks.register('verifyModelAssets') { new JsonSlurper().parse(jsonFile) } - def localAtlas = atlasFile.isFile() ? new JsonSlurper().parse(atlasFile) : [sources: []] - // Vanilla's generic directories also cover mod namespaces. Power adds the legacy directories. - List atlasSources = [[type: 'directory', source: 'block', prefix: 'block/'], - [type: 'directory', source: 'item', prefix: 'item/']] + Set registeredSprites = [] as Set + [blocksAtlasPath, itemsAtlasPath].each { path -> + validateOwnedAtlas(new JsonSlurper().parse(new File(resourceRoot, path)), project.mod_id, + registeredSprites, { resource -> new File(resourceRoot, resource).isFile() }, path) + } + if (registeredSprites.contains('baseelectric:materials/laser_beam') + || !registeredSprites.contains('baseelectric:entity/laser_turret') + || !registeredSprites.contains('baseelectric:materials/machine_case')) { + throw new GradleException('Electric atlas is missing a required sprite or includes the direct-render beam') + } Set powerAdvantageAssets = new ZipFile(powerAdvantageDeobfJar.get()).withCloseable { zip -> - def entry = zip.getEntry(blocksAtlasPath) - if (entry == null) throw new GradleException('Pinned basePower is missing its blocks atlas') - zip.getInputStream(entry).withCloseable { input -> - atlasSources.addAll(new JsonSlurper().parse(input, 'UTF-8').sources) + [blocksAtlasPath, itemsAtlasPath].each { path -> + def entry = zip.getEntry(path) + if (entry == null) throw new GradleException("Pinned basePower is missing ${path}") + zip.getInputStream(entry).withCloseable { input -> + validateOwnedAtlas(new JsonSlurper().parse(input, 'UTF-8'), 'basepower', registeredSprites, + { resource -> zip.getEntry(resource) != null }, 'Pinned Power ' + path) + } } zip.entries().collect { it.name } as Set } - atlasSources.addAll(localAtlas.sources) + Set vanillaAssets = [] as Set + sourceSets.main.compileClasspath.files.findAll { it.isFile() && it.name.endsWith('.jar') }.each { archive -> + new ZipFile(archive).withCloseable { zip -> + zip.entries().each { entry -> + if (entry.name.startsWith('assets/minecraft/')) vanillaAssets.add(entry.name) + } + } + } def verifyReference = { String reference, String assetType, String sourcePath -> if (reference.startsWith('#')) return int separator = reference.indexOf(':') String namespace = separator >= 0 ? reference.substring(0, separator) : 'minecraft' String path = separator >= 0 ? reference.substring(separator + 1) : reference + if (namespace == 'minecraft') { + if (assetType == 'models' && path.startsWith('builtin/')) return + String suffix = assetType == 'textures' ? '.png' : '.json' + if (!vanillaAssets.contains("assets/minecraft/${assetType}/${path}${suffix}".toString())) { + throw new GradleException("${sourcePath} references missing vanilla ${assetType} asset ${reference}") + } + return + } if (namespace != project.mod_id && namespace != 'basepower') return String suffix = assetType == 'textures' ? '.png' : '.json' @@ -1464,13 +1745,8 @@ tasks.register('verifyModelAssets') { throw new GradleException( "${sourcePath} references missing ${assetType} asset ${reference}") } - if (assetType == 'textures' && !atlasSources.any { source -> - if (source.type == 'single') return (source.sprite ?: source.resource) == reference - if (source.type != 'directory') return false - String folder = source.source + '/' - path.startsWith(folder) && (source.prefix + path.substring(folder.length())) == path - }) { - throw new GradleException("${sourcePath} references unstitched blocks-atlas sprite ${reference}") + if (assetType == 'textures' && !registeredSprites.contains(reference)) { + throw new GradleException("${sourcePath} references an unregistered native sprite ${reference}") } } @@ -1494,10 +1770,6 @@ tasks.register('verifyModelAssets') { } } } - if (localAtlas != electricBlocksAtlas) { - throw new GradleException('Electric blocks atlas must add only the turret item sprite') - } - Closure verifyBlockstateModels verifyBlockstateModels = { Object node, String sourcePath -> if (node instanceof Map) { @@ -1617,6 +1889,7 @@ tasks.register('verifyRecipeTagGuards') { } tasks.named('check') { + dependsOn tasks.named('verifyEclipseLaunchArguments') dependsOn tasks.named('compileGameTestValidationJava'), tasks.named('compileClientValidationJava') dependsOn tasks.named('verifyJavaSourceLayout') dependsOn tasks.named('verifyReleaseConfiguration') diff --git a/docs/NATIVE2612.md b/docs/NATIVE2612.md new file mode 100644 index 0000000..a18c9b3 --- /dev/null +++ b/docs/NATIVE2612.md @@ -0,0 +1,330 @@ +# Minecraft 26.1.2 + +Version 2.2.17.2601021 is accepted for release after automated qualification and +manual testing. See the +[family qualification record](../../../PowerAdvantage/docs/26.1.2/QUALIFICATION.md) +for the exact tested jars, completed checks and scope limits. The usual fork, +MMD merge and CurseForge/GitHub/MMD Maven release workflow is authorized. +Publication has not happened. Source pushes, merges, tags and publisher runs +remain held until the verified public Power and Base Metals dependency handoff. +Power is released first, then Steam, then Electric. + +The existing source branch remains feature/baseelectric-26.1.2-port. Public file +IDs and merged-source pins are not filled from local candidates. Electric's +manifest records Power provenance, so the public-pin rebuild needs its own hash +and entry comparison. Earlier checkpoint notes, old holds and qualification +receipts below remain historical and bound to their original inputs, not proof +of a later public-pin jar. + +This is a local port, not a release. The starting source is the released +1.21.11 merge `951fc3921f27e963e96f29c78a38ee7093a65fde`. + +Candidate01 was stopped after the parent found an actual Power inventory-lock +reflection failure during finite operations. The parent repaired that code. +Historical candidate01 results remain bound to their original inputs, including Electric +`D836D4AC22D1C46BFCD98B77A10A9475429D95141192B9C632F59DC87C273D73`. +They must not be relabeled as candidate02 or full family acceptance. + +## OreSpawn 4.1.1 Resume + +The resumed local qualification pins published OreSpawn 4.1.1.2601021, source +`101fd12b598934c353e417c57edc5c2bbefd5f53`, CurseForge file `9572926`. +Player and development SHA-256 are both +`810284ECB32D5249381F246498CEFA57E70FFF7CCC40110F799FC276CBC42BF7`. +The supplied public jar and the parent's frozen copy match that hash and retain +the actual `26.1.2-64.0.9` / Java 25 manifest. Provenance checks stay strict. + +The exact parent Power handoff is source +`d0bd0bdbde6c499e800e5a5b4ecc7f9ac63d2055`, player/development SHA-256 +`23ABCC1FEFDAA6988BF709437EB165C8948C757E773ADAB738092EB93CA8D2B0`. +The jar and all four unit-helper inputs were verified and frozen locally. +The clean full Gradle suite executes 52 tasks, with 117 unit tests and 28 +independent native resource checks passing without failures, errors or skips. +Javadoc, dependency, exact archive, checksum and Eclipse audits pass. +The independent warm-cache source export in a path with spaces passes the same +suite and reproduces all three release archives exactly. A genuinely empty-cache +export also passes all 52 tasks in 6m55s and reproduces all three archives. +The Gradle download and regenerated native Forge SDK use a new unseeded cache; +both the source and cache paths contain spaces and stay in this checkout. +The regenerated Forge binary and source jars also match the primary SDK exactly. + +The four old exact OreSpawn constants in the standalone audit and ECJ guard +were updated with user approval. The standalone source audit now passes all +nine checks. Fresh ECJ compiles 87 main and 36 test sources, the GameTest and +client-contract scopes, and passes all 117 tests with no ignored tests or +assumptions. The complete compiled inventory and strict negative controls remain +enabled. The original stale-pin failures remain separately preserved; the new +receipts are static-contracts-green-02.log and ecj-02/ecj-results.json under the +new Electric evidence directory. No assertion was relaxed or skipped. + +Final player SHA-256 is +`E2929ED56B4CEE1AD50B6B0D2BF1AF000AA15F2C9D66E5DFB3679B53FBDED9FC`. +Only the manifest differs from candidate09; all 576 other entries match. +No new gameplay or combined-family result is claimed here. The parent owns all +harnesses, scripts and combined profiles. Assigned actor/order evidence is +separate: all nine routes and both actual-order campaigns completed against +profile12. The three actor routes pass strictly; six dimension routes retain +PASS_WITH_NATIVE_DIFFERENCE for the independently controlled native wolf health +and max-health scaling. Original strict differences are preserved. Both actual +orders, six strict profile-boot audits, source/input custody and all 39 game +process closures pass. Instrumented order qualificationAllowed remains false. +The assigned handoff is actors-orders/final-owned-handoff-01.json under the +resume directory, SHA-256 +53E74959A4B3DF78F92652FB4C79C77BC185E326F23F525CB12E1CDBB2B48020. +This is not human gameplay acceptance or a global family verdict. New child evidence is +kept under +`D:/PowerAdvantage/_verification/base-2612-port-20261112/resume-orespawn411-20261112/electric`. +All historical results below retain their original candidate bindings. + +## Previous Candidate09 Checkpoint + +The candidate09 ordinary jar is +`0911C5D68C81E3B0E4EE3A596F20E25E59A4EDE1BFDC96A36398A3799E754FC1`. +It uses local Power `b947b4b81f3508fa220a212b41157ed2f801909f` / +`5309E7C433933F1426543310611292BF80388A476836139F0117E6A7E5C9AFC1`. +The full 51-task suite passes 117 unit and 28 native checks. Fresh ECJ passes +all 117 tests with the complete compiled inventory and negative controls. +Evidence is in `build/port-evidence/candidate09-lifecycle-pin-01`. + +Only the three Power provenance attributes in the ordinary manifest change +from the previous `E873...` and frozen `F2E71...` archives. Every class and +resource is identical; sources and Javadoc jars are also identical. The new +Power hash must not be recorded against either old Electric player hash. + +Power adds a canonical quartz lookup alias and a load-complete profile migration +and cache refresh. This is not an OreSpawn patch or a log waiver. Actual first +boot, restart and new-chunk checks must use a newly sealed candidate09 family. +The candidate08 failures below remain unchanged and do not qualify this pin. + +The sealed family09 profile retains the old `F2E71...` Electric archive, not the +repinned `0911...` jar above. Its raw retained-profile checks pass original110 +but record first-boot quartz failures for released110 and released112. The +stopped Forge registry actually contains the valid old-to-canonical quartz +alias, which the old canonical-ID-only check does not read; there are no ignored +quartz warnings. Raw failures remain unchanged while that oracle boundary is +reviewed. A separate Base Metals legacy/default duplicate-placement review is +also unresolved. Neither issue is waived by these passing child build checks. + +## Candidate08 History + +The candidate08 ordinary jar is +`E873CE65BA94AF5D37AB4FD059368251427C81DB88F4D5AD2CCADC97ECAA9913`. +It uses local Power `e565f99284de5e6c7860d378abc43b96d9faa5ee` / +`CC94656A75B3E6311E5F9F7CB4E4E60014B94A87665743D5A5D3C84CEA849720`. +Only dependency provenance in the ordinary jar's manifest changes from +candidate07; all classes and resources are identical. Sources and Javadoc +jars are also identical. Keeping the old player hash would retain a false +Power source/hash manifest, so the new ordinary bytes have their own receipt. + +The full 51-task suite passes 117 unit and 28 native checks. Fresh ECJ output +passes all 117 tests with the complete compiled inventory and negative controls. +Current evidence belongs to `build/port-evidence/candidate08-schema-pin-01`. +It is not family gameplay, generation, graphics or manual acceptance. + +The parent's frozen family08 profile instead contains the previous `F2E71...` +ordinary Electric jar. Its retained-profile receipts are evidence for those +exact frozen bytes, not for this repinned `E873...` archive. Original110 passed +both boots. Released110 and released112 failed first boot on unresolved +`minecraft:quartz_ore`; their passing restarts do not waive that real failure. +All other retained output payloads and policy selections were preserved, and +released110's disabled sulfur produced zero blocks. New family campaigns are +held for the parent's lifecycle repair. Failed logs and raw verdicts remain +unchanged. + +Candidate07 remains blocked by its actual first OS1 startup failure: seeding +schema5 typed defaults into a schema4 provider override caused OreSpawn to +reject `placement_channel`. Power's candidate08 repair upgrades only the owned +provider schema at that boundary; global8 and world7 markers remain unchanged. +The successful local07 checks and failed live07 receipts remain historical. +No old result is relabelled as08 or used to waive the early legacy startup gate. + +## Candidate07 History + +The candidate07 ordinary jar is +`F2E71F0693033FB4BBA0DE775A542C185C9163AEFA8BF819D0E8F423121A94BC`. +It uses local Power `9f6ffb30a14efe4360fee0cbdfa52dc8090a9819` / +`05031F1A3DCF25A405D9222E65619F7173C37CCF57B76308B72F741CA0E5198C`. +The full 51-task suite passes 117 unit and 28 independent native checks; +fresh ECJ output passes all 117 tests with the full compiled inventory. +The two added tests check the actual compiled constructor path, fail-closed +exception handling, single registration and the native JSON defaults. + +Only the manifest and the two migration integration classes differ from the +candidate06 ordinary jar. All resources, PNGs and other classes are exact. +Both new independent source snapshots pass all 51 tasks, 117 unit tests and +28 native checks, reproducing all three jars exactly. One uses the earlier +cold-generated engine cache; no fresh cold-download claim is made. All four +named source GameTests also pass against these pins, with their first-boot +warnings retained. Current receipts are kept separately in +`build/port-evidence/candidate07-retained-profile-01`; old results do not +qualify this jar. Family migration, generation, graphics and endurance remain +separate candidate-bound gates, not a release or manual acceptance claim. + +An actual frozen06 retained-profile regression started a fresh normal world +with the approved released110 saved profile, generated 126 sampled chunks, +then restarted in a second process. Oil/lithium/sulfur counts were positive, +but both boots correctly failed the profile contract: old output IDs remained +unresolved and new active defaults were added. The source profile, warnings, +raw failure and frozen06 inputs are retained, not rewritten as candidate07. + +Read-only collision inventory covers all eight supported machine source routes +and nine actor/dimension source routes. All 22 existing global/world profile +hashes match their genuine pre-setup seals, with no original-source active +canonical collisions. Candidate06 destinations did acquire duplicate defaults. +The released110 machine source has real world overrides: lithium frequency +0.25 versus global 0.125, and sulfur disabled versus globally enabled. +The released112 machine and actor fixtures are distinct and keep their own +profile/import oracles. Unsupported chained113 findings are labelled separately. +Dormant quartz custom rules are not proof of lost active vanilla generation: +these profiles keep vanilla management and global ore suppression disabled. + +## Candidate06 History + +The frozen candidate06 player jar is +`DFC63C4E619A74BEE2A22146D9720ADB2B32801865EAE1B716726424015775FA`. +It uses Power `0ac874de66f792c6efca5bccd0536b1b113fb786`, ordinary jar +`618C3F96736392CE2853575F5A6A076EAC2A9B9F76F9303D86DEF58E418857D0`, +and Base Metals `331fa46914f854db71a8c33500cc559a8878b7d3` / `67862388...`. +Java classes and PNGs remain byte-identical to the previous `1936D3E2...` +candidate. The intended resource differences are the two atlas definitions and +removal of the abstract battery's standalone item definition, plus dependency +provenance in the manifest. This is not final family or manual acceptance. + +Actual native client loading exposed an 8-pixel direct-render laser beam being +included by Power's global materials directory, and a standalone item definition +for the unregistered abstract battery parent. Hegel replaced the atlas imports +with explicit namespace-owned sprites. Electric's common native registry census +confirms `battery_base` is absent and all four real batteries are registered. +Only `items/battery_base.json` is removed; its inherited parent model and every +PNG remain unchanged. A focused resource regression fails before that deletion +and passes after it, including native atlas codec and required-sprite checks. + +The earlier diagnostic asset jar is +`982D94A4B469216643D1B6609B3C16990447F82DD4A47BD155EE00CA91BE60C4`. +Against the intermediate jar it changes only the blocks atlas, adds the items +atlas and removes the abstract item definition. All class and PNG bytes match. +It uses the sealed `000A...` compile input and is not final qualification. +Its original receipt is retained. Candidate06 uses the later coherent atlas +pins and needs its own graphical, migration and endurance receipts. + +Candidate06's forced fresh 51-task suite passes 115 unit and 28 native resource tests, +with zero failures or skips. Fresh ECJ output passes all 115 tests with the +complete compiled-method inventory and negative controls. All four named +source GameTests and both new-pin reproduction builds pass. Receipts are +recorded separately in `build/port-evidence/candidate06-final-assets-01`. +This local checkpoint is not final family qualification. + +The parent repaired the shared migration context after candidate05's actual +original110 actor run lost four Steam enchantments from cart and dropped-item +stacks. The raw failure, source lineage and independently assessed dimension +results remain bound to candidate05. New qualification must use the repaired +Power and Base Metals inputs below; no old result is relabeled for candidate06. +The intermediate `fb5fb0e` source-pin jar is retained separately and is not the +final candidate. The later `540bc666` context jar and `4D2C5563...` child also +remain historical, not relabeled as the atlas-repaired candidate06. + +## Target + +- Minecraft 26.1.2, Forge 64.1.5, MCP 20260409.101008, official runtime names. +- Temurin 25.0.3+9 for all roles, ForgeGradle 7.0.34, Gradle 9.6.1, `reobf=false`. +- baseElectric 2.2.17.2601021; basePower 2.4.16.2601021 and Base Metals 3.1.2.2601021 required. +- OreSpawn 4.1.1.2601021, source `101fd12b598934c353e417c57edc5c2bbefd5f53`, + ordinary jar SHA-256 `810284ECB32D5249381F246498CEFA57E70FFF7CCC40110F799FC276CBC42BF7`. + +The local Power checkpoint is `d0bd0bdbde6c499e800e5a5b4ecc7f9ac63d2055`; +the Base Metals shared-hook checkpoint is `331fa46914f854db71a8c33500cc559a8878b7d3`. +Their exact ordinary jar hashes are recorded in `gradle.properties`. +These are local integration inputs, not public releases or completed family +qualification. Earlier provisional diagnostics remain under their original +hashes and scope labels. + +## Compatibility + +Registry IDs, legacy aliases, flat NBT keys, recipes, unlock IDs and machine +balance remain unchanged. Native recipe assembly takes only the input; +results use native stack templates. Client renderers use native extraction +and the target camera-state package. Rendered scenes remain a required gate. + +The old villager trade event no longer exists. The six Electric librarian +offers are now `villager_trade` resources, added to the existing novice and +apprentice tags without replacing vanilla offers. The same quantity and +emerald ranges, seven uses, one XP and zero price multiplier are retained. + +Root pack metadata declares data format 101.1. Forge's hidden client pack +must synthesize native resource format 84.0. Both paths are tested through +actual Forge discovery, with strict warning and error rejection unchanged. + +## Candidate06 Verification + +The independent native suite passes 28 tests without skips. All 58 recipe +resources and 50 unlocks are retained. Native pack discovery accepts resource +84.0 and data 101.1, with zero warning/error diagnostics. + +Compilation of all 87 production sources and all 36 test sources +passes against Power `618C3F96736392CE2853575F5A6A076EAC2A9B9F76F9303D86DEF58E418857D0` +and Base Metals `6786238835D4D8A653C769F3B0E61D226959146300E17523799CFDF17540FD19`. +All 115 unit tests pass without skips. Fresh ECJ output also passes 115 tests; +main, test, GameTest and client-contract scopes compile as Java 25. Both pinned +unit-support sources are compiled with the tests. The separately rebuilt agent +must match `088FC137BEBB9B2E75C596DE868499E6099CF228BECFCF07EAE36791245FD1B6`. +It never enters production jars or game launches. The ECJ gate scans every +fresh compiled `@Test` method, rejects filtered/stale XML and unreviewed runners, +and retains forbidden-JDK-access and namespace-exclusion negative controls. + +The full 51-task local release/dependency/artifact/Eclipse suite passes against +these local pins. Native resource children use an explicit four-CPU argument, +clear/reject all three inherited JVM-option variables and record runtime +arguments and classpaths. No warning or error filtering was added. + +All four named source GameTests passed again against the current Power pin on +actual Forge 64.1.5, followed by normal save and shutdown: + +- `baseelectric:drillupgradesandtransferslegacybattery` +- `baseelectric:recipeavailabilitydoesnotremovediscovery` +- `baseelectric:siliconandsolderresolverealmaterialtags` +- `baseelectric:turretusesnativeupdatepacket` + +The XML and runtime receipt are in `candidate06-final-assets-01/gametest-01`. +This is source-output GameTest evidence, not frozen-player-jar proof. The +first-boot log retains configuration-default corrections and native union-URL +warnings; its clean-log gate is not green. The runtime receipt records Java +25.0.3+9, four CPUs, explicit JVM arguments and absent inherited JVM options. + +The previous Power pin's two independent source snapshots in paths containing spaces pass the same +51-task suite, each with 115 unit and 28 native checks and no skips. One uses +the established isolated cache; the other starts with a new empty cache, +downloads Gradle and native inputs, and regenerates the Forge engine. That +engine and all three Electric jars match the primary bytes exactly. The +completed warm/cold receipts remain under `candidate02-2612-01`. These runs +started before the fuel-repair hold; no later pin is covered by them. The new +checkpoint has separate passing reproduction receipts, using the established +cache and the previously cold-generated engine cache rather than repeating +downloads. Both use new source snapshots and output directories containing +spaces, force all 51 tasks without a build cache, and reproduce the ordinary, +sources and Javadoc jars exactly. Both again pass 115 unit and 28 native checks. + +The current frozen ordinary Electric and Power jars also pass the packaged +server smoke's 19 narrow checks, followed by normal save/stop. The run uses +Java 25 with explicit CPU arguments and no inherited JVM-option variables. +It is not a restart, whole-world, fuel-container or clean-log qualification. + +GUI/assets/scenes and combined development and packaged classpaths remain +parent-owned gates. Current logs, including failures, are retained under +`build/port-evidence/initial-2612-01` for candidate01 and the separate candidate02 +folder and separate candidate05/candidate06 folders; none is a release acceptance claim. + +A candidate01 packaged smoke already in flight at the hold completed 19 narrow +checks, including drill inventory conversion, battery components, sulfur drops, +damage types and a normal save/stop. It did not exercise the failed inventory +lock path, a restart, or a whole-world upgrade. It remains historical evidence. + +The approved Base Metals and public OreSpawn jars both retain their exact +`26.1.2-64.0.9` build stamps. Electric targets runtime 64.1.5 and audits those +dependency stamps separately without rebuilding or changing their jars. +Actual coordinated client/server checks still determine runtime compatibility. + +No protected or manual world may be started by these checks. The parent owns +combined migrations, gameplay campaigns and endurance. The historical turbine +manual-world limit is not a pass for this version. + +No 26.1.2 push, PR, tag or publication is authorized. diff --git a/gradle.properties b/gradle.properties index 79bd931..3a82cf5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,58 +5,66 @@ org.gradle.caching=true org.gradle.parallel=false org.gradle.workers.max=2 net.minecraftforge.gradle.merge-source-sets=false +reobf=false -minecraft_version=1.21.11 -minecraft_version_range=[1.21.11] -forge_version=61.1.0 -forge_installer_sha256=8BD1724EAEAF26F51DACFB7FDC46D79E9B640705B70D5F0019ED38907145EB48 -forge_shim_sha256=09008DB9881E35E62669865F3B7420741EDB1A52BC59C67A5FA03D24C6C8E6E3 -forge_server_sha256=8D997159003977A22DB8E4EA6DAAB6CBB0E76F752A196A1821C74734A1CF71D9 -forge_universal_sha256=101041931319D508D94EF9ED6643CA4374EA3950237443301F7AA5A3B2631CC4 -forge_version_range=[61.1.0,62) -loader_version_range=[61,) +minecraft_version=26.1.2 +minecraft_version_range=[26.1.2] +forge_version=64.1.5 +forge_installer_sha256=6E01323837051176762BD466340A601DA122F179FE6A00AC616E05429AE07648 +forge_shim_sha256=0C74CB2A8E7A71AEB7055396C1E25864584A2D837D7DD42136A06D0B41ADD545 +forge_server_sha256=C6B85C178B037519464D436A75B7C68408CBE591576270BA0E826542B50BF41B +forge_universal_sha256=687C2698ECC1906CA2AD398A445C2498CF8BC09F1CBE6C2A6E24BFC9F90736C7 +forge_version_range=[64.1.5,65) +loader_version_range=[64,65) mapping_channel=official -mapping_version=1.21.11 -mcp_version=20251223.124241 +mapping_version=26.1.2 +mcp_version=20260409.101008 loader_name=forge loader_code=1 -java_version=21 -java_toolchain_version=21.0.7+6 -java_setup_version=21.0.7+6.0.LTS -gradle_java_version=21 +java_version=25 +java_toolchain_version=25.0.3+9 +java_setup_version=25.0.3+9.0.LTS +gradle_java_version=25 curseforge_project_id=1783006 mod_id=baseelectric mod_name=baseElectric mod_license=GPL-3.0-only -mod_version=2.2.16.121111 +mod_version=2.2.17.2601021 mod_group=zone.moddev.mc.baseelectric mod_authors=DrCyano mod_description=Electric-powered machines and infrastructure for basePower. cf_requirements=basepower,base-metals cf_optionals= -cf_versions=1.21.11 +cf_versions=26.1.2 -poweradvantage_version=2.4.15.121111 +poweradvantage_version=2.4.16.2601021 poweradvantage_pin_state=MERGED_SOURCE poweradvantage_source_repository=MinecraftModDevelopmentMods/basePower -# Verified public basePower release; the ordinary native jar is also the dev jar. -poweradvantage_source_ref=9b0043483d79d3e575895f12aeb0dde7ef6c4890 -poweradvantage_public_file_id=9566998 -poweradvantage_deobf_sha256=C98D2CF163BE2480D9AFB0023BBD2D92D401AE0D50BEF73191235BF3F83774A7 -poweradvantage_runtime_sha256=C98D2CF163BE2480D9AFB0023BBD2D92D401AE0D50BEF73191235BF3F83774A7 - -basemetals_version=3.1.2.121111 -basemetals_source_commit=6f4c44e4d0a68bca3838fb025c09cddfaaa08fe8 -# Verified public release includes the shared non-cancelling migration hook. -basemetals_public_file_id=9566738 -basemetals_deobf_sha256=598D3FB0FF956EBCC67FE477A01CE393C75D0013AF0CE00A79E2072B63E9F2BA -basemetals_runtime_sha256=598D3FB0FF956EBCC67FE477A01CE393C75D0013AF0CE00A79E2072B63E9F2BA - -orespawn_version=4.1.0.121111 -orespawn_source_commit=ecad75b24309a344f712d898d717c6a8823fc9f2 -orespawn_public_file_id=9268464 -orespawn_deobf_sha256=DE39929057288CEA110196C9E9D708E129B80BC360A2E53976337FFA6A84A67B -orespawn_runtime_sha256=DE39929057288CEA110196C9E9D708E129B80BC360A2E53976337FFA6A84A67B +# Exact merged parent source; the hosted gate also checks its public player jar. +poweradvantage_source_ref=a684e71e5397cb5a1a4879dcf0b6a2be0815e85d +poweradvantage_public_file_id=9576332 +poweradvantage_deobf_sha256=23ABCC1FEFDAA6988BF709437EB165C8948C757E773ADAB738092EB93CA8D2B0 +poweradvantage_runtime_sha256=23ABCC1FEFDAA6988BF709437EB165C8948C757E773ADAB738092EB93CA8D2B0 + +basemetals_version=3.1.2.2601021 +basemetals_built_on=26.1.2-64.0.9 +# Published shared-hook patch; family qualification is recorded separately. +basemetals_source_commit=5e708468302a7fb3b35aca09d5f832f8882f88f3 +basemetals_public_file_id=9576007 +basemetals_deobf_sha256=6786238835D4D8A653C769F3B0E61D226959146300E17523799CFDF17540FD19 +basemetals_runtime_sha256=6786238835D4D8A653C769F3B0E61D226959146300E17523799CFDF17540FD19 + +unit_bootstrap_sha256=088FC137BEBB9B2E75C596DE868499E6099CF228BECFCF07EAE36791245FD1B6 +unit_bootstrap_java_sha256=D4B72FD65D9F8549B142507C6446E3B1E047B637784AF9FA74E8F28413E14A51 +unit_registries_java_sha256=01DDE50C9874C2DC501BE2F699CD8D16D6974FACFAAC1B803434CA8C64227ABF +unit_loader_java_sha256=455CA9403D9CBEA72E7D055966D6CDC822E634418C14B3CAD0500588DEE35E5B + +orespawn_version=4.1.1.2601021 +orespawn_built_on=26.1.2-64.0.9 +orespawn_source_commit=101fd12b598934c353e417c57edc5c2bbefd5f53 +orespawn_public_file_id=9572926 +orespawn_deobf_sha256=810284ECB32D5249381F246498CEFA57E70FFF7CCC40110F799FC276CBC42BF7 +orespawn_runtime_sha256=810284ECB32D5249381F246498CEFA57E70FFF7CCC40110F799FC276CBC42BF7 diff --git a/gradle/forge61-mavenizer.gradle b/gradle/forge64-mavenizer.gradle similarity index 58% rename from gradle/forge61-mavenizer.gradle rename to gradle/forge64-mavenizer.gradle index f587816..a84b873 100644 --- a/gradle/forge61-mavenizer.gradle +++ b/gradle/forge64-mavenizer.gradle @@ -1,22 +1,22 @@ import java.security.MessageDigest import org.gradle.jvm.toolchain.JvmVendorSpec -def fixture = file(providers.gradleProperty('forge61MavenizerJar').getOrElse( - '../../../../MinecraftMineralogy/OS12111/OreSpawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar')) -def expected = 'B36CA046DB72E30F27BD38E02B4AE34274ADB51AE1D65BAF9E1D07763755682E' +def fixture = file(providers.gradleProperty('forge64MavenizerJar').getOrElse( + '../../../../MinecraftMineralogy/OS2612/OreSpawn/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar')) +def expected = 'BB0DACB3DEB2A711CA67DD402D5FDAB5A9847C37C7B9B596DEC7CF37378FA085' if (!fixture.isFile()) { - throw new GradleException("Missing Forge 61 Mavenizer fixture: ${fixture}. Set -Pforge61MavenizerJar=.") + throw new GradleException("Missing Forge 64 Mavenizer fixture: ${fixture}. Set -Pforge64MavenizerJar=.") } def actual = MessageDigest.getInstance('SHA-256').digest(fixture.bytes).encodeHex().toString().toUpperCase(Locale.ROOT) if (actual != expected) { - throw new GradleException("Forge 61 Mavenizer fixture SHA-256 mismatch: expected ${expected}, got ${actual}") + throw new GradleException("Forge 64 Mavenizer fixture SHA-256 mismatch: expected ${expected}, got ${actual}") } def rules = new File(fixture.parentFile, 'minecraft-source-compatibility.json') -def expectedRules = '88F2EE4C6B7C903EB1C7D693C0BEEFC0EFC601C1023FB197F7CBA187B1142DB7' +def expectedRules = '21D1F5984E735BCEA1B1EFB9F965655796B39538FC91AAED1C3474004710423D' if (!rules.isFile() || MessageDigest.getInstance('SHA-256').digest(rules.bytes) .encodeHex().toString().toUpperCase(Locale.ROOT) != expectedRules) { - throw new GradleException("Missing or mismatched Forge 61 Mavenizer rules: ${rules}") + throw new GradleException("Missing or mismatched Forge 64 Mavenizer rules: ${rules}") } fgtools.configure('mavenizer') { diff --git a/src/gametest/java/zone/moddev/mc/baseelectric/gametest/ElectricGameTests.java b/src/gametest/java/zone/moddev/mc/baseelectric/gametest/ElectricGameTests.java index 86bce49..39db043 100644 --- a/src/gametest/java/zone/moddev/mc/baseelectric/gametest/ElectricGameTests.java +++ b/src/gametest/java/zone/moddev/mc/baseelectric/gametest/ElectricGameTests.java @@ -36,6 +36,9 @@ private ElectricGameTests() {} @net.minecraftforge.eventbus.api.listener.SubscribeEvent public static void registerFunctions(net.minecraftforge.registries.RegisterEvent event) { + if (event.getRegistryKey().equals(net.minecraft.core.registries.Registries.TEST_FUNCTION)) { + recordRuntimeBoundary(); + } event.register(net.minecraft.core.registries.Registries.TEST_FUNCTION, registry -> { registry.register(Identifier.parse("baseelectric:drillupgradesandtransferslegacybattery"), ElectricGameTests::drillUpgradesAndTransfersLegacyBattery); @@ -54,6 +57,33 @@ public static void main(String[] args) throws Exception { NbtIo.writeCompressed(NbtUtils.snbtToStructure(Files.readString(Path.of(args[0]))), output); } + private static void recordRuntimeBoundary() { + var arguments = java.lang.management.ManagementFactory.getRuntimeMXBean().getInputArguments(); + for (String name : java.util.List.of("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS")) { + if (System.getenv(name) != null) throw new AssertionError("Inherited native GameTest JVM options: " + name); + } + if (!arguments.contains("-XX:ActiveProcessorCount=4") || Runtime.getRuntime().availableProcessors() != 4) { + throw new AssertionError("Native GameTests require an explicit four-CPU JVM argument"); + } + String destination = System.getProperty("baseelectric.gameTestRuntimeReceipt"); + if (destination == null) throw new AssertionError("Native GameTest runtime receipt path is missing"); + var result = new com.google.gson.JsonObject(); + result.addProperty("scope", "SOURCE_GAMETEST_NOT_FROZEN_PLAYER_JAR"); + result.addProperty("javaRuntimeVersion", System.getProperty("java.runtime.version")); + result.addProperty("availableProcessors", Runtime.getRuntime().availableProcessors()); + result.addProperty("environmentJvmOptionsAbsent", true); + result.addProperty("classpath", System.getProperty("java.class.path")); + result.addProperty("electricCodeSource", ElectricAdvantage.class.getProtectionDomain().getCodeSource().getLocation().toString()); + var inputs = new com.google.gson.JsonArray(); + arguments.forEach(inputs::add); + result.add("jvmArguments", inputs); + try { + Files.writeString(Path.of(destination), result.toString(), java.nio.file.StandardOpenOption.CREATE_NEW); + } catch (java.io.IOException failure) { + throw new AssertionError("Cannot preserve native GameTest runtime receipt", failure); + } + } + @GameTest(name = "drillupgradesandtransferslegacybattery", structure = "empty") public static void drillUpgradesAndTransfersLegacyBattery(GameTestHelper helper) { BlockPos source = new BlockPos(1, 1, 1); @@ -96,6 +126,26 @@ public static void drillUpgradesAndTransfersLegacyBattery(GameTestHelper helper) @GameTest(name = "recipeavailabilitydoesnotremovediscovery", structure = "empty") public static void recipeAvailabilityDoesNotRemoveDiscovery(GameTestHelper helper) { var server = helper.getLevel().getServer(); + var itemIds = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKeys().stream() + .filter(id -> id.getNamespace().equals(ElectricAdvantage.MODID)) + .map(Object::toString).sorted().toList(); + helper.assertTrue(!itemIds.contains("baseelectric:battery_base"), "Abstract battery parent became a registered item"); + for (String battery : java.util.List.of("alkaline_battery", "lead_acid_battery", "lithium_battery", "nickel_hydride_battery")) { + helper.assertTrue(itemIds.contains("baseelectric:" + battery), "Concrete battery is not registered: " + battery); + } + var census = new com.google.gson.JsonObject(); + census.addProperty("scope", "ACTUAL_NATIVE_COMMON_ITEM_REGISTRY_SOURCE_GAMETEST_NOT_CLIENT_PIXELS"); + census.addProperty("abstractBatteryRegistered", false); + var registryEntries = new com.google.gson.JsonArray(); + itemIds.forEach(registryEntries::add); + census.add("registeredElectricItems", registryEntries); + try { + Files.writeString(Path.of(System.getProperty("baseelectric.gameTestRuntimeReceipt")) + .resolveSibling("electric-item-registry-census.json"), census.toString(), + java.nio.file.StandardOpenOption.CREATE_NEW); + } catch (java.io.IOException failure) { + throw new AssertionError("Cannot retain actual item registry census", failure); + } var advancement = Identifier.fromNamespaceAndPath(ElectricAdvantage.MODID, "recipes/alkaline_battery"); helper.assertTrue(server.getAdvancements().get(advancement) != null, "A material-gated recipe lost its discovery advancement"); diff --git a/src/main/java/zone/moddev/mc/baseelectric/ElectricAdvantage.java b/src/main/java/zone/moddev/mc/baseelectric/ElectricAdvantage.java index 479774c..bcedea4 100644 --- a/src/main/java/zone/moddev/mc/baseelectric/ElectricAdvantage.java +++ b/src/main/java/zone/moddev/mc/baseelectric/ElectricAdvantage.java @@ -21,17 +21,16 @@ import zone.moddev.mc.baseelectric.init.GUI; import zone.moddev.mc.baseelectric.init.OreSpawnWorldGen; import zone.moddev.mc.baseelectric.init.TreasureChests; -import zone.moddev.mc.baseelectric.init.Villages; import zone.moddev.mc.baseelectric.recipe.RecipeConditions; import zone.moddev.mc.baseelectric.migration.ElectricLegacyMigrations; -/** Forge 1.21.11 entry point for baseElectric. */ +/** Forge 26.1.2 entry point for baseElectric. */ @Mod(ElectricAdvantage.MODID) public final class ElectricAdvantage { public static final String MODID = "baseelectric"; public static final String LEGACY_MODID = "electricadvantage"; public static final String NAME = "baseElectric"; - public static final String VERSION = "2.2.16.121111"; + public static final String VERSION = "2.2.17.2601021"; public static final Logger LOGGER = LogManager.getLogger(MODID); public static ElectricAdvantage INSTANCE; @@ -59,12 +58,13 @@ private static void migrateLegacyOreSpawnConfiguration() { try { LegacyOreSpawnConfigBridge.Result result = LegacyOreSpawnConfigBridge.migrate(FMLPaths.CONFIGDIR.get(), MODID, LEGACY_MODID); + OreSpawnWorldGen.registerMigrationDefaults(FMLPaths.CONFIGDIR.get()); if (result.wasMigrated()) { LOGGER.info("Imported {} legacy Electric Advantage OreSpawn rule(s) from '{}' into '{}'", result.getRuleCount(), result.getSource(), result.getTarget()); } } catch (IOException exception) { - LOGGER.error("Could not migrate legacy Electric Advantage OreSpawn configuration", exception); + throw new IllegalStateException("Could not preserve Electric Advantage OreSpawn configuration", exception); } } @@ -87,7 +87,6 @@ private void commonSetup(FMLCommonSetupEvent event) { event.enqueueWork(GUI::init); ElectricAdvantageConfig.apply(); TreasureChests.init(FMLPaths.CONFIGDIR.get()); - Villages.init(); LOGGER.info("{} {} common setup complete", NAME, VERSION); } diff --git a/src/main/java/zone/moddev/mc/baseelectric/blocks/Ore.java b/src/main/java/zone/moddev/mc/baseelectric/blocks/Ore.java index cf77455..db1726f 100644 --- a/src/main/java/zone/moddev/mc/baseelectric/blocks/Ore.java +++ b/src/main/java/zone/moddev/mc/baseelectric/blocks/Ore.java @@ -11,6 +11,7 @@ import net.minecraft.world.level.material.MapColor; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemInstance; import net.minecraft.world.item.enchantment.EnchantmentHelper; import net.minecraft.world.item.enchantment.Enchantments; import net.minecraft.world.level.storage.loot.LootParams; @@ -52,14 +53,14 @@ public Ore(int pickLevel) { @Override public List getDrops(BlockState state, LootParams.Builder context) { - ItemStack tool = context.getOptionalParameter(LootContextParams.TOOL); + ItemInstance tool = context.getOptionalParameter(LootContextParams.TOOL); ItemStack dropItem = drop.get(); if (dropItem.isEmpty() || (tool != null && EnchantmentHelper.getItemEnchantmentLevel(context.getLevel().registryAccess() .lookupOrThrow(Registries.ENCHANTMENT).getOrThrow(Enchantments.SILK_TOUCH), tool) > 0)) { return Collections.singletonList(new ItemStack(this)); } - RandomSource random = context.getLevel().random; + RandomSource random = context.getLevel().getRandom(); ItemStack result = dropItem.copy(); result.setCount(dropItem.getCount() + (dropRange > 1 ? random.nextInt(dropRange) : 0)); return Collections.singletonList(result); diff --git a/src/main/java/zone/moddev/mc/baseelectric/compat/BaseMetalsCompat.java b/src/main/java/zone/moddev/mc/baseelectric/compat/BaseMetalsCompat.java index cabdac3..3eecc94 100644 --- a/src/main/java/zone/moddev/mc/baseelectric/compat/BaseMetalsCompat.java +++ b/src/main/java/zone/moddev/mc/baseelectric/compat/BaseMetalsCompat.java @@ -17,7 +17,7 @@ public final class BaseMetalsCompat { private BaseMetalsCompat() {} public static boolean isLoaded() { - return ModList.get().isLoaded("basemetals"); + return ModList.isLoaded("basemetals"); } /** 1.13 additions are supplied as JSON crushing recipes and item tags. */ @@ -36,7 +36,7 @@ public static ItemStack getCrusherRecipeOutput(Level world, ItemStack input) { SingleRecipeInput inventory = new SingleRecipeInput(input.copy()); for (RecipeHolder holder : server.recipeAccess().getRecipes()) { if (holder.value() instanceof CrushingRecipe candidate && candidate.matches(inventory, world)) { - return candidate.assemble(inventory, world.registryAccess()); + return candidate.assemble(inventory); } } } diff --git a/src/main/java/zone/moddev/mc/baseelectric/entities/HydroturbineEntity.java b/src/main/java/zone/moddev/mc/baseelectric/entities/HydroturbineEntity.java index e2c3f52..95453f5 100644 --- a/src/main/java/zone/moddev/mc/baseelectric/entities/HydroturbineEntity.java +++ b/src/main/java/zone/moddev/mc/baseelectric/entities/HydroturbineEntity.java @@ -30,7 +30,7 @@ public class HydroturbineEntity extends net.minecraft.world.entity.Entity{ public HydroturbineEntity(EntityType type, Level w) { super(type, w); this.blocksBuilding = true; - roffset = w.random.nextInt(8); + roffset = w.getRandom().nextInt(8); } public HydroturbineEntity(Level w, BlockPos parentTileEntity) { this(w, w.getBlockEntity(parentTileEntity)); diff --git a/src/main/java/zone/moddev/mc/baseelectric/graphics/ElectricRenderTypes.java b/src/main/java/zone/moddev/mc/baseelectric/graphics/ElectricRenderTypes.java index 1a84168..9fae384 100644 --- a/src/main/java/zone/moddev/mc/baseelectric/graphics/ElectricRenderTypes.java +++ b/src/main/java/zone/moddev/mc/baseelectric/graphics/ElectricRenderTypes.java @@ -1,6 +1,7 @@ package zone.moddev.mc.baseelectric.graphics; import com.mojang.blaze3d.pipeline.BlendFunction; +import com.mojang.blaze3d.pipeline.ColorTargetState; import com.mojang.blaze3d.pipeline.RenderPipeline; import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.client.renderer.rendertype.RenderSetup; @@ -19,7 +20,7 @@ private static RenderPipeline pipeline(boolean translucent) { .withLocation(Identifier.fromNamespaceAndPath("baseelectric", translucent ? "pipeline/unshaded_translucent" : "pipeline/unshaded")) .withShaderDefine("NO_CARDINAL_LIGHTING").withSampler("Sampler1").withCull(false); - if (translucent) builder.withBlend(BlendFunction.TRANSLUCENT); + if (translucent) builder.withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)); RenderPipeline pipeline = builder.build(); RenderPipelines.PIPELINES_BY_LOCATION.put(pipeline.getLocation(), pipeline); return pipeline; diff --git a/src/main/java/zone/moddev/mc/baseelectric/graphics/HydroturbineRenderer.java b/src/main/java/zone/moddev/mc/baseelectric/graphics/HydroturbineRenderer.java index f626870..81472d9 100644 --- a/src/main/java/zone/moddev/mc/baseelectric/graphics/HydroturbineRenderer.java +++ b/src/main/java/zone/moddev/mc/baseelectric/graphics/HydroturbineRenderer.java @@ -7,7 +7,7 @@ import net.minecraft.client.renderer.rendertype.RenderTypes; import net.minecraft.client.renderer.SubmitNodeCollector; import net.minecraft.client.renderer.entity.state.EntityRenderState; -import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.state.level.CameraRenderState; import com.mojang.math.Axis; import net.minecraft.client.renderer.texture.OverlayTexture; import net.minecraft.client.renderer.entity.EntityRenderer; diff --git a/src/main/java/zone/moddev/mc/baseelectric/graphics/LaserDrillRenderer.java b/src/main/java/zone/moddev/mc/baseelectric/graphics/LaserDrillRenderer.java index e0e6973..1bbfeb3 100644 --- a/src/main/java/zone/moddev/mc/baseelectric/graphics/LaserDrillRenderer.java +++ b/src/main/java/zone/moddev/mc/baseelectric/graphics/LaserDrillRenderer.java @@ -8,7 +8,7 @@ import net.minecraft.client.renderer.SubmitNodeCollector; import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; import net.minecraft.client.renderer.feature.ModelFeatureRenderer; -import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.state.level.CameraRenderState; import net.minecraft.world.phys.Vec3; import net.minecraft.client.renderer.texture.OverlayTexture; import com.mojang.math.Axis; diff --git a/src/main/java/zone/moddev/mc/baseelectric/graphics/LaserTurretRenderer.java b/src/main/java/zone/moddev/mc/baseelectric/graphics/LaserTurretRenderer.java index 237682c..40e46bc 100644 --- a/src/main/java/zone/moddev/mc/baseelectric/graphics/LaserTurretRenderer.java +++ b/src/main/java/zone/moddev/mc/baseelectric/graphics/LaserTurretRenderer.java @@ -8,7 +8,7 @@ import net.minecraft.client.renderer.SubmitNodeCollector; import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; import net.minecraft.client.renderer.feature.ModelFeatureRenderer; -import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.state.level.CameraRenderState; import net.minecraft.client.renderer.texture.OverlayTexture; import net.minecraft.world.phys.Vec3; import com.mojang.math.Axis; diff --git a/src/main/java/zone/moddev/mc/baseelectric/init/OreSpawnWorldGen.java b/src/main/java/zone/moddev/mc/baseelectric/init/OreSpawnWorldGen.java index 1e6a004..10ec6cc 100644 --- a/src/main/java/zone/moddev/mc/baseelectric/init/OreSpawnWorldGen.java +++ b/src/main/java/zone/moddev/mc/baseelectric/init/OreSpawnWorldGen.java @@ -1,11 +1,13 @@ package zone.moddev.mc.baseelectric.init; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import net.minecraft.resources.Identifier; import net.minecraftforge.fml.ModList; import zone.moddev.mc.baseelectric.ElectricAdvantage; +import zone.moddev.mc.basepower.migration.OreSpawnProfileMigration; import zone.moddev.mc.orespawn.api.GeologyFamily; import zone.moddev.mc.orespawn.api.OrePattern; import zone.moddev.mc.orespawn.api.OreSpawnApi; @@ -24,9 +26,14 @@ public final class OreSpawnWorldGen { private OreSpawnWorldGen() { } + public static void registerMigrationDefaults(Path configDirectory) throws IOException { + OreSpawnProfileMigration.registerProvider(configDirectory, + createProvider(ModList.isLoaded("mineralogy"), ModList.isLoaded("baseminerals")).toJson()); + } + public static void register(Path configDirectory) { - boolean mineralogyLoaded = ModList.get().isLoaded("mineralogy"); - boolean baseMineralsLoaded = ModList.get().isLoaded("baseminerals"); + boolean mineralogyLoaded = ModList.isLoaded("mineralogy"); + boolean baseMineralsLoaded = ModList.isLoaded("baseminerals"); boolean accepted = OreSpawnApi.enqueue(createProvider(mineralogyLoaded, baseMineralsLoaded)); String sulfurOwner = mineralogyLoaded ? "mineralogy" : baseMineralsLoaded ? "baseminerals" : ElectricAdvantage.MODID; diff --git a/src/main/java/zone/moddev/mc/baseelectric/init/Villages.java b/src/main/java/zone/moddev/mc/baseelectric/init/Villages.java deleted file mode 100644 index 59add05..0000000 --- a/src/main/java/zone/moddev/mc/baseelectric/init/Villages.java +++ /dev/null @@ -1,45 +0,0 @@ -package zone.moddev.mc.baseelectric.init; - -import java.util.List; -import net.minecraft.world.entity.npc.villager.VillagerProfession; -import net.minecraft.world.entity.npc.villager.VillagerTrades; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.trading.MerchantOffer; -import net.minecraft.world.item.trading.ItemCost; -import net.minecraftforge.event.village.VillagerTradesEvent; - -/** Adds the historical librarian offers through the native profession trade event. */ -public final class Villages { - private static boolean initialized; - - private Villages() {} - - public static synchronized void init() { - if (initialized) return; - VillagerTradesEvent.BUS.addListener(Villages::addTrades); - initialized = true; - } - - public static void addTrades(VillagerTradesEvent event) { - if (event.getType() != VillagerProfession.LIBRARIAN) return; - List novice = event.getTrades().get(1); - List apprentice = event.getTrades().get(2); - novice.add(sell(Blocks.electric_conduit.asItem(), -6, -3)); - novice.add(sell(Items.solder, -10, -5)); - novice.add(sell(Items.integrated_circuit, -6, -3)); - novice.add(sell(Items.blank_circuit_board, -6, -3)); - apprentice.add(sell(Items.petrolplastic_ingot, -8, -4)); - apprentice.add(sell(Items.power_supply_unit, 1, 3)); - } - - static VillagerTrades.ItemListing sell(Item item, int minimumPrice, int maximumPrice) { - return (level, villager, random) -> { - int price = minimumPrice + random.nextInt(maximumPrice - minimumPrice + 1); - int emeralds = price < 0 ? 1 : price; - int count = price < 0 ? -price : 1; - return new MerchantOffer(new ItemCost(net.minecraft.world.item.Items.EMERALD, emeralds), - new ItemStack(item, count), 7, 1, 0.0F); - }; - } -} diff --git a/src/main/java/zone/moddev/mc/baseelectric/machines/ElectricMachineTileEntity.java b/src/main/java/zone/moddev/mc/baseelectric/machines/ElectricMachineTileEntity.java index a29d401..abcbfea 100644 --- a/src/main/java/zone/moddev/mc/baseelectric/machines/ElectricMachineTileEntity.java +++ b/src/main/java/zone/moddev/mc/baseelectric/machines/ElectricMachineTileEntity.java @@ -221,7 +221,7 @@ protected ItemStack getSmeltingResult(ItemStack input) { SingleRecipeInput inventory = new SingleRecipeInput(input); if (!(getLevel() instanceof net.minecraft.server.level.ServerLevel server)) return ItemStack.EMPTY; return server.recipeAccess().getRecipeFor(RecipeType.SMELTING, inventory, getLevel()) - .map(recipe -> recipe.value().assemble(inventory, getLevel().registryAccess())).orElse(ItemStack.EMPTY); + .map(recipe -> recipe.value().assemble(inventory)).orElse(ItemStack.EMPTY); } @Override diff --git a/src/main/java/zone/moddev/mc/baseelectric/machines/GrowthChamberTileEntity.java b/src/main/java/zone/moddev/mc/baseelectric/machines/GrowthChamberTileEntity.java index 26fbf92..f581344 100644 --- a/src/main/java/zone/moddev/mc/baseelectric/machines/GrowthChamberTileEntity.java +++ b/src/main/java/zone/moddev/mc/baseelectric/machines/GrowthChamberTileEntity.java @@ -75,9 +75,9 @@ public void tickUpdate(boolean isServer) { getLevel().addFreshEntity( new ItemEntity( getLevel(), - 2*(getLevel().random.nextDouble()-0.5), - 2*(getLevel().random.nextDouble()-0.5), - 2*(getLevel().random.nextDouble()-0.5), + 2*(getLevel().getRandom().nextDouble()-0.5), + 2*(getLevel().getRandom().nextDouble()-0.5), + 2*(getLevel().getRandom().nextDouble()-0.5), remainder ) ); diff --git a/src/main/java/zone/moddev/mc/baseelectric/machines/PhotovoltaicGeneratorTileEntity.java b/src/main/java/zone/moddev/mc/baseelectric/machines/PhotovoltaicGeneratorTileEntity.java index 179b5b9..69ebf45 100644 --- a/src/main/java/zone/moddev/mc/baseelectric/machines/PhotovoltaicGeneratorTileEntity.java +++ b/src/main/java/zone/moddev/mc/baseelectric/machines/PhotovoltaicGeneratorTileEntity.java @@ -97,7 +97,7 @@ public static float getLightIntensityAt(BlockPos pos, Level w){ float light; if(w.canSeeSky(lightPos) && w.dimensionType().hasSkyLight()){ // outdoor light imposes no penalty - light = Mth.sin((float)w.getDayTime() * 2.61799E-4f); // (worldTime * pi / 12000); + light = Mth.sin((float)w.getOverworldClockTime() * 2.61799E-4f); // (worldTime * pi / 12000); // handle weather if(w.isThundering()){ light *= 0.2f; diff --git a/src/main/java/zone/moddev/mc/baseelectric/machines/SteamPoweredElectricGeneratorTileEntity.java b/src/main/java/zone/moddev/mc/baseelectric/machines/SteamPoweredElectricGeneratorTileEntity.java index 49c9a9d..8751a8f 100644 --- a/src/main/java/zone/moddev/mc/baseelectric/machines/SteamPoweredElectricGeneratorTileEntity.java +++ b/src/main/java/zone/moddev/mc/baseelectric/machines/SteamPoweredElectricGeneratorTileEntity.java @@ -46,7 +46,7 @@ public SteamPoweredElectricGeneratorTileEntity(net.minecraft.core.BlockPos pos, } super.powerUpdate(); super.setActive(flag); - if(flag && getLevel().random.nextInt(200) == 0){ + if(flag && getLevel().getRandom().nextInt(200) == 0){ playSoundEffect(getBlockPos().getX()+0.5, getBlockPos().getY()+0.5, getBlockPos().getZ()+0.5, SoundEvents.FIRE_EXTINGUISH, 0.25f, 1f); } } diff --git a/src/main/java/zone/moddev/mc/baseelectric/util/farming/VirtualCrop.java b/src/main/java/zone/moddev/mc/baseelectric/util/farming/VirtualCrop.java index aabea91..e4f9da7 100644 --- a/src/main/java/zone/moddev/mc/baseelectric/util/farming/VirtualCrop.java +++ b/src/main/java/zone/moddev/mc/baseelectric/util/farming/VirtualCrop.java @@ -56,7 +56,7 @@ public static VirtualCrop createVirtualCrop(ItemStack stack, Level w, BlockPos p && !(b instanceof net.minecraft.world.level.block.CropBlock) && !(b instanceof StemBlock)) || b instanceof net.minecraft.world.level.block.GrassBlock || b instanceof net.minecraft.world.level.block.VineBlock - || b instanceof net.minecraft.world.level.block.WaterlilyBlock){ + || b instanceof net.minecraft.world.level.block.LilyPadBlock){ // duplicate ItemStack product = seed.copy(); product.setCount(2); @@ -84,7 +84,7 @@ public static VirtualCrop createVirtualCrop(ItemStack stack, Level w, BlockPos p private static Block resolveStemFruit(StemBlock stem, Level world) { - // Forge 61 exposes the registered fruit key through the native stem codec. + // The native stem codec exposes the registered fruit key. var ops = world.registryAccess().createSerializationContext(JsonOps.INSTANCE); return StemBlock.CODEC.codec().encodeStart(ops, stem).result() .flatMap(data -> ResourceKey.codec(Registries.BLOCK) diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index 427e222..4020e36 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -31,13 +31,13 @@ side="BOTH" [[dependencies.baseelectric]] modId="basepower" mandatory=true -versionRange="[2.4.15.121111,3.0.0)" +versionRange="[2.4.16.2601021,3.0.0)" ordering="AFTER" side="BOTH" [[dependencies.baseelectric]] modId="basemetals" mandatory=true -versionRange="[3.1.2.121111,4.0.0)" +versionRange="[3.1.2.2601021,4.0.0)" ordering="AFTER" side="BOTH" diff --git a/src/main/resources/assets/baseelectric/items/battery_base.json b/src/main/resources/assets/baseelectric/items/battery_base.json deleted file mode 100644 index 01738b1..0000000 --- a/src/main/resources/assets/baseelectric/items/battery_base.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "model": { - "type": "minecraft:model", - "model": "baseelectric:item/battery_base" - } -} diff --git a/src/main/resources/assets/minecraft/atlases/blocks.json b/src/main/resources/assets/minecraft/atlases/blocks.json index d413b81..70bd027 100644 --- a/src/main/resources/assets/minecraft/atlases/blocks.json +++ b/src/main/resources/assets/minecraft/atlases/blocks.json @@ -1,5 +1,200 @@ -{ - "sources": [ - {"type":"single","resource":"baseelectric:entity/laser_turret"} - ] -} +{ + "sources": [ + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_battery_array" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_conduit" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_crusher_off" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_crusher_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_crusher_powered" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_drill_off" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_drill_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_fabricator_off" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_fabricator_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_fabricator_powered" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_furnace_off" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_furnace_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_furnace_powered" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_oven_off" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_oven_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_oven_powered" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_pcb_printer_off" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_pcb_printer_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_pcb_printer_powered" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_pump_off" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_pump_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_still_off" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_still_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_still_powered" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_switch_off" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/electric_switch_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/growth_chamber_controller" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/growth_chamber_off" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/growth_chamber_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/growth_chamber_powered" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/hydroelectric_generator_off" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/hydroelectric_generator_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/laser_drill_optic" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/laser_drill_optic_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/laser_turret_base" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/led_bar_off" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/led_bar_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/lithium_ore" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/machine_back" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/machine_side" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/machine_top" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/photovoltaic_cell" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/plastic_refinery_off" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/plastic_refinery_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/steam_powered_generator_off" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/steam_powered_generator_on" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:blocks/sulfur_ore" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:entity/laser_turret" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:materials/machine_case" + } + ] +} diff --git a/src/main/resources/assets/minecraft/atlases/items.json b/src/main/resources/assets/minecraft/atlases/items.json new file mode 100644 index 0000000..c1703fc --- /dev/null +++ b/src/main/resources/assets/minecraft/atlases/items.json @@ -0,0 +1,72 @@ +{ + "sources": [ + { + "type": "minecraft:single", + "resource": "baseelectric:items/alkaline_battery" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/blank_circuit_board" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/control_circuit" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/integrated_circuit" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/lead_acid_battery" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/li_ingot" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/li_nugget" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/li_powder" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/lithium_battery" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/nickel_hydride_battery" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/petrolplastic_ingot" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/psu" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/silicon_ingot" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/silicon_mix" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/solder" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/solder_mix" + }, + { + "type": "minecraft:single", + "resource": "baseelectric:items/sulfur_powder" + } + ] +} diff --git a/src/main/resources/data/baseelectric/villager_trade/librarian/blank_circuit_board.json b/src/main/resources/data/baseelectric/villager_trade/librarian/blank_circuit_board.json new file mode 100644 index 0000000..4de72dd --- /dev/null +++ b/src/main/resources/data/baseelectric/villager_trade/librarian/blank_circuit_board.json @@ -0,0 +1,22 @@ +{ + "wants": { + "id": "minecraft:emerald", + "count": 1 + }, + "gives": { + "id": "baseelectric:blank_circuit_board" + }, + "max_uses": 7, + "xp": 1, + "reputation_discount": 0, + "given_item_modifiers": [ + { + "function": "minecraft:set_count", + "count": { + "type": "minecraft:uniform", + "min": 3, + "max": 6 + } + } + ] +} diff --git a/src/main/resources/data/baseelectric/villager_trade/librarian/electric_conduit.json b/src/main/resources/data/baseelectric/villager_trade/librarian/electric_conduit.json new file mode 100644 index 0000000..16177c8 --- /dev/null +++ b/src/main/resources/data/baseelectric/villager_trade/librarian/electric_conduit.json @@ -0,0 +1,22 @@ +{ + "wants": { + "id": "minecraft:emerald", + "count": 1 + }, + "gives": { + "id": "baseelectric:electric_conduit" + }, + "max_uses": 7, + "xp": 1, + "reputation_discount": 0, + "given_item_modifiers": [ + { + "function": "minecraft:set_count", + "count": { + "type": "minecraft:uniform", + "min": 3, + "max": 6 + } + } + ] +} diff --git a/src/main/resources/data/baseelectric/villager_trade/librarian/integrated_circuit.json b/src/main/resources/data/baseelectric/villager_trade/librarian/integrated_circuit.json new file mode 100644 index 0000000..60be326 --- /dev/null +++ b/src/main/resources/data/baseelectric/villager_trade/librarian/integrated_circuit.json @@ -0,0 +1,22 @@ +{ + "wants": { + "id": "minecraft:emerald", + "count": 1 + }, + "gives": { + "id": "baseelectric:integrated_circuit" + }, + "max_uses": 7, + "xp": 1, + "reputation_discount": 0, + "given_item_modifiers": [ + { + "function": "minecraft:set_count", + "count": { + "type": "minecraft:uniform", + "min": 3, + "max": 6 + } + } + ] +} diff --git a/src/main/resources/data/baseelectric/villager_trade/librarian/petrolplastic_ingot.json b/src/main/resources/data/baseelectric/villager_trade/librarian/petrolplastic_ingot.json new file mode 100644 index 0000000..0e4f4b6 --- /dev/null +++ b/src/main/resources/data/baseelectric/villager_trade/librarian/petrolplastic_ingot.json @@ -0,0 +1,22 @@ +{ + "wants": { + "id": "minecraft:emerald", + "count": 1 + }, + "gives": { + "id": "baseelectric:petrolplastic_ingot" + }, + "max_uses": 7, + "xp": 1, + "reputation_discount": 0, + "given_item_modifiers": [ + { + "function": "minecraft:set_count", + "count": { + "type": "minecraft:uniform", + "min": 4, + "max": 8 + } + } + ] +} diff --git a/src/main/resources/data/baseelectric/villager_trade/librarian/psu.json b/src/main/resources/data/baseelectric/villager_trade/librarian/psu.json new file mode 100644 index 0000000..640910f --- /dev/null +++ b/src/main/resources/data/baseelectric/villager_trade/librarian/psu.json @@ -0,0 +1,16 @@ +{ + "wants": { + "id": "minecraft:emerald", + "count": { + "type": "minecraft:uniform", + "min": 1, + "max": 3 + } + }, + "gives": { + "id": "baseelectric:psu" + }, + "max_uses": 7, + "xp": 1, + "reputation_discount": 0 +} diff --git a/src/main/resources/data/baseelectric/villager_trade/librarian/solder.json b/src/main/resources/data/baseelectric/villager_trade/librarian/solder.json new file mode 100644 index 0000000..b0b9d9b --- /dev/null +++ b/src/main/resources/data/baseelectric/villager_trade/librarian/solder.json @@ -0,0 +1,22 @@ +{ + "wants": { + "id": "minecraft:emerald", + "count": 1 + }, + "gives": { + "id": "baseelectric:solder" + }, + "max_uses": 7, + "xp": 1, + "reputation_discount": 0, + "given_item_modifiers": [ + { + "function": "minecraft:set_count", + "count": { + "type": "minecraft:uniform", + "min": 5, + "max": 10 + } + } + ] +} diff --git a/src/main/resources/data/minecraft/tags/villager_trade/librarian/level_1.json b/src/main/resources/data/minecraft/tags/villager_trade/librarian/level_1.json new file mode 100644 index 0000000..aa7c4f6 --- /dev/null +++ b/src/main/resources/data/minecraft/tags/villager_trade/librarian/level_1.json @@ -0,0 +1,9 @@ +{ + "replace": false, + "values": [ + "baseelectric:librarian/electric_conduit", + "baseelectric:librarian/solder", + "baseelectric:librarian/integrated_circuit", + "baseelectric:librarian/blank_circuit_board" + ] +} diff --git a/src/main/resources/data/minecraft/tags/villager_trade/librarian/level_2.json b/src/main/resources/data/minecraft/tags/villager_trade/librarian/level_2.json new file mode 100644 index 0000000..76aa789 --- /dev/null +++ b/src/main/resources/data/minecraft/tags/villager_trade/librarian/level_2.json @@ -0,0 +1,7 @@ +{ + "replace": false, + "values": [ + "baseelectric:librarian/petrolplastic_ingot", + "baseelectric:librarian/psu" + ] +} diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index e1986b3..0140236 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,7 +1,7 @@ { "pack":{ - "min_format":[94,1], - "max_format":[94,1], + "min_format":[101,1], + "max_format":[101,1], "description":"baseElectric" } } diff --git a/src/test/java/zone/moddev/mc/baseelectric/entities/ElectricOperationalHydroturbineAnchorTest.java b/src/test/java/zone/moddev/mc/baseelectric/entities/ElectricOperationalHydroturbineAnchorTest.java index da1aded..a3f75a9 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/entities/ElectricOperationalHydroturbineAnchorTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/entities/ElectricOperationalHydroturbineAnchorTest.java @@ -103,7 +103,7 @@ private static Level world() throws Exception { field.setAccessible(true); Level world = (Level)allocator.getMethod("allocateInstance", Class.class) .invoke(field.get(null), NoTickWorld.class); - Field random = Level.class.getField("random"); + Field random = Level.class.getDeclaredField("random"); random.setAccessible(true); random.set(world, RandomSource.create(0)); return world; @@ -111,7 +111,7 @@ private static Level world() throws Exception { private static final class NoTickWorld extends ServerLevel { private NoTickWorld() { - super(null, null, null, null, null, null, false, 0L, Collections.emptyList(), false, null); + super(null, null, null, null, null, null, false, 0L, Collections.emptyList(), false); } } diff --git a/src/test/java/zone/moddev/mc/baseelectric/init/OreSpawnWorldGenTest.java b/src/test/java/zone/moddev/mc/baseelectric/init/OreSpawnWorldGenTest.java index ab029c8..341914b 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/init/OreSpawnWorldGenTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/init/OreSpawnWorldGenTest.java @@ -1,9 +1,18 @@ package zone.moddev.mc.baseelectric.init; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import com.google.gson.JsonObject; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; import org.junit.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; public class OreSpawnWorldGenTest { @Test public void electricOwnsBothMaterialsWhenNoPreferredProviderIsLoaded() { @@ -22,6 +31,71 @@ public class OreSpawnWorldGenTest { assertDefaults(true, true, false, false); } + @Test public void constructorRegistersMigrationBeforeLifecycleAndFailsClosed() throws Exception { + ClassNode entry = bytecode("zone/moddev/mc/baseelectric/ElectricAdvantage"); + List constructor = calls(method(entry, "")); + assertTrue(constructor.contains("migrateLegacyOreSpawnConfiguration") && constructor.contains("registerConfig")); + assertTrue(constructor.indexOf("migrateLegacyOreSpawnConfiguration") + < constructor.indexOf("registerConfig")); + MethodNode migration = method(entry, "migrateLegacyOreSpawnConfiguration"); + List migrationCalls = calls(migration); + assertTrue(migrationCalls.contains("migrate")); + assertEquals(1, migrationCalls.stream().filter("registerMigrationDefaults"::equals).count()); + assertTrue(migrationCalls.indexOf("migrate") < migrationCalls.indexOf("registerMigrationDefaults")); + assertTrue(migration.tryCatchBlocks.stream().anyMatch(block -> "java/io/IOException".equals(block.type))); + assertTrue(java.util.stream.StreamSupport.stream(migration.instructions.spliterator(), false) + .anyMatch(instruction -> instruction.getOpcode() == Opcodes.ATHROW)); + assertTrue(!migrationCalls.contains("error")); + + MethodNode registration = method(bytecode("zone/moddev/mc/baseelectric/init/OreSpawnWorldGen"), + "registerMigrationDefaults"); + assertTrue((registration.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC)) + == (Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC)); + assertTrue(registration.exceptions.contains("java/io/IOException")); + List registrationCalls = calls(registration); + assertEquals(1, registrationCalls.stream().filter("registerProvider"::equals).count()); + assertEquals(2, registrationCalls.stream().filter("isLoaded"::equals).count()); + assertTrue(registrationCalls.contains("createProvider") && registrationCalls.contains("toJson")); + assertTrue(!registrationCalls.contains("enqueue")); + } + + @Test public void migrationDefaultsKeepOwnedNativeRangesAndFallbackHierarchy() { + JsonObject root = OreSpawnWorldGen.createProvider(false, false).toJson(); + assertEquals("baseelectric", root.get("provider_modid").getAsString()); + for (String material : new String[] {"lithium", "sulfur"}) { + JsonObject rule = root.getAsJsonObject("ores").getAsJsonObject("baseelectric:ore/" + material); + assertEquals("baseelectric:" + (material.equals("lithium") ? "li_ore" : "sulfur_ore"), + rule.get("block").getAsString()); + JsonObject dimension = rule.getAsJsonObject("dimensions").getAsJsonObject("minecraft:overworld"); + assertEquals(1, dimension.get("min_y").getAsInt()); + assertEquals(31, dimension.get("max_y").getAsInt()); + assertEquals(material.equals("lithium") ? 0.125 : 1.0, dimension.get("frequency").getAsDouble(), 0.0); + assertEquals(material.equals("lithium") ? 6 : 8, dimension.get("min_quantity").getAsInt()); + assertEquals(material.equals("lithium") ? 9 : 23, dimension.get("max_quantity").getAsInt()); + } + } + + private static ClassNode bytecode(String name) throws Exception { + try (InputStream input = OreSpawnWorldGenTest.class.getResourceAsStream("/" + name + ".class")) { + assertTrue("Missing actual compiled production class", input != null); + ClassNode result = new ClassNode(); + new ClassReader(input).accept(result, 0); + return result; + } + } + + private static MethodNode method(ClassNode type, String name) { + return type.methods.stream().filter(method -> method.name.equals(name)).findFirst().orElseThrow(); + } + + private static List calls(MethodNode method) { + List names = new ArrayList<>(); + for (var instruction : method.instructions) { + if (instruction instanceof MethodInsnNode call) names.add(call.name); + } + return names; + } + private static void assertDefaults(boolean mineralogy, boolean baseMinerals, boolean sulfurEnabled, boolean lithiumEnabled) { JsonObject root = OreSpawnWorldGen.createProvider(mineralogy, baseMinerals).toJson(); diff --git a/src/test/java/zone/moddev/mc/baseelectric/init/VillagesTest.java b/src/test/java/zone/moddev/mc/baseelectric/init/VillagesTest.java index f0ed429..90baf88 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/init/VillagesTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/init/VillagesTest.java @@ -1,51 +1,109 @@ package zone.moddev.mc.baseelectric.init; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; +import net.minecraft.core.HolderGetter; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.Identifier; import net.minecraft.util.RandomSource; -import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; import net.minecraft.world.item.trading.MerchantOffer; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.NbtOps; -import zone.moddev.mc.baseelectric.verification.NativeTestBootstrap; +import net.minecraft.world.item.trading.VillagerTrade; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.world.level.storage.loot.LootContext; +import net.minecraft.world.level.storage.loot.LootParams; import org.junit.BeforeClass; import org.junit.Test; +import zone.moddev.mc.baseelectric.verification.NativeTestBootstrap; import static org.junit.Assert.*; public class VillagesTest { + private static final Path TRADES = Path.of("src/main/resources/data/baseelectric/villager_trade/librarian"); + private static Map GOODS; + private static HolderLookup.Provider registries; + @BeforeClass public static void bootstrapVanillaItems() { NativeTestBootstrap.initialize(); + // Native Item.CODEC resolves the built-in registry, not a private RegistryOps substitute. + GOODS = Stream.of("electric_conduit", "solder", "integrated_circuit", "blank_circuit_board", "petrolplastic_ingot", "psu") + .collect(java.util.stream.Collectors.toMap(id -> id, id -> { + var key = ResourceKey.create(Registries.ITEM, Identifier.parse("baseelectric:" + id)); + Item item = new Item(new Item.Properties().setId(key)); + net.minecraftforge.registries.ForgeRegistries.ITEMS.register(key.identifier(), item); + return item; + })); + NativeTestBootstrap.bindNativeComponents(); + registries = NativeTestBootstrap.registries(); + } + + @Test public void nativeNegativePricesKeepBulkGoodsForOneEmerald() throws Exception { + for (var bounds : List.of(new Object[] {"electric_conduit", 3, 6}, new Object[] {"solder", 5, 10}, + new Object[] {"integrated_circuit", 3, 6}, new Object[] {"blank_circuit_board", 3, 6}, + new Object[] {"petrolplastic_ingot", 4, 8})) { + assertOffer((String)bounds[0], false, 1, (int)bounds[1]); + assertOffer((String)bounds[0], true, 1, (int)bounds[2]); + } } - @Test public void nativeNegativePricesKeepBulkGoodsForOneEmerald() { - assertOffer(-8, -4, false, 1, 8); - assertOffer(-8, -4, true, 1, 4); - assertOffer(-6, -3, false, 1, 6); - assertOffer(-6, -3, true, 1, 3); - assertOffer(-10, -5, false, 1, 10); - assertOffer(-10, -5, true, 1, 5); + @Test public void nativePositivePricesKeepOneGoodForOneToThreeEmeralds() throws Exception { + assertOffer("psu", false, 1, 1); + assertOffer("psu", true, 3, 1); } - @Test public void nativePositivePricesKeepOneGoodForOneToThreeEmeralds() { - assertOffer(1, 3, false, 1, 1); - assertOffer(1, 3, true, 3, 1); + @Test public void allSixOffersExtendOnlyTheirHistoricalLibrarianTiers() throws Exception { + try (var files = Files.list(TRADES)) { + assertEquals(GOODS.keySet(), files.map(file -> file.getFileName().toString().replace(".json", "")) + .collect(java.util.stream.Collectors.toSet())); + } + for (int level : List.of(1, 2)) { + JsonObject tag = JsonParser.parseString(Files.readString(Path.of( + "src/main/resources/data/minecraft/tags/villager_trade/librarian/level_" + level + ".json"))) + .getAsJsonObject(); + assertFalse("Vanilla offers must remain available", tag.get("replace").getAsBoolean()); + Set expected = level == 1 ? Set.of("electric_conduit", "solder", "integrated_circuit", "blank_circuit_board") + : Set.of("petrolplastic_ingot", "psu"); + assertEquals(expected.stream().map(id -> "baseelectric:librarian/" + id).collect(java.util.stream.Collectors.toSet()), + tag.getAsJsonArray("values").asList().stream().map(value -> value.getAsString()) + .collect(java.util.stream.Collectors.toSet())); + assertEquals(expected.size(), tag.getAsJsonArray("values").size()); + } } - private static void assertOffer(int minimum, int maximum, boolean upper, int emeralds, int goods) { + private static void assertOffer(String id, boolean upper, int emeralds, int goods) throws Exception { + var json = JsonParser.parseString(Files.readString(TRADES.resolve(id + ".json"))); + var ops = registries.createSerializationContext(JsonOps.INSTANCE); + VillagerTrade trade = VillagerTrade.CODEC.parse(ops, json).getOrThrow(); + var roundTrip = VillagerTrade.CODEC.parse(ops, VillagerTrade.CODEC.encodeStart(ops, trade).getOrThrow()).getOrThrow(); RandomSource random = new LegacyRandomSource(0L) { @Override public int nextInt(int bound) { return upper ? bound - 1 : 0; } }; - MerchantOffer offer = Villages.sell(net.minecraft.world.item.Items.APPLE, minimum, maximum) - .getOffer(null, null, random); - CompoundTag nbt = (CompoundTag) MerchantOffer.CODEC.encodeStart( - NativeTestBootstrap.registries().createSerializationContext(NbtOps.INSTANCE), offer).getOrThrow(); - assertEquals("minecraft:emerald", nbt.getCompound("buy").orElseThrow().getString("id").orElseThrow()); - CompoundTag payment = nbt.getCompound("buy").orElseThrow(); - assertEquals(emeralds, payment.contains("count") ? payment.getInt("count").orElseThrow() : 1); - assertEquals("minecraft:apple", nbt.getCompound("sell").orElseThrow().getString("id").orElseThrow()); - CompoundTag sale = nbt.getCompound("sell").orElseThrow(); - assertEquals(goods, sale.contains("count") ? sale.getInt("count").orElseThrow() : 1); - assertEquals(7, nbt.getInt("maxUses").orElseThrow().intValue()); - assertEquals(0.0F, offer.getPriceMultiplier(), 0.0F); - // Native MerchantOffer.CODEC omits its zero price-multiplier default. - assertEquals(0.0F, nbt.getFloatOr("priceMultiplier", 0.0F), 0.0F); + // Constant/uniform providers need no world; use the native context constructor directly. + var constructor = LootContext.class.getDeclaredConstructor(LootParams.class, RandomSource.class, HolderGetter.Provider.class); + constructor.setAccessible(true); + LootContext context = constructor.newInstance(new LootParams(null, new ContextMap.Builder().create(), Map.of(), 0), + random, registries); + for (VillagerTrade definition : List.of(trade, roundTrip)) { + MerchantOffer offer = definition.getOffer(context); + assertNotNull(id, offer); + assertTrue(offer.getBaseCostA().is(Items.EMERALD)); + assertEquals(id, emeralds, offer.getBaseCostA().getCount()); + assertTrue(id, offer.getResult().is(GOODS.get(id))); + assertEquals(id, goods, offer.getResult().getCount()); + assertTrue(offer.getCostB().isEmpty()); + assertEquals(7, offer.getMaxUses()); + assertEquals(1, offer.getXp()); + assertEquals(0.0F, offer.getPriceMultiplier(), 0.0F); + } } } diff --git a/src/test/java/zone/moddev/mc/baseelectric/items/BatteryComponentTest.java b/src/test/java/zone/moddev/mc/baseelectric/items/BatteryComponentTest.java index e6dcf1b..ecc7dc2 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/items/BatteryComponentTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/items/BatteryComponentTest.java @@ -12,13 +12,21 @@ import static org.junit.Assert.*; public class BatteryComponentTest { - @BeforeClass public static void bootstrap() { NativeTestBootstrap.initialize(); } + private static BatteryItem testBattery; + + @BeforeClass public static void bootstrap() { + NativeTestBootstrap.initialize(); + var key = zone.moddev.mc.baseelectric.util.RegistryIds.item("lead_acid_battery"); + testBattery = new BatteryItem(100, new Item.Properties().setId(key)); + net.minecraftforge.registries.ForgeRegistries.ITEMS.register(key.identifier(), testBattery); + NativeTestBootstrap.bindNativeComponents(); + } @Test public void vanillaItemUpgradePreservesDepletedChargeAndUnrelatedData() { CompoundTag custom = LegacyBatteryDataTest.upgradeBattery() .getCompound("components").orElseThrow().getCompound("minecraft:custom_data").orElseThrow(); - BatteryItem battery = new BatteryItem(100, new Item.Properties().setId(zone.moddev.mc.baseelectric.util.RegistryIds.item("lead_acid_battery"))); + BatteryItem battery = testBattery; ItemStack stack = new ItemStack(battery); assertEquals(100, battery.getEnergy(stack), 0); stack.set(DataComponents.CUSTOM_DATA, CustomData.of(custom)); @@ -30,7 +38,7 @@ public class BatteryComponentTest { } @Test public void chargeMutationDoesNotLeakIntoAnItemCopy() { - BatteryItem battery = new BatteryItem(100, new Item.Properties().setId(zone.moddev.mc.baseelectric.util.RegistryIds.item("lead_acid_battery"))); + BatteryItem battery = testBattery; ItemStack original = new ItemStack(battery); battery.setEnergy(original, 25); ItemStack copy = original.copy(); diff --git a/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalNativeDefectsTest.java b/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalNativeDefectsTest.java index c6d114c..b2fdce3 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalNativeDefectsTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalNativeDefectsTest.java @@ -113,7 +113,7 @@ private static final class LightWorld extends ServerLevel { BlockPos surface; int brightness, skyReads, lightReads; private LightWorld() { - super(null, null, null, null, null, null, false, 0L, Collections.emptyList(), false, null); + super(null, null, null, null, null, null, false, 0L, Collections.emptyList(), false); } @Override public boolean canSeeSky(BlockPos pos) { assertEquals("Sky access must be sampled above the solid panel", surface, pos); @@ -131,7 +131,7 @@ private static final class BeamWorld extends ServerLevel { BlockPos turret; int traces; private BeamWorld() { - super(null, null, null, null, null, null, false, 0L, Collections.emptyList(), false, null); + super(null, null, null, null, null, null, false, 0L, Collections.emptyList(), false); } @Override public BlockHitResult clip(ClipContext context) { traces++; diff --git a/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalPumpPickupTest.java b/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalPumpPickupTest.java index 0694b35..d1a0e2d 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalPumpPickupTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalPumpPickupTest.java @@ -184,7 +184,7 @@ private static final class PumpWorld extends ServerLevel { Map blocks; int writes; private PumpWorld() { - super(null, null, null, null, null, null, false, 0L, Collections.emptyList(), false, null); + super(null, null, null, null, null, null, false, 0L, Collections.emptyList(), false); } @Override public boolean hasChunkAt(BlockPos pos) { return true; } @Override public BlockState getBlockState(BlockPos pos) { diff --git a/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalRedstoneCacheTest.java b/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalRedstoneCacheTest.java index 35ab6e6..61ddf1b 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalRedstoneCacheTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalRedstoneCacheTest.java @@ -208,7 +208,7 @@ private static final class SignalWorld extends ServerLevel { long time; List queryTimes; private SignalWorld() { - super(null, null, null, null, null, null, false, 0L, Collections.emptyList(), false, null); + super(null, null, null, null, null, null, false, 0L, Collections.emptyList(), false); } @Override public boolean hasNeighborSignal(BlockPos position) { assertEquals(this.position, position); diff --git a/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalSoundBroadcastTest.java b/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalSoundBroadcastTest.java index 9c5bc62..cc85788 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalSoundBroadcastTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/machines/ElectricOperationalSoundBroadcastTest.java @@ -112,7 +112,7 @@ private static final class QueryRejectingWorld extends ServerLevel { int playerListReads; private QueryRejectingWorld() { - super(null, null, null, null, null, null, false, 0L, Collections.emptyList(), false, null); + super(null, null, null, null, null, null, false, 0L, Collections.emptyList(), false); } @Override public List players() { diff --git a/src/test/java/zone/moddev/mc/baseelectric/machines/Native118ConstructionTest.java b/src/test/java/zone/moddev/mc/baseelectric/machines/Native118ConstructionTest.java index ccd543b..32712ec 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/machines/Native118ConstructionTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/machines/Native118ConstructionTest.java @@ -69,7 +69,7 @@ public class Native118ConstructionTest { private static final class PacketWorld extends ServerLevel { private PacketWorld() { - super(null, null, null, null, null, null, false, 0L, java.util.List.of(), false, null); + super(null, null, null, null, null, null, false, 0L, java.util.List.of(), false); } @Override public RegistryAccess registryAccess() { diff --git a/src/test/java/zone/moddev/mc/baseelectric/resources/BlockLootResourceTest.java b/src/test/java/zone/moddev/mc/baseelectric/resources/BlockLootResourceTest.java index 0b0c537..58e4fb2 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/resources/BlockLootResourceTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/resources/BlockLootResourceTest.java @@ -33,10 +33,10 @@ public class BlockLootResourceTest { assertFalse(Files.exists(root.resolve("pump_pipe_electric.json"))); } - @Test public void resourcePackDeclaresTheNativeOneTwentyOneFormat() throws Exception { + @Test public void resourcePackDeclaresTheNativeTwentySixFormat() throws Exception { JsonObject pack = read(Paths.get("src/main/resources/pack.mcmeta")).getAsJsonObject("pack"); - assertEquals(JsonParser.parseString("[94,1]"), pack.get("min_format")); - assertEquals(JsonParser.parseString("[94,1]"), pack.get("max_format")); + assertEquals(JsonParser.parseString("[101,1]"), pack.get("min_format")); + assertEquals(JsonParser.parseString("[101,1]"), pack.get("max_format")); assertFalse(pack.has("pack_format")); assertFalse(pack.has("supported_formats")); assertFalse(pack.has("forge:server_data_pack_format")); @@ -44,9 +44,9 @@ public class BlockLootResourceTest { .matcher(Files.readString(Path.of("src/main/resources/META-INF/mods.toml"))).find()); zone.moddev.mc.baseelectric.verification.NativeTestBootstrap.initialize(); var version = net.minecraft.SharedConstants.getCurrentVersion(); - assertEquals(new net.minecraft.server.packs.metadata.pack.PackFormat(75, 0), + assertEquals(new net.minecraft.server.packs.metadata.pack.PackFormat(84, 0), version.packVersion(net.minecraft.server.packs.PackType.CLIENT_RESOURCES)); - assertEquals(new net.minecraft.server.packs.metadata.pack.PackFormat(94, 1), + assertEquals(new net.minecraft.server.packs.metadata.pack.PackFormat(101, 1), version.packVersion(net.minecraft.server.packs.PackType.SERVER_DATA)); var proof = zone.moddev.mc.baseelectric.verification.NativePackResourceProof.read(); assertEquals(0, proof.getAsJsonArray("warningsAndErrors").size()); @@ -58,8 +58,8 @@ public class BlockLootResourceTest { assertTrue(type.toString(), range.isValueInRange(version.packVersion(type))); assertEquals("COMPATIBLE", side.get("currentCompatibility").getAsString()); assertTrue(side.get("resourcesVerified").getAsInt() > 0); - assertFalse(range.isValueInRange(new net.minecraft.server.packs.metadata.pack.PackFormat(74, 0))); - assertFalse(range.isValueInRange(new net.minecraft.server.packs.metadata.pack.PackFormat(95, 0))); + assertFalse(range.isValueInRange(new net.minecraft.server.packs.metadata.pack.PackFormat(83, 0))); + assertFalse(range.isValueInRange(new net.minecraft.server.packs.metadata.pack.PackFormat(102, 0))); var codec = net.minecraft.server.packs.metadata.pack.PackMetadataSection.forPackType(type).codec(); var decoded = codec.parse(com.mojang.serialization.JsonOps.INSTANCE, metadata).getOrThrow(); assertEquals(metadata, codec.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, decoded).getOrThrow()); @@ -71,14 +71,17 @@ public class BlockLootResourceTest { assertEquals("mod:baseelectric", server.get("id").getAsString()); assertEquals("net.minecraft.server.packs.PathPackResources", server.get("resourcesClass").getAsString()); assertEquals("COMPATIBLE", client.get("loaderCompatibility").getAsString()); - assertEquals("TOO_NEW", server.get("loaderCompatibility").getAsString()); + assertEquals("COMPATIBLE", server.get("loaderCompatibility").getAsString()); JsonObject insufficient = pack.deepCopy(); - insufficient.add("min_format", JsonParser.parseString("[75,0]")); + insufficient.add("min_format", JsonParser.parseString("[84,0]")); insufficient.remove("pack_format"); insufficient.remove("supported_formats"); - assertTrue(net.minecraft.server.packs.metadata.pack.PackFormat.packCodec( + var broad = net.minecraft.server.packs.metadata.pack.PackFormat.packCodec( net.minecraft.server.packs.PackType.SERVER_DATA).codec() - .parse(com.mojang.serialization.JsonOps.INSTANCE, insufficient).error().isPresent()); + .parse(com.mojang.serialization.JsonOps.INSTANCE, insufficient).getOrThrow(); + assertTrue("Native 26 accepts the broad range, so reject it in the exact root contract above", + broad.isValueInRange(version.packVersion(net.minecraft.server.packs.PackType.CLIENT_RESOURCES))); + assertTrue(broad.isValueInRange(version.packVersion(net.minecraft.server.packs.PackType.SERVER_DATA))); } @Test public void eachTrackStateUsesTheNativeCutoutRenderLayer() throws Exception { diff --git a/src/test/java/zone/moddev/mc/baseelectric/resources/BuildWorkflowContractTest.java b/src/test/java/zone/moddev/mc/baseelectric/resources/BuildWorkflowContractTest.java index 19da656..488db44 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/resources/BuildWorkflowContractTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/resources/BuildWorkflowContractTest.java @@ -7,15 +7,20 @@ import static org.junit.Assert.*; public class BuildWorkflowContractTest { - @Test public void hostedBuildsProvisionAllThreePinnedToolchainsBeforeGradle() throws Exception { + @Test public void hostedBuildsPinJava25ForEveryRoleBeforeGradle() throws Exception { for (String workflow : new String[] {"ci.yml", "codeql-analysis.yml"}) { String source = new String(Files.readAllBytes(Paths.get(".github/workflows", workflow)), StandardCharsets.UTF_8); int setup = source.indexOf("- name: Set up Gradle"); - for (String version : new String[] {"25.0.3+9.0.LTS", "8.0.502+7", "21.0.7+6.0.LTS"}) { + for (String version : new String[] {"25.0.3+9.0.LTS"}) { int pin = source.indexOf("java-version: '" + version + "'"); assertTrue(workflow + " missing pinned JDK " + version, pin >= 0 && pin < setup); } - assertTrue(source.contains("-Porg.gradle.java.installations.paths=$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64")); + assertTrue(source.contains("-Porg.gradle.java.installations.paths=$JAVA_HOME\n")); + assertEquals("One pinned JDK must serve all roles", 1, source.split("java-version:", -1).length - 1); + assertFalse(source.contains("JAVA_HOME_8_X64")); + assertFalse(source.contains("JAVA_HOME_21_X64")); + assertFalse(source.contains("8.0.502+7")); + assertFalse(source.contains("21.0.7+6")); assertTrue(source.contains("-Porg.gradle.java.installations.auto-detect=false")); assertTrue(source.contains("-Porg.gradle.java.installations.auto-download=false")); assertFalse(source.contains("-Dorg.gradle.java.installations.")); @@ -42,8 +47,8 @@ public class BuildWorkflowContractTest { } assertTrue(source.contains("ref: ${{ steps.public-pins.outputs.orespawn_ref }}")); assertTrue(source.contains("ref: ${{ steps.public-pins.outputs.power_ref }}")); - assertTrue(source.contains("branches: [master-1.21.11")); - assertTrue(source.contains("-Pforge61MavenizerJar=$MAVENIZER")); + assertTrue(source.contains("branches: [master-26.1.2")); + assertTrue(source.contains("-Pforge64MavenizerJar=$MAVENIZER")); assertTrue(source.contains("-PpowerAdvantageDeobfJar=$POWER_PLAYER")); assertFalse(source.contains("forge47")); if (workflow.equals("ci.yml")) { diff --git a/src/test/java/zone/moddev/mc/baseelectric/resources/ClientResourceContractTest.java b/src/test/java/zone/moddev/mc/baseelectric/resources/ClientResourceContractTest.java index 053fdfa..80c7711 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/resources/ClientResourceContractTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/resources/ClientResourceContractTest.java @@ -3,6 +3,9 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; +import net.minecraft.client.renderer.texture.atlas.SpriteSources; +import net.minecraft.client.renderer.texture.atlas.sources.SingleFile; import org.junit.Test; import java.io.Reader; import java.nio.charset.StandardCharsets; @@ -39,6 +42,35 @@ public class ClientResourceContractTest { } @Test public void parentsAndTexturesExistInActualPacks() throws Exception { + SpriteSources.bootstrap(); + var sprites = new java.util.HashSet(); + for (String atlas : new String[]{"blocks", "items"}) { + var decoded = SpriteSources.FILE_CODEC.parse(JsonOps.INSTANCE, + read(ROOT.resolve("assets/minecraft/atlases/" + atlas + ".json"))).getOrThrow(); + assertEquals(atlas, "blocks".equals(atlas) ? 49 : 17, decoded.size()); + for (var source : decoded) { + assertTrue("No global directories or order-dependent filters", source instanceof SingleFile); + var single = (SingleFile) source; + assertEquals("baseelectric", single.resourceId().getNamespace()); + assertTrue(single.spriteId().isEmpty()); + assertTrue("Duplicate sprite across native atlases", sprites.add(single.resourceId().toString())); + } + } + assertTrue(sprites.contains("baseelectric:entity/laser_turret")); + assertTrue(sprites.contains("baseelectric:materials/machine_case")); + assertFalse(sprites.contains("baseelectric:materials/laser_beam")); + assertTrue(SpriteSources.FILE_CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString( + "{\"sources\":[{\"type\":\"minecraft:single\",\"resource\":5}]}" )).error().isPresent()); + assertFalse("Abstract parent is not a standalone registered item", + Files.exists(ASSETS.resolve("items/battery_base.json"))); + assertTrue(Files.isRegularFile(ASSETS.resolve("models/item/battery_base.json"))); + for (String battery : new String[]{"alkaline_battery", "lead_acid_battery", "lithium_battery", "nickel_hydride_battery"}) { + JsonObject child = read(ASSETS.resolve("models/item/" + battery + ".json")); + assertEquals("baseelectric:item/battery_base", child.get("parent").getAsString()); + assertEquals("baseelectric:items/" + battery, child.getAsJsonObject("textures").get("item_texture").getAsString()); + assertEquals("baseelectric:item/" + battery, read(ASSETS.resolve("items/" + battery + ".json")) + .getAsJsonObject("model").get("model").getAsString()); + } try (Stream files = Files.walk(ASSETS.resolve("models"))) { for (Path file : (Iterable) files.filter(p -> p.toString().endsWith(".json"))::iterator) { JsonObject model = read(file); diff --git a/src/test/java/zone/moddev/mc/baseelectric/resources/NativePackRangeContractTest.java b/src/test/java/zone/moddev/mc/baseelectric/resources/NativePackRangeContractTest.java index 339ae89..414aeab 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/resources/NativePackRangeContractTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/resources/NativePackRangeContractTest.java @@ -18,6 +18,10 @@ public class NativePackRangeContractTest { Path evidence = Files.createTempDirectory(root, "piped-dumb-"); var builder = NativePackResourceProof.childProcess(evidence); assertEquals("dumb", builder.environment().get("TERM")); + assertTrue(builder.command().contains("-XX:ActiveProcessorCount=4")); + for (String name : java.util.List.of("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS")) { + assertFalse(builder.environment().containsKey(name)); + } assertEquals("terminal.jline", net.minecrell.terminalconsole.TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY); assertEquals(ProcessBuilder.Redirect.PIPE, builder.redirectInput()); assertEquals(ProcessBuilder.Redirect.PIPE, builder.redirectOutput()); @@ -44,6 +48,10 @@ public class NativePackRangeContractTest { property.isJsonNull() ? null : property.getAsString()); assertFalse(proof.get("terminalPresent").getAsBoolean()); assertTrue(proof.get("interactiveTerminalDisabled").getAsBoolean()); + assertTrue(proof.get("environmentJvmOptionsAbsent").getAsBoolean()); + assertEquals(4, proof.get("availableProcessors").getAsInt()); + assertTrue(proof.getAsJsonArray("jvmArguments").asList().stream() + .anyMatch(value -> value.getAsString().equals("-XX:ActiveProcessorCount=4"))); assertEquals(0, proof.getAsJsonArray("warningsAndErrors").size()); assertTrue(proof.getAsJsonObject("CLIENT_RESOURCES").get("resourcesVerified").getAsInt() > 0); assertTrue(proof.getAsJsonObject("SERVER_DATA").get("resourcesVerified").getAsInt() > 0); @@ -52,7 +60,7 @@ public class NativePackRangeContractTest { } } - @Test public void nonInteractiveModeDoesNotWhitelistAnyDiagnostic() { + @Test public void nonInteractiveModeDoesNotWhitelistAnyDiagnostic() throws Exception { Path log = Path.of("negative-control.log"); for (String diagnostic : new String[] { "[main/WARN] real resource warning", "[main/ERROR] real resource error", "[main/FATAL] real fatal", @@ -63,6 +71,24 @@ public class NativePackRangeContractTest { () -> NativePackResourceProof.requireNoDiagnostics(log, diagnostic)); } NativePackResourceProof.requireNoDiagnostics(log, "[main/INFO] valid resource receipt"); + Path root = Path.of("build/native-pack-resources").toAbsolutePath(); + Files.createDirectories(root); + for (String name : java.util.List.of("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS")) { + Path evidence = Files.createTempDirectory(root, "inherited-options-negative-"); + Path output = evidence.resolve("rejected.log"); + var command = NativePackResourceProof.childProcess(evidence); + command.environment().put(name, "-XX:ActiveProcessorCount=3"); + Process child = command.redirectOutput(output.toFile()).start(); + try { + assertTrue(child.waitFor(30, java.util.concurrent.TimeUnit.SECONDS)); + assertNotEquals(0, child.exitValue()); + assertTrue(Files.readString(output), Files.readString(output).contains( + "Inherited JVM environment options are forbidden in native proof: " + name)); + assertFalse("Rejected environment must not emit a success receipt", Files.exists(evidence.resolve("receipt.json"))); + } finally { + if (child.isAlive()) child.destroyForcibly().waitFor(); + } + } } @Test public void baseMetalsIsRequiredByNativeModInfoWithOptionalNegativeControl() throws Exception { @@ -72,7 +98,7 @@ public class NativePackRangeContractTest { assertEquals(java.util.Set.of("forge", "minecraft", "basepower", "basemetals"), dependencies.keySet()); var metals = dependencies.getAsJsonObject("basemetals"); assertTrue("Forge must require Base Metals on both sides", metals.get("mandatory").getAsBoolean()); - assertEquals("[3.1.2.121111,4.0.0)", metals.get("versionRange").getAsString()); + assertEquals("[3.1.2.2601021,4.0.0)", metals.get("versionRange").getAsString()); assertEquals("AFTER", metals.get("ordering").getAsString()); assertEquals("BOTH", metals.get("side").getAsString()); var control = proof.getAsJsonObject("optionalBaseMetalsControl"); @@ -91,11 +117,11 @@ 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(75, 0))); + range.isValueInRange(PackFormat.of(84, 0))); assertEquals(type.toString(), type == PackType.SERVER_DATA, - range.isValueInRange(PackFormat.of(94, 1))); - assertFalse(range.isValueInRange(PackFormat.of(74))); - assertFalse(range.isValueInRange(PackFormat.of(95))); + range.isValueInRange(PackFormat.of(101, 1))); + assertFalse(range.isValueInRange(PackFormat.of(83))); + assertFalse(range.isValueInRange(PackFormat.of(102))); } } @@ -117,11 +143,11 @@ public class NativePackRangeContractTest { } @Test public void dataOnlyRangeDoesNotQualifyClientResources() { - JsonObject dataOnly = JsonParser.parseString("{\"min_format\":[94,1],\"max_format\":[94,1]}") + JsonObject dataOnly = JsonParser.parseString("{\"min_format\":[101,1],\"max_format\":[101,1]}") .getAsJsonObject(); var range = PackFormat.packCodec(PackType.CLIENT_RESOURCES).codec() .parse(JsonOps.INSTANCE, dataOnly).getOrThrow(); - assertFalse(range.isValueInRange(PackFormat.of(75, 0))); - assertTrue(range.isValueInRange(PackFormat.of(94, 1))); + assertFalse(range.isValueInRange(PackFormat.of(84, 0))); + assertTrue(range.isValueInRange(PackFormat.of(101, 1))); } } diff --git a/src/test/java/zone/moddev/mc/baseelectric/resources/RecipeResourceTest.java b/src/test/java/zone/moddev/mc/baseelectric/resources/RecipeResourceTest.java index 985b1a9..99e5965 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/resources/RecipeResourceTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/resources/RecipeResourceTest.java @@ -32,7 +32,7 @@ public class RecipeResourceTest { String text = new String(Files.readAllBytes(file), StandardCharsets.UTF_8); JsonObject json = new JsonParser().parse(text).getAsJsonObject(); String type = json.get("type").getAsString(); - assertTrue(file + " must use the 1.21.11 result stack codec", json.getAsJsonObject("result").has("id")); + assertTrue(file + " must use the 26.1.2 result template codec", json.getAsJsonObject("result").has("id")); assertFalse(file + " still has pre-1.21.11 Forge conditions", json.has("conditions")); assertTrue(file + " has unsupported serializer " + type, "minecraft:crafting_shaped".equals(type) @@ -96,7 +96,7 @@ private static void assertNativeCodecShapes() throws ReflectiveOperationExceptio } for (String obsolete : List.of("{\"item\":\"minecraft:apple\"}", "{\"tag\":\"baseelectric:codec_test_choices\"}")) { - assertTrue("Forge 61 must reject old ingredient objects: " + obsolete, + assertTrue("Forge 64 must reject old ingredient objects: " + obsolete, net.minecraft.world.item.crafting.Ingredient.CODEC.parse(ops, JsonParser.parseString(obsolete)).error().isPresent()); } diff --git a/src/test/java/zone/moddev/mc/baseelectric/resources/ShaderResourceContractTest.java b/src/test/java/zone/moddev/mc/baseelectric/resources/ShaderResourceContractTest.java index bf61922..436fc83 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/resources/ShaderResourceContractTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/resources/ShaderResourceContractTest.java @@ -29,7 +29,7 @@ public class ShaderResourceContractTest { assertFalse(vertex.contains("IViewRotMat")); for (String renderer : Arrays.asList("HydroturbineRenderer", "LaserDrillRenderer", "LaserTurretRenderer")) { String source = Files.readString(PIPELINES.resolveSibling(renderer + ".java")); - assertFalse("Forge 61 warns about mod-owned class annotations: " + renderer, source.contains("@OnlyIn")); + assertFalse("Forge 64 warns about mod-owned class annotations: " + renderer, source.contains("@OnlyIn")); } String registration = Files.readString(PIPELINES.resolveSibling("ClientRegistration.java")); assertTrue(registration.contains("value = Dist.CLIENT")); @@ -38,7 +38,8 @@ public class ShaderResourceContractTest { @Test public void noDiffuseLightingDoesNotRemoveLightmapOrOverlaySampling() throws Exception { String vertex = vanilla("entity.vsh"); assertTrue(vertex.contains("vertexColor = Color;")); - assertTrue(vertex.contains("lightMapColor = texelFetch(Sampler2, UV2 / 16, 0);")); + assertTrue(vertex.contains("#moj_import ")); + assertTrue(vertex.contains("lightMapColor = sample_lightmap(Sampler2, UV2);")); assertTrue(vertex.contains("overlayColor = texelFetch(Sampler1, UV1, 0);")); assertTrue(vertex.contains("#elif defined(NO_CARDINAL_LIGHTING)\n vertexColor = Color;")); assertTrue(vertex.contains("#moj_import ")); diff --git a/src/test/java/zone/moddev/mc/baseelectric/util/crafting/RecipeDeconstructorTest.java b/src/test/java/zone/moddev/mc/baseelectric/util/crafting/RecipeDeconstructorTest.java index b08b049..391bfb2 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/util/crafting/RecipeDeconstructorTest.java +++ b/src/test/java/zone/moddev/mc/baseelectric/util/crafting/RecipeDeconstructorTest.java @@ -3,6 +3,9 @@ import java.util.List; import java.util.Map; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemStackTemplate; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.CraftingRecipe; import net.minecraft.world.item.Items; import net.minecraft.world.item.crafting.CraftingBookCategory; import net.minecraft.world.item.crafting.Ingredient; @@ -18,9 +21,10 @@ public class RecipeDeconstructorTest { @BeforeClass public static void bootstrap() { NativeTestBootstrap.initialize(); } @Test public void shapedPlansKeepRepeatedIngredientsAndResultCountWithoutEmptySlots() { - var recipe = new ShapedRecipe("native-plan", CraftingBookCategory.MISC, + var recipe = new ShapedRecipe(new Recipe.CommonInfo(true), + new CraftingRecipe.CraftingBookInfo(CraftingBookCategory.MISC, "native-plan"), ShapedRecipePattern.of(Map.of('A', Ingredient.of(Items.APPLE)), " A ", "AA "), - new ItemStack(Items.GOLDEN_APPLE, 3)); + new ItemStackTemplate(Items.GOLDEN_APPLE, 3)); var plans = RecipeDeconstructor.describe(recipe, NativeTestBootstrap.registries()); assertEquals(1, plans.size()); var plan = plans.getFirst(); @@ -34,8 +38,9 @@ public class RecipeDeconstructorTest { } @Test public void shapelessPlansKeepAlternativesAndDoNotInventFreeIngredients() { - var recipe = new ShapelessRecipe("native-plan", CraftingBookCategory.MISC, - new ItemStack(Items.BREAD, 2), List.of(Ingredient.of(Items.APPLE, Items.CARROT), Ingredient.of(Items.WHEAT))); + var recipe = new ShapelessRecipe(new Recipe.CommonInfo(true), + new CraftingRecipe.CraftingBookInfo(CraftingBookCategory.MISC, "native-plan"), + new ItemStackTemplate(Items.BREAD, 2), List.of(Ingredient.of(Items.APPLE, Items.CARROT), Ingredient.of(Items.WHEAT))); var plan = RecipeDeconstructor.describe(recipe, NativeTestBootstrap.registries()).getFirst(); assertEquals(2, plan.ingredients().size()); assertEquals(2, plan.result().getCount()); diff --git a/src/test/java/zone/moddev/mc/baseelectric/verification/NativePackResourceProof.java b/src/test/java/zone/moddev/mc/baseelectric/verification/NativePackResourceProof.java index 2b20da4..928bddf 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/verification/NativePackResourceProof.java +++ b/src/test/java/zone/moddev/mc/baseelectric/verification/NativePackResourceProof.java @@ -47,7 +47,7 @@ public static synchronized JsonObject read() throws Exception { if (receipt != null) return receipt.deepCopy(); Path evidenceRoot = Path.of("build/native-pack-resources").toAbsolutePath(); Files.createDirectories(evidenceRoot); - Path evidence = Files.createTempDirectory(evidenceRoot, "forge61-"); + Path evidence = Files.createTempDirectory(evidenceRoot, "forge64-"); Path log = evidence.resolve("forge-resources.log"); Process process = childProcess(evidence).redirectOutput(log.toFile()).start(); if (!process.waitFor(90, TimeUnit.SECONDS)) { @@ -75,6 +75,9 @@ public static ProcessBuilder childProcess(Path evidence) { evidence.toString()).directory(evidence.toFile()) .redirectErrorStream(true); process.environment().put("TERM", "dumb"); + for (String name : List.of("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS")) { + process.environment().remove(name); + } return process; } @@ -86,6 +89,16 @@ public static void requireNoDiagnostics(Path log, String output) { } public static void main(String[] args) throws Exception { + for (String name : List.of("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS")) { + if (System.getenv(name) != null) { + throw new IllegalStateException("Inherited JVM environment options are forbidden in native proof: " + name); + } + } + if (Runtime.getRuntime().availableProcessors() != 4 + || !java.lang.management.ManagementFactory.getRuntimeMXBean().getInputArguments() + .contains("-XX:ActiveProcessorCount=4")) { + throw new IllegalStateException("Native proof requires the explicit four-CPU JVM argument"); + } SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); Path resources = Path.of(args[0]); @@ -115,7 +128,7 @@ public static void main(String[] args) throws Exception { output.closeEntry(); } ModFile optionalMod = new ModFile(SecureJar.from(optionalJar), new ForgeDevLocator(), ModFileParser::modsTomlParser); - ModList.of(List.of(mod), mod.getModInfos().stream().map(ModInfo.class::cast).toList()); + initializeNativeModList(mod); List diagnostics = new ArrayList<>(); AbstractAppender appender = new AbstractAppender("ElectricPackContract", null, PatternLayout.createDefaultLayout(), false, Property.EMPTY_ARRAY) { @@ -127,6 +140,13 @@ public static void main(String[] args) throws Exception { var logger = (org.apache.logging.log4j.core.Logger)LogManager.getRootLogger(); logger.addAppender(appender); JsonObject result = new JsonObject(); + result.addProperty("environmentJvmOptionsAbsent", true); + result.addProperty("availableProcessors", Runtime.getRuntime().availableProcessors()); + result.addProperty("javaRuntimeVersion", System.getProperty("java.runtime.version")); + JsonArray arguments = new JsonArray(); + java.lang.management.ManagementFactory.getRuntimeMXBean().getInputArguments().forEach(arguments::add); + result.add("jvmArguments", arguments); + result.addProperty("classpath", System.getProperty("java.class.path")); result.addProperty("terminalJline", System.getProperty(net.minecrell.terminalconsole.TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY)); result.addProperty("terminalPresent", net.minecrell.terminalconsole.TerminalConsoleAppender.getTerminal() != null); result.addProperty("interactiveTerminalDisabled", "false".equals(System.getProperty( @@ -164,8 +184,8 @@ public static void main(String[] args) throws Exception { side.addProperty("resourcesClass", opened.getClass().getName()); side.addProperty("acceptsClient", formats.isValueInRange(SharedConstants.getCurrentVersion().packVersion(PackType.CLIENT_RESOURCES))); side.addProperty("acceptsServer", formats.isValueInRange(SharedConstants.getCurrentVersion().packVersion(PackType.SERVER_DATA))); - side.addProperty("accepts74", formats.isValueInRange(net.minecraft.server.packs.metadata.pack.PackFormat.of(74))); - side.addProperty("accepts95", formats.isValueInRange(net.minecraft.server.packs.metadata.pack.PackFormat.of(95))); + side.addProperty("accepts83", formats.isValueInRange(net.minecraft.server.packs.metadata.pack.PackFormat.of(83))); + side.addProperty("accepts102", formats.isValueInRange(net.minecraft.server.packs.metadata.pack.PackFormat.of(102))); side.add("nativeMetadata", encoded); if (type == PackType.CLIENT_RESOURCES) { if (opened.getChildren() == null || opened.getChildren().size() != 1) { @@ -213,6 +233,19 @@ public static void main(String[] args) throws Exception { System.out.println("BASEELECTRIC_NATIVE_PACK_RESOURCES_PASS " + result); } + private static void initializeNativeModList(ModFile mod) throws ReflectiveOperationException { + // Forge 64 freezes discovery before ModList's static initialization. No mod code runs in this child. + Class stateType = Class.forName("net.minecraftforge.fml.loading.ModSorter$State"); + var constructor = stateType.getDeclaredConstructor(List.class, List.class); + constructor.setAccessible(true); + Object state = constructor.newInstance(List.of(mod), mod.getModInfos().stream().map(ModInfo.class::cast).toList()); + Class loader = Class.forName("net.minecraftforge.fml.loading.LoadingModListImpl"); + var initialize = loader.getDeclaredMethod("init", stateType, net.minecraftforge.fml.loading.EarlyLoadingException.class); + initialize.setAccessible(true); + initialize.invoke(null, state, null); + ModList.init(); + } + private static JsonObject dependencies(ModFile mod) { ModInfo info = ModInfo.class.cast(mod.getModInfos().stream() .filter(entry -> entry.getModId().equals("baseelectric")).findFirst().orElseThrow()); diff --git a/src/test/java/zone/moddev/mc/baseelectric/verification/NativeTestBootstrap.java b/src/test/java/zone/moddev/mc/baseelectric/verification/NativeTestBootstrap.java index c3d4107..601b120 100644 --- a/src/test/java/zone/moddev/mc/baseelectric/verification/NativeTestBootstrap.java +++ b/src/test/java/zone/moddev/mc/baseelectric/verification/NativeTestBootstrap.java @@ -28,6 +28,7 @@ public static synchronized void initialize() { if (ready) return; SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); + bindNativeComponents(); // The plain test JVM has no FML registration phase after vanilla bootstrap. GameData.unfreezeData(); ready = true; @@ -37,16 +38,24 @@ public static RegistryAccess.Frozen registries() { return RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); } + public static void bindNativeComponents() { + var lookup = net.minecraft.data.registries.VanillaRegistries.createLookup(); + BuiltInRegistries.DATA_COMPONENT_INITIALIZERS.build(lookup).forEach( + net.minecraft.core.component.DataComponentInitializers.PendingComponents::apply); + } + public static net.minecraft.world.level.storage.ValueInput input(net.minecraft.nbt.CompoundTag data) { return net.minecraft.world.level.storage.TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING, registries(), data); } public static synchronized void registerVanillaFluidTypes() throws ReflectiveOperationException { + initialize(); registerNativeForgeRegistry(ForgeRegistries.FLUID_TYPES, "VANILLA_FLUID_TYPES"); } public static synchronized void registerVanillaIngredientSerializers() throws ReflectiveOperationException { + initialize(); registerNativeForgeRegistry(ForgeRegistries.INGREDIENT_SERIALIZERS, "INGREDIENT_SERIALIZERS"); } diff --git a/tools/verification/AuditedJUnit.java b/tools/verification/AuditedJUnit.java index fa80d4e..42511ed 100644 --- a/tools/verification/AuditedJUnit.java +++ b/tools/verification/AuditedJUnit.java @@ -30,7 +30,7 @@ public static void main(String[] args) throws Exception { XMLStreamWriter xml = XMLOutputFactory.newFactory().createXMLStreamWriter(output, "UTF-8"); xml.writeStartDocument("UTF-8", "1.0"); xml.writeStartElement("testsuite"); - xml.writeAttribute("name", "baseElectric-1.21.11-ECJ"); + xml.writeAttribute("name", "baseElectric-26.1.2-ECJ"); xml.writeAttribute("tests", Integer.toString(result.getRunCount())); xml.writeAttribute("failures", Integer.toString(result.getFailureCount())); xml.writeAttribute("ignored", Integer.toString(result.getIgnoreCount())); diff --git a/tools/verification/EclipseTestInventory.java b/tools/verification/EclipseTestInventory.java new file mode 100644 index 0000000..9f29a13 --- /dev/null +++ b/tools/verification/EclipseTestInventory.java @@ -0,0 +1,64 @@ +package zone.moddev.mc.baseelectric.verification; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; + +/** Compares fresh compiled JUnit methods with exact Gradle XML inventory, without loading test classes. */ +public final class EclipseTestInventory { + public static void main(String[] args) throws Exception { + if (args.length != 3) throw new IllegalArgumentException("classes, inventory file, expected test count"); + List lines = Files.readAllLines(Path.of(args[1]), StandardCharsets.UTF_8); + Set declared = new TreeSet<>(lines); + if (declared.isEmpty() || declared.contains("") || declared.size() != lines.size()) { + throw new AssertionError("Empty or duplicate Gradle test inventory"); + } + Set compiled = new TreeSet<>(); + Set suites = new TreeSet<>(); + try (var paths = Files.walk(Path.of(args[0]))) { + for (Path file : paths.filter(path -> path.toString().endsWith(".class")).sorted().toList()) { + ClassNode type = new ClassNode(); + new ClassReader(Files.readAllBytes(file)).accept(type, + ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + if (has(type.visibleAnnotations, "Lorg/junit/runner/RunWith;") + || "junit/framework/TestCase".equals(type.superName)) { + throw new AssertionError("Review custom JUnit discovery before qualification: " + type.name); + } + for (var method : type.methods) { + if (!has(method.visibleAnnotations, "Lorg/junit/Test;")) continue; + if (type.name.contains("$") || !"java/lang/Object".equals(type.superName) + || (type.access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_INTERFACE)) != 0 + || (method.access & Opcodes.ACC_PUBLIC) == 0 + || (method.access & Opcodes.ACC_STATIC) != 0 || !"()V".equals(method.desc) + || has(type.visibleAnnotations, "Lorg/junit/Ignore;") + || has(method.visibleAnnotations, "Lorg/junit/Ignore;")) { + throw new AssertionError("Review excluded, nested, inherited or nonstandard JUnit test: " + type.name + "#" + method.name); + } + String suite = type.name.replace('/', '.'); + suites.add(suite); + if (!compiled.add(suite + "#" + method.name)) throw new AssertionError("Duplicate compiled test"); + } + } + } + if (!compiled.equals(declared) || compiled.size() != Integer.parseInt(args[2])) { + Set omitted = new TreeSet<>(compiled); + omitted.removeAll(declared); + Set stale = new TreeSet<>(declared); + stale.removeAll(compiled); + throw new AssertionError("Incomplete or stale Gradle method inventory: omitted=" + omitted + + ", stale=" + stale + ", compiled tests=" + compiled.size() + ", declared tests=" + args[2]); + } + System.out.println("PASS: exact compiled JUnit inventory; " + suites.size() + " suites, " + compiled.size() + " methods"); + } + + private static boolean has(List annotations, String descriptor) { + return annotations != null && annotations.stream().anyMatch(annotation -> descriptor.equals(annotation.desc)); + } +} diff --git a/tools/verification/Invoke-ElectricPackagedSmoke.ps1 b/tools/verification/Invoke-ElectricPackagedSmoke.ps1 index 11ec3cf..f15f3d7 100644 --- a/tools/verification/Invoke-ElectricPackagedSmoke.ps1 +++ b/tools/verification/Invoke-ElectricPackagedSmoke.ps1 @@ -30,7 +30,7 @@ $target = "$($pins.minecraft_version)-$($pins.forge_version)" $forgeDir = "libraries/net/minecraftforge/forge/$target" $winArgs = "$forgeDir/win_args.txt" $receipt = [ordered]@{ - status = 'FAIL'; scope = 'Packaged Forge 1.21.11 dedicated server and legacy inventory probe; no client or whole-world upgrade proof' + status = 'FAIL'; scope = 'Packaged Forge 26.1.2 dedicated server and legacy inventory probe; no client or whole-world upgrade proof' baseMetalsIncluded = !$WithoutBaseMetals seed = $seed; java = $javaPath; inputs = @(); checks = [ordered]@{}; exitCode = $null; error = $null } @@ -63,31 +63,31 @@ function Send-Command([string]$Command) { } try { - if ($target -cne '1.21.11-61.1.0' -or $pins.mod_version -cne '2.2.16.121111') { - throw 'This smoke runner is bound to baseElectric 2.2.16.121111 on Forge 61.1.0.' + if ($target -cne '26.1.2-64.1.5' -or $pins.mod_version -cne '2.2.17.2601021') { + throw 'This smoke runner is bound to baseElectric 2.2.17.2601021 on Forge 64.1.5.' } if ($pins.poweradvantage_pin_state -notin @('LOCAL_SOURCE', 'MERGED_SOURCE') -or $PowerSha256 -ne $pins.poweradvantage_runtime_sha256) { throw 'Packaged smoke requires the exact source-pinned basePower player JAR.' } $receipt.powerSourceRef = $pins.poweradvantage_source_ref $javaRelease = Join-Path (Split-Path (Split-Path $javaPath)) 'release' - if ((Get-Content -LiteralPath $javaRelease -Raw) -notmatch 'JAVA_VERSION="21\.') { - throw 'The dedicated-server smoke requires Java 21.' + if ((Get-Content -LiteralPath $javaRelease -Raw) -notmatch 'JAVA_VERSION="25\.') { + throw 'The dedicated-server smoke requires Java 25.' } $receipt.javaRelease = (Get-FileHash -LiteralPath $javaRelease).Hash $receipt.javaSha256 = (Get-FileHash -LiteralPath $javaPath).Hash $argsText = Get-Content -LiteralPath (Join-Path $seed $winArgs) -Raw $shim = "forge-$target-shim.jar" - if (!$argsText.Contains("-jar $shim")) { throw 'Unexpected Forge 61 server argument file.' } + if (!$argsText.Contains("-jar $shim")) { throw 'Unexpected Forge 64 server argument file.' } $receipt.shimSha256 = (Get-FileHash -LiteralPath (Join-Path $seed $shim)).Hash - if ($receipt.shimSha256 -cne $pins.forge_shim_sha256) { throw 'Forge shim differs from the recorded 61.1.0 runtime pin.' } + if ($receipt.shimSha256 -cne $pins.forge_shim_sha256) { throw 'Forge shim differs from the recorded 64.1.5 runtime pin.' } $receipt.seedArgumentsSha256 = (Get-FileHash -LiteralPath (Join-Path $seed $winArgs)).Hash $forgeUniversal = Join-Path $seed "$forgeDir/forge-$target-universal.jar" $receipt.forgeUniversalSha256 = (Get-FileHash -LiteralPath $forgeUniversal).Hash - if ($receipt.forgeUniversalSha256 -cne $pins.forge_universal_sha256) { throw 'Forge universal differs from the recorded 61.1.0 runtime pin.' } + if ($receipt.forgeUniversalSha256 -cne $pins.forge_universal_sha256) { throw 'Forge universal differs from the recorded 64.1.5 runtime pin.' } $forgeServer = Join-Path $seed "$forgeDir/forge-$target-server.jar" $receipt.forgeServerSha256 = (Get-FileHash -LiteralPath $forgeServer).Hash - if ($receipt.forgeServerSha256 -cne $pins.forge_server_sha256) { throw 'Forge server differs from the recorded 61.1.0 runtime pin.' } + if ($receipt.forgeServerSha256 -cne $pins.forge_server_sha256) { throw 'Forge server differs from the recorded 64.1.5 runtime pin.' } if ((Get-Content -LiteralPath $EulaFile -Raw) -notmatch '(?m)^eula=true\s*$') { throw 'Pass the existing accepted EULA from the local smoke fixture.' } @@ -127,9 +127,12 @@ try { $start.RedirectStandardInput = $true $start.RedirectStandardOutput = $true $start.RedirectStandardError = $true - foreach ($arg in @('-Xmx3G', '-Dfml.earlyprogresswindow=false', "@$winArgs", 'nogui')) { + foreach ($arg in @('-Xmx3G', '-XX:ActiveProcessorCount=4', '-Dterminal.jline=false', '-Dfml.earlyprogresswindow=false', "@$winArgs", 'nogui')) { $start.ArgumentList.Add($arg) } + foreach ($name in @('JAVA_TOOL_OPTIONS', 'JDK_JAVA_OPTIONS', '_JAVA_OPTIONS')) { $null = $start.Environment.Remove($name) } + $receipt.jvmArguments = @($start.ArgumentList) + $receipt.environmentJvmOptionsAbsent = @('JAVA_TOOL_OPTIONS', 'JDK_JAVA_OPTIONS', '_JAVA_OPTIONS').Where({ $start.Environment.ContainsKey($_) }).Count -eq 0 $stdout = [IO.StreamWriter]::new((Join-Path $output 'console.log')) $stdout.AutoFlush = $true $process = [Diagnostics.Process]::Start($start) diff --git a/tools/verification/Verify-ElectricEcj.ps1 b/tools/verification/Verify-ElectricEcj.ps1 index ac7be30..e6e5a21 100644 --- a/tools/verification/Verify-ElectricEcj.ps1 +++ b/tools/verification/Verify-ElectricEcj.ps1 @@ -2,7 +2,7 @@ param( [string]$ProjectRoot = (Join-Path $PSScriptRoot '../..'), [Parameter(Mandatory = $true)][string]$Evidence, [string]$Compiler = 'C:/Users/johnb/.p2/pool/plugins/org.eclipse.jdt.core.compiler.batch_3.46.0.v20260528-0407.jar', - [string]$Java21Home = 'C:/Users/johnb/.jdks/temurin-21.0.7+6' + [string]$Java25Home = 'C:/Users/johnb/.jdks/jdk-25.0.3+9' ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest @@ -13,6 +13,11 @@ if (!$Evidence.StartsWith($ProjectRoot + [IO.Path]::DirectorySeparatorChar, [Str } if (Test-Path -LiteralPath $Evidence) { throw 'Refusing to overwrite ECJ evidence' } $null = New-Item -ItemType Directory -Path $Evidence +$inheritedJvmOptions = @{} +foreach ($name in @('JAVA_TOOL_OPTIONS', 'JDK_JAVA_OPTIONS', '_JAVA_OPTIONS')) { + $inheritedJvmOptions[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') + [Environment]::SetEnvironmentVariable($name, $null, 'Process') +} function Get-Pin([string]$Path) { $file = Get-Item -LiteralPath $Path @@ -30,7 +35,7 @@ function Test-ClassFiles([string]$Directory) { if ($bytes.Length -lt 8 -or $bytes[0] -ne 0xCA -or $bytes[1] -ne 0xFE -or $bytes[2] -ne 0xBA -or $bytes[3] -ne 0xBE) { throw "Invalid class file: $($file.FullName)" } - if (($bytes[6] * 256 + $bytes[7]) -ne 65) { throw "Non-Java-21 ECJ output: $($file.FullName)" } + if (($bytes[6] * 256 + $bytes[7]) -ne 69) { throw "Non-Java-25 ECJ output: $($file.FullName)" } } return $files.Count } @@ -38,17 +43,18 @@ function Invoke-Ecj([string]$Name, [string[]]$Sources, [string[]]$Libraries) { $output = Join-Path $Evidence $Name $null = New-Item -ItemType Directory -Path $output $xmlLog = Join-Path $Evidence "$Name.xml" - # Use the real Java 21 module image and keep forbidden/discouraged accesses fatal. - $arguments = @('-21', '-proc:none', '-encoding', 'UTF-8', '-err:+forbidden,discouraged', '--system', - ('"' + $Java21Home.Replace('\', '/') + '"'), '-classpath', + # Use the real Java 25 module image and keep forbidden/discouraged accesses fatal. + $arguments = @('-25', '-proc:none', '-encoding', 'UTF-8', '-err:+forbidden,discouraged', '--system', + ('"' + $Java25Home.Replace('\', '/') + '"'), '-classpath', ('"' + (($Libraries -replace '\\', '/') -join ';') + '"'), '-d', ('"' + $output.Replace('\', '/') + '"'), '-log', ('"' + $xmlLog.Replace('\', '/') + '"')) $arguments += @($Sources | ForEach-Object { '"' + $_.Replace('\', '/') + '"' }) $argFile = Join-Path $Evidence "$Name.args" [IO.File]::WriteAllLines($argFile, $arguments, [Text.UTF8Encoding]::new($false)) - & $java21 -Xmx2G -jar $Compiler "@$argFile" *> (Join-Path $Evidence "$Name-compile.log") + & $java25 -XX:ActiveProcessorCount=4 -Xmx2G -jar $Compiler "@$argFile" *> (Join-Path $Evidence "$Name-compile.log") $exitCode = $LASTEXITCODE $entry = [ordered]@{ exitCode = $exitCode; sourceCount = $Sources.Count; output = $output; log = "$Name-compile.log" } + $entry.jvmArguments = @('-XX:ActiveProcessorCount=4', '-Xmx2G', '-jar', $Compiler, "@$argFile") $report.compilations[$Name] = $entry if ($exitCode -ne 0) { throw "$Name ECJ compilation failed; retain its log and XML diagnostics" } [xml]$diagnostics = Get-Content -LiteralPath $xmlLog -Raw @@ -61,16 +67,16 @@ function Invoke-Ecj([string]$Name, [string[]]$Sources, [string[]]$Libraries) { } $report = [ordered]@{ status = 'RUNNING'; project = $ProjectRoot; evidence = $Evidence - scope = 'Pinned local baseElectric 1.21.11 candidate; fresh ECJ main/test/helper outputs and worldless Java 21 JUnit; no Gradle or Minecraft launch' + scope = 'Pinned local baseElectric 26.1.2 candidate; fresh ECJ main/test/helper outputs and worldless Java 25 JUnit; no Gradle or Minecraft launch' compiler = $null; toolchains = $null; libraries = @(); sources = @(); gradleInventory = @() compilations = [ordered]@{}; junit = $null; negativeControl = $null; apiPinState = $null } try { $properties = ConvertFrom-StringData -StringData (Get-Content -LiteralPath (Join-Path $ProjectRoot 'gradle.properties') -Raw) $report.apiPinState = $properties.poweradvantage_pin_state - if ($properties.mod_id -ne 'baseelectric' -or $properties.mod_version -ne '2.2.16.121111' -or - $properties.minecraft_version -ne '1.21.11' -or $properties.forge_version -ne '61.1.0' -or - $properties.poweradvantage_version -ne '2.4.15.121111' -or $properties.basemetals_version -ne '3.1.2.121111' -or - $properties.orespawn_version -ne '4.1.0.121111' -or + if ($properties.mod_id -ne 'baseelectric' -or $properties.mod_version -ne '2.2.17.2601021' -or + $properties.minecraft_version -ne '26.1.2' -or $properties.forge_version -ne '64.1.5' -or + $properties.poweradvantage_version -ne '2.4.16.2601021' -or $properties.basemetals_version -ne '3.1.2.2601021' -or + $properties.orespawn_version -ne '4.1.1.2601021' -or $properties.poweradvantage_pin_state -notin @('LOCAL_SOURCE', 'MERGED_SOURCE') -or $properties.poweradvantage_source_ref -cnotmatch '^[a-f0-9]{40}$' -or $properties.poweradvantage_runtime_sha256 -cnotmatch '^[A-F0-9]{64}$' -or @@ -78,20 +84,23 @@ try { $properties.poweradvantage_deobf_sha256 -cnotmatch '^[A-F0-9]{64}$' -or $properties.basemetals_deobf_sha256 -cnotmatch '^[A-F0-9]{64}$' -or $properties.orespawn_deobf_sha256 -cnotmatch '^[A-F0-9]{64}$') { - throw 'This verifier requires the source-pinned baseElectric 1.21.11 inputs' + throw 'This verifier requires the source-pinned baseElectric 26.1.2 inputs' } $report.compiler = Get-Pin $Compiler if ($report.compiler.sha256 -ne 'D0D43F8E2D7003E5EFED612E2CBB5F01870043397D8F1BBE536FD9128F4FCBF7') { throw 'ECJ compiler pin mismatch' } - $java21 = Join-Path $Java21Home 'bin/java.exe' - $report.toolchains = [ordered]@{ java = (Get-Pin $java21) - java21Release = (Get-Pin (Join-Path $Java21Home 'release')) - systemModules = (Get-Pin (Join-Path $Java21Home 'lib/modules')) } - if ((Get-Content -LiteralPath (Join-Path $Java21Home 'release') -Raw) -notmatch 'JAVA_VERSION="21\.0\.7"') { - throw 'ECJ production system image must be Java 21.0.7' + $java25 = Join-Path $Java25Home 'bin/java.exe' + $report.toolchains = [ordered]@{ java = (Get-Pin $java25) + java25Release = (Get-Pin (Join-Path $Java25Home 'release')) + systemModules = (Get-Pin (Join-Path $Java25Home 'lib/modules')) } + if ($report.toolchains.java.sha256 -cne 'D5F1B9E03E298EBFB04D2326ECC1E2AF3E6817F9B4D7F9F151FE856D56975650') { + throw 'ECJ must use the pinned Temurin 25.0.3+9 executable' + } + if ((Get-Content -LiteralPath (Join-Path $Java25Home 'release') -Raw) -notmatch 'JAVA_VERSION="25\.0\.3"') { + throw 'ECJ production system image must be Java 25.0.3' } $negativeSource = Join-Path $PSScriptRoot 'negative/RestrictedJdkReference.java' $negativeLog = Join-Path $Evidence 'restricted-jdk-negative.log' - & $java21 -jar $Compiler -21 -proc:none '-err:+forbidden,discouraged' --system $Java21Home ` + & $java25 -XX:ActiveProcessorCount=4 -jar $Compiler -25 -proc:none '-err:+forbidden,discouraged' --system $Java25Home ` -d (Join-Path $Evidence 'negative') $negativeSource *> $negativeLog $negativeExit = $LASTEXITCODE $report.negativeControl = [ordered]@{ source = (Get-Pin $negativeSource); exitCode = $negativeExit @@ -129,17 +138,33 @@ try { $report.sources += @($sources[$set] | ForEach-Object { Get-Pin $_ }) } $classes = @() + $methodInventory = @() $expected = 0 foreach ($xml in @(Get-ChildItem -LiteralPath (Join-Path $ProjectRoot 'build/test-results/test') -File -Filter 'TEST-*.xml' | Sort-Object Name)) { [xml]$inventory = Get-Content -LiteralPath $xml.FullName -Raw if ([int]$inventory.testsuite.failures -or [int]$inventory.testsuite.errors -or [int]$inventory.testsuite.skipped) { throw 'Gradle suite inventory contains failed/skipped tests' } $classes += [string]$inventory.testsuite.name $expected += [int]$inventory.testsuite.tests + foreach ($case in @($inventory.testsuite.testcase)) { + if ([string]$case.classname -cne [string]$inventory.testsuite.name) { throw 'Unexpected JUnit testcase owner' } + $methodInventory += ([string]$case.classname + '#' + [string]$case.name) + } $report.gradleInventory += Get-Pin $xml.FullName } - if ($classes.Count -ne 33 -or $expected -ne 114 -or $sources.test.Count -ne 36 -or $sources.main.Count -ne 88) { throw 'Unexpected Electric 1.21.11 source/test inventory' } + if ($classes.Count -ne 33 -or $expected -ne 117 -or $sources.test.Count -ne 36 -or $sources.main.Count -ne 87) { throw 'Unexpected Electric 26.1.2 source/test inventory' } + $support = @(Get-ChildItem -LiteralPath (Join-Path $ProjectRoot 'build/generated-unit-support') -Recurse -File -Filter '*.java' | Sort-Object Name) + if ($support.Count -ne 2) { throw 'Expected exactly the two pinned native unit support sources' } + $supportPins = @{ 'ForgeUnitRegistries.java' = $properties.unit_registries_java_sha256; 'Forge64UnitLoader.java' = $properties.unit_loader_java_sha256 } + foreach ($source in $support) { + $pin = Get-Pin $source.FullName + if (!$supportPins.ContainsKey($source.Name) -or $pin.sha256 -cne $supportPins[$source.Name]) { throw 'Pinned unit support source mismatch' } + $report.sources += $pin + } + $bootstrap = Join-Path $ProjectRoot 'build/unit-bootstrap/forge-native-unit-bootstrap.jar' + $report.unitBootstrap = Get-Pin $bootstrap + if ($report.unitBootstrap.sha256 -cne $properties.unit_bootstrap_sha256) { throw 'Frozen unit bootstrap jar mismatch' } $main = Invoke-Ecj 'main' $sources.main $mainLibraries - $test = Invoke-Ecj 'test' $sources.test (@($main) + $testLibraries) + $test = Invoke-Ecj 'test' (@($sources.test) + @($support.FullName)) (@($main) + $testLibraries) $gameTests = @(Get-ChildItem -LiteralPath (Join-Path $ProjectRoot 'src/gametest/java') -Recurse -File -Filter '*.java' | ForEach-Object { $_.FullName }) if ($gameTests.Count -ne 1) { throw 'Unexpected GameTest source inventory' } $report.sources += @($gameTests | ForEach-Object { Get-Pin $_ }) @@ -149,21 +174,41 @@ try { $null = Invoke-Ecj 'client' $clientTests (@($main, $test) + $testLibraries) $helperSource = Join-Path $PSScriptRoot 'AuditedJUnit.java' $report.helper = Get-Pin $helperSource - $helper = Invoke-Ecj 'helper' @($helperSource) $testLibraries + $inventorySource = Join-Path $PSScriptRoot 'EclipseTestInventory.java' + $report.inventoryHelper = Get-Pin $inventorySource + $helper = Invoke-Ecj 'helper' @($helperSource, $inventorySource) $testLibraries + $inventoryPath = Join-Path $Evidence 'gradle-method-inventory.txt' + [IO.File]::WriteAllLines($inventoryPath, $methodInventory, [Text.UTF8Encoding]::new($false)) + $inventoryRuntime = @($helper) + $testLibraries + & $java25 -XX:ActiveProcessorCount=4 -cp ($inventoryRuntime -join ';') zone.moddev.mc.baseelectric.verification.EclipseTestInventory $test $inventoryPath $expected *> (Join-Path $Evidence 'inventory.log') + if ($LASTEXITCODE -ne 0) { throw 'Fresh compiled JUnit method inventory does not match Gradle XML' } + $report.inventory = [ordered]@{ status = 'PASS'; methods = $methodInventory.Count; suites = $classes.Count; negatives = @() } + foreach ($prefix in @('zone.moddev.mc.baseelectric.entities.', 'zone.moddev.mc.baseelectric.items.')) { + $filtered = @($methodInventory | Where-Object { !$_.StartsWith($prefix, [StringComparison]::Ordinal) }) + if (!$filtered.Count -or $filtered.Count -eq $methodInventory.Count) { throw 'Inventory negative control excluded no tests' } + $negativePath = Join-Path $Evidence ('inventory-exclusion-' + $report.inventory.negatives.Count + '.txt') + [IO.File]::WriteAllLines($negativePath, $filtered, [Text.UTF8Encoding]::new($false)) + $log = $negativePath + '.log' + & $java25 -XX:ActiveProcessorCount=4 -cp ($inventoryRuntime -join ';') zone.moddev.mc.baseelectric.verification.EclipseTestInventory $test $negativePath $filtered.Count *> $log + if ($LASTEXITCODE -eq 0 -or (Get-Content -LiteralPath $log -Raw) -notmatch 'Incomplete or stale Gradle method inventory') { throw 'JUnit prefix-exclusion negative control was not rejected' } + $report.inventory.negatives += [ordered]@{ prefix = $prefix; tests = $filtered.Count; expectedRejection = $true; log = $log } + } $junitXml = Join-Path $Evidence 'junit.xml' $runtime = @($main, $test, $helper, (Join-Path $ProjectRoot 'build/resources/main'), (Join-Path $ProjectRoot 'build/resources/test')) + $testLibraries $report.runtime = $runtime Push-Location $ProjectRoot try { - & $java21 -Xmx2G -cp ($runtime -join ';') zone.moddev.mc.baseelectric.verification.AuditedJUnit $junitXml $expected @classes *> (Join-Path $Evidence 'junit.log') + & $java25 -XX:ActiveProcessorCount=4 -Xmx2G "-javaagent:$bootstrap" -cp ($runtime -join ';') zone.moddev.mc.baseelectric.verification.AuditedJUnit $junitXml $expected @classes *> (Join-Path $Evidence 'junit.log') $junitExit = $LASTEXITCODE } finally { Pop-Location } $report.junit = [ordered]@{ exitCode = $junitExit; expected = $expected; suites = $classes; log = 'junit.log'; xml = 'junit.xml' } + $report.junit.jvmArguments = @('-XX:ActiveProcessorCount=4', '-Xmx2G', "-javaagent:$bootstrap", '-cp', ($runtime -join ';')) + $report.junit.inheritedJvmEnvironment = 'CLEARED' if ($junitExit -ne 0) { throw 'Fresh ECJ JUnit execution failed' } [xml]$junit = Get-Content -LiteralPath $junitXml -Raw if ($junit.testsuite.passed -ne 'true' -or [int]$junit.testsuite.tests -ne $expected -or [int]$junit.testsuite.failures -or [int]$junit.testsuite.ignored -or [int]$junit.testsuite.assumptions) { throw 'Strict ECJ test counts failed' } - foreach ($pin in @($report.sources) + @($report.libraries) + @($report.helper) + @($report.negativeControl.source)) { + foreach ($pin in @($report.sources) + @($report.libraries) + @($report.helper, $report.inventoryHelper, $report.unitBootstrap) + @($report.negativeControl.source)) { if ((Get-FileHash -LiteralPath $pin.path -Algorithm SHA256).Hash -ne $pin.sha256) { throw "Input changed during ECJ qualification: $($pin.path)" } } $report.status = 'PASS' @@ -171,6 +216,11 @@ try { $report.status = 'FAIL' $report.error = $_.ToString() throw -} finally { Save-Report } +} finally { + Save-Report + foreach ($name in $inheritedJvmOptions.Keys) { + [Environment]::SetEnvironmentVariable($name, $inheritedJvmOptions[$name], 'Process') + } +} Get-Content -LiteralPath (Join-Path $Evidence 'junit.log') -Tail 8 -Write-Output "ECJ PASS: $($sources.main.Count) main, $($sources.test.Count) test sources; $expected tests; Java 21 class files" +Write-Output "ECJ PASS: $($sources.main.Count) main, $($sources.test.Count) test sources; $expected tests; Java 25 class files" diff --git a/tools/verify-native-port.js b/tools/verify-native-port.js index fb49ac0..c5b407a 100644 --- a/tools/verify-native-port.js +++ b/tools/verify-native-port.js @@ -5,7 +5,7 @@ const { execFileSync } = require('node:child_process'); const { createHash } = require('node:crypto'); const root = path.resolve(__dirname, '..'); -const baseline = 'b81645579c3330476b84149402c3ce78183df85f'; +const baseline = '951fc3921f27e963e96f29c78a38ee7093a65fde'; const legacyTrackBaseline = '013737d4cc41947e8b6fa42f4bf926a1fa5ab998'; const git = (...args) => execFileSync('git', args, { cwd: root }); const old = file => git('show', baseline + ':' + file); @@ -35,34 +35,26 @@ function check(name, run) { catch (error) { checks.push({name, pass: false, error: error.message}); } } -check('Exact Forge 61 and local port identity', () => { +check('Exact Forge 64 and local port identity', () => { const props = Object.fromEntries(read('gradle.properties').toString().split(/\r?\n/) .filter(line => /^[a-z][a-z_\d]*=/.test(line)).map(line => line.split(/=(.*)/s).slice(0, 2))); for (const [key, value] of Object.entries({ - minecraft_version: '1.21.11', forge_version: '61.1.0', mapping_channel: 'official', - mapping_version: '1.21.11', mcp_version: '20251223.124241', java_version: '21', - mod_id: 'baseelectric', mod_name: 'baseElectric', mod_version: '2.2.16.121111', - poweradvantage_version: '2.4.15.121111', basemetals_version: '3.1.2.121111', - poweradvantage_pin_state: 'MERGED_SOURCE', - poweradvantage_source_ref: '9b0043483d79d3e575895f12aeb0dde7ef6c4890', - poweradvantage_public_file_id: '9566998', - poweradvantage_deobf_sha256: 'C98D2CF163BE2480D9AFB0023BBD2D92D401AE0D50BEF73191235BF3F83774A7', - poweradvantage_runtime_sha256: 'C98D2CF163BE2480D9AFB0023BBD2D92D401AE0D50BEF73191235BF3F83774A7', - basemetals_source_commit: '6f4c44e4d0a68bca3838fb025c09cddfaaa08fe8', - basemetals_public_file_id: '9566738', - basemetals_deobf_sha256: '598D3FB0FF956EBCC67FE477A01CE393C75D0013AF0CE00A79E2072B63E9F2BA', - basemetals_runtime_sha256: '598D3FB0FF956EBCC67FE477A01CE393C75D0013AF0CE00A79E2072B63E9F2BA', - orespawn_source_commit: 'ecad75b24309a344f712d898d717c6a8823fc9f2', - orespawn_deobf_sha256: 'DE39929057288CEA110196C9E9D708E129B80BC360A2E53976337FFA6A84A67B', - orespawn_version: '4.1.0.121111', curseforge_project_id: '1783006', + minecraft_version: '26.1.2', forge_version: '64.1.5', mapping_channel: 'official', + mapping_version: '26.1.2', mcp_version: '20260409.101008', java_version: '25', + mod_id: 'baseelectric', mod_name: 'baseElectric', mod_version: '2.2.17.2601021', + poweradvantage_version: '2.4.16.2601021', basemetals_version: '3.1.2.2601021', + orespawn_source_commit: '101fd12b598934c353e417c57edc5c2bbefd5f53', + orespawn_deobf_sha256: '810284ECB32D5249381F246498CEFA57E70FFF7CCC40110F799FC276CBC42BF7', + orespawn_version: '4.1.1.2601021', curseforge_project_id: '1783006', cf_requirements: 'basepower,base-metals' })) assert.equal(props[key], value, key); + assert.equal(props.reobf, 'false'); assert.deepEqual(json('src/main/resources/pack.mcmeta').pack, - {min_format:[94,1], max_format:[94,1], description:'baseElectric'}); + {min_format:[101,1], max_format:[101,1], description:'baseElectric'}); const mods = read('src/main/resources/META-INF/mods.toml').toString(); assert.match(mods, /^showAsResourcePack=false$/m); - assert.match(mods, /modId="basepower"\s+mandatory=true\s+versionRange="\[2\.4\.15\.121111,3\.0\.0\)"/); - assert.match(mods, /modId="basemetals"\s+mandatory=true\s+versionRange="\[3\.1\.2\.121111,4\.0\.0\)"/); + assert.match(mods, /modId="basepower"\s+mandatory=true\s+versionRange="\[2\.4\.16\.2601021,3\.0\.0\)"/); + assert.match(mods, /modId="basemetals"\s+mandatory=true\s+versionRange="\[3\.1\.2\.2601021,4\.0\.0\)"/); const build = read('build.gradle').toString(); assert.match(build, /inputs\.files\(javadocTool\.map/); assert.match(build, /legal\/java\.base/); @@ -70,6 +62,18 @@ check('Exact Forge 61 and local port identity', () => { assert.match(build, /must contain the selected JDK's full canonical legal text/); }); +check('Native source and artifact pins are complete before qualification', () => { + const props = Object.fromEntries(read('gradle.properties').toString().split(/\r?\n/) + .filter(line => /^[a-z][a-z_\d]*=/.test(line)).map(line => line.split(/=(.*)/s).slice(0, 2))); + assert(['LOCAL_SOURCE', 'MERGED_SOURCE'].includes(props.poweradvantage_pin_state)); + for (const prefix of ['poweradvantage', 'basemetals', 'orespawn']) { + const source = props[prefix + (prefix === 'poweradvantage' ? '_source_ref' : '_source_commit')]; + assert.match(source, /^[a-f0-9]{40}$/, prefix + ' source pin is pending'); + assert.match(props[prefix + '_deobf_sha256'], /^[A-F0-9]{64}$/, prefix + ' artifact pin is pending'); + assert.equal(props[prefix + '_deobf_sha256'], props[prefix + '_runtime_sha256']); + } +}); + function checkTrackModel(file, model) { assert.equal(model.render_type, 'minecraft:cutout', file + ' lacks its native cutout layer'); const { render_type, ...unchanged } = model; @@ -96,9 +100,29 @@ check('Track audit rejects the old asset and a geometry mutation', () => { assert.throws(() => checkTrackModel(file, mutated), /changed beyond the render layer/); }); -check('Accepted 1.21.1 resources retain bytes except native recipe schema and pack metadata', () => { +check('Released resources retain bytes except exact native metadata and atlas repairs', () => { const resources = tracked.filter(file => /^src\/(main|test)\/resources\//.test(file)); - const exceptions = new Set(['src/main/resources/pack.mcmeta', 'src/main/resources/META-INF/mods.toml']); + const removedAbstract = assets + 'items/battery_base.json'; + const blocksAtlas = 'src/main/resources/assets/minecraft/atlases/blocks.json'; + const itemsAtlas = 'src/main/resources/assets/minecraft/atlases/items.json'; + const exceptions = new Set(['src/main/resources/pack.mcmeta', 'src/main/resources/META-INF/mods.toml', blocksAtlas, removedAbstract]); + assert(!fs.existsSync(path.join(root, removedAbstract)), 'Abstract battery parent must not be a standalone item definition'); + const sprites = new Set(); + for (const [atlas, count] of [[blocksAtlas, 49], [itemsAtlas, 17]]) { + const entries = json(atlas).sources; + assert.equal(entries.length, count, atlas); + for (const source of entries) { + assert.equal(source.type, 'minecraft:single', atlas + ' has a non-owned directory or filter'); + assert.match(source.resource, /^baseelectric:[a-z0-9_./-]+$/); + assert(!('sprite' in source), 'Unexpected sprite alias'); + assert(!sprites.has(source.resource), 'Duplicate block/item atlas sprite: ' + source.resource); + sprites.add(source.resource); + assert(fs.existsSync(path.join(root, assets + 'textures/' + source.resource.split(':')[1] + '.png'))); + } + } + assert(sprites.has('baseelectric:entity/laser_turret')); + assert(sprites.has('baseelectric:materials/machine_case')); + assert(!sprites.has('baseelectric:materials/laser_beam'), 'Direct-render beam must not enter a mipmapped atlas'); for (const file of resources.filter(file => !exceptions.has(file))) { if (file.startsWith('src/main/resources/data/baseelectric/recipe/')) { assert.deepEqual(json(file), nativeIngredients(JSON.parse(old(file))), file); @@ -111,20 +135,16 @@ check('Accepted 1.21.1 resources retain bytes except native recipe schema and pa .filter(entry => entry.isFile()).map(entry => path.relative(root, path.join(entry.parentPath, entry.name)) .split(path.sep).join('/')).sort(); const definitions = tracked.filter(file => file.startsWith(assets + 'models/item/') && file.endsWith('.json')) - .map(file => file.replace('/models/item/', '/items/')); - assert.equal(definitions.length, 44); + .map(file => file.replace('/models/item/', '/items/')).filter(file => file !== removedAbstract); + assert.equal(definitions.length, 43); for (const file of definitions) { const id = path.basename(file, '.json'); assert.deepEqual(json(file), {model:{type:'minecraft:model', model:'baseelectric:item/' + id}}, file); } - assert.deepEqual(actual, [...tracked.map(relocated), ...definitions, - javaRoot + 'util/RegistryIds.java', javaRoot + 'util/LegacyNbt.java', - javaRoot.replace('main', 'test') + 'util/crafting/RecipeDeconstructorTest.java', - javaRoot.replace('main', 'test') + 'resources/NativePackRangeContractTest.java', - javaRoot.replace('main', 'test') + 'verification/NativePackResourceProof.java', - ...['drillupgradesandtransferslegacybattery', 'recipeavailabilitydoesnotremovediscovery', - 'siliconandsolderresolverealmaterialtags', 'turretusesnativeupdatepacket'] - .map(name => 'src/gametest/resources/data/baseelectric/test_instance/' + name + '.json')].sort(), + const trades = ['electric_conduit', 'solder', 'integrated_circuit', 'blank_circuit_board', 'petrolplastic_ingot', 'psu'] + .map(id => 'src/main/resources/data/baseelectric/villager_trade/librarian/' + id + '.json'); + const tiers = [1,2].map(level => 'src/main/resources/data/minecraft/tags/villager_trade/librarian/level_' + level + '.json'); + assert.deepEqual(actual, [...tracked.filter(file => file !== javaRoot + 'init/Villages.java' && file !== removedAbstract), itemsAtlas, ...trades, ...tiers].sort(), 'Source and resource inventory'); }); @@ -143,36 +163,44 @@ check('Migration and registration code keep their accepted IDs and compatibility .replaceAll('ResourceLocation', 'Identifier').replace(/\s/g,'')); }); -check('All 106 accepted tests and their assertions are retained without skips', () => { +check('All 114 released tests retain their coverage without skips', () => { const methods = value => [...value.toString().matchAll(/@Test\s+public void (\w+)\(/g)].map(m => m[1]); const files = tracked.filter(file => file.startsWith('src/test/java/') && file.endsWith('.java')); + const renamed = { + hostedBuildsProvisionAllThreePinnedToolchainsBeforeGradle: 'hostedBuildsPinJava25ForEveryRoleBeforeGradle', + resourcePackDeclaresTheNativeOneTwentyOneFormat: 'resourcePackDeclaresTheNativeTwentySixFormat' + }; let inherited = 0; let actual = 0; for (const file of files) { const current = read(file); const currentMethods = methods(current); const priorMethods = methods(old(file)); - for (const name of priorMethods) assert(currentMethods.includes(name), file + ' lost ' + name); + for (const name of priorMethods) assert(currentMethods.includes(renamed[name] || name), file + ' lost ' + name); const assertions = value => [...value.toString().matchAll(/\bassert\w*\s*\(/g)].length; assert(assertions(current) >= assertions(old(file)), file + ' lost assertions'); assert.doesNotMatch(current.toString(), /@Ignore|Assume\.|assumeTrue\(|assumeFalse\(/, file); inherited += priorMethods.length; actual += currentMethods.length; } - assert.equal(inherited, 106); - assert.equal(actual, 106); - const added = read(javaRoot.replace('main','test') + 'util/crafting/RecipeDeconstructorTest.java'); - assert.equal(methods(added).length, 2); - assert.doesNotMatch(added.toString(), /@Ignore|Assume\.|assumeTrue\(|assumeFalse\(/); - const pack = read(javaRoot.replace('main', 'test') + 'resources/NativePackRangeContractTest.java'); - assert.equal(methods(pack).length, 6); - assert.doesNotMatch(pack.toString(), /@Ignore|Assume\.|assumeTrue\(|assumeFalse\(/); + assert.equal(inherited, 114); + assert.equal(actual, 117); const gameTests = javaRoot.replace('main', 'gametest') + 'gametest/ElectricGameTests.java'; const names = value => [...value.toString().matchAll(/public static void (\w+)\(GameTestHelper/g)].map(m => m[1]); for (const name of names(old(gameTests))) assert(names(read(gameTests)).includes(name), 'Lost GameTest: ' + name); }); +check('Official-name production code contains no obsolete SRG field or method names', () => { + const files = fs.readdirSync(path.join(root, javaRoot), {recursive:true, withFileTypes:true}) + .filter(entry => entry.isFile() && entry.name.endsWith('.java')); + for (const entry of files) { + const file = path.join(entry.parentPath, entry.name); + assert.doesNotMatch(fs.readFileSync(file, 'utf8'), /\b(?:f_\d+_|m_\d+_|field_\d+_[A-Za-z_]+|func_\d+_[A-Za-z_]+)\b/, + path.relative(root, file) + ' still uses obsolete mapped field/method names'); + } +}); + check('All 58 recipe IDs, results, guards and ingredients are unchanged', () => { const files = tracked.filter(file => /\/data\/baseelectric\/recipe\/.*\.json$/.test(file)); assert.equal(files.length, 58);