diff --git a/.github/verify-published-pins.sh b/.github/verify-published-pins.sh index 8cd0265..d2b5090 100644 --- a/.github/verify-published-pins.sh +++ b/.github/verify-published-pins.sh @@ -9,10 +9,10 @@ require_merged() { echo "::error::$repo needs an exact source pin. A pending local candidate is not a public dependency." exit 1 fi - 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 } @@ -39,7 +39,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" @@ -48,8 +48,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 77bccf0..a292070 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,16 @@ -name: baseSteam 1.21.11 CI +name: baseSteam 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: basesteam-1.21.11-${{ github.workflow }}-${{ github.ref }} + group: basesteam-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 Gradle, compiler, Mavenizer and Slime 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,7 +64,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: Build the released basePower test bootstrap shell: bash @@ -82,36 +72,37 @@ jobs: set -euo pipefail power="$GITHUB_WORKSPACE/.ci-dependencies/basePower" (cd "$power" && bash ./gradlew $MMD_GRADLE_TOOLCHAINS unitBootstrapJar \ - "-Pforge61MavenizerJar=$MAVENIZER" "-PbaseMetalsDeobfJar=$METALS_PLAYER" "-PoreSpawnDeobfJar=$ORESPAWN_PLAYER" --no-daemon) + "-Pforge64MavenizerJar=$MAVENIZER" "-PbaseMetalsDeobfJar=$METALS_PLAYER" "-PoreSpawnDeobfJar=$ORESPAWN_PLAYER" --no-daemon) echo "POWER_BOOTSTRAP=$power/build/unit-bootstrap/forge-native-unit-bootstrap.jar" >> "$GITHUB_ENV" echo "POWER_BOOTSTRAP_SOURCE=$power/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java" >> "$GITHUB_ENV" echo "POWER_REGISTRY_SOURCE=$power/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/ForgeUnitRegistries.java" >> "$GITHUB_ENV" + echo "POWER_LOADER_SOURCE=$power/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/Forge64UnitLoader.java" >> "$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" \ "-PbaseMetalsDeobfJar=$METALS_PLAYER" "-PoreSpawnDeobfJar=$ORESPAWN_PLAYER" \ "-PforgeCapabilityBootstrapJar=$POWER_BOOTSTRAP" "-PforgeCapabilityBootstrapSource=$POWER_BOOTSTRAP_SOURCE" \ - "-PforgeUnitRegistriesSource=$POWER_REGISTRY_SOURCE" + "-PforgeUnitRegistriesSource=$POWER_REGISTRY_SOURCE" "-Pforge64UnitLoaderSource=$POWER_LOADER_SOURCE" - name: Upload audited release candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: baseSteam-1.21.11-${{ github.sha }} + name: baseSteam-26.1.2-${{ github.sha }} if-no-files-found: error retention-days: 30 path: | - build/libs/baseSteam-2.2.16.121111.jar - build/libs/baseSteam-2.2.16.121111-sources.jar - build/libs/baseSteam-2.2.16.121111-javadoc.jar + build/libs/baseSteam-2.2.17.2601021.jar + build/libs/baseSteam-2.2.17.2601021-sources.jar + build/libs/baseSteam-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: baseSteam-1.21.11-diagnostics-${{ github.sha }} + name: baseSteam-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 2ebea56..da6ede7 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: basesteam-1.21.11-${{ github.workflow }}-${{ github.ref }} + group: basesteam-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 Gradle, compiler, Mavenizer and Slime 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,7 @@ 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" \ "-PbaseMetalsDeobfJar=$METALS_PLAYER" "-PoreSpawnDeobfJar=$ORESPAWN_PLAYER" - name: Analyze uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index 9a26df1..863ab62 100644 --- a/.github/workflows/release-on-tag.yml +++ b/.github/workflows/release-on-tag.yml @@ -37,9 +37,9 @@ jobs: minecraft_version="$(value minecraft_version)" loader_name="$(value loader_name)" loader_code="$(value loader_code)" - if [[ "$minecraft_version" != '1.21.11' || "$(value forge_version)" != '61.1.0' || - "$(value mapping_version)" != '1.21.11' || "$(value mcp_version)" != '20251223.124241' ]]; then - echo 'Expected the exact Minecraft 1.21.11 Forge 61.1.0 release target' >&2 + if [[ "$minecraft_version" != '26.1.2' || "$(value forge_version)" != '64.1.5' || + "$(value mapping_version)" != '26.1.2' || "$(value mcp_version)" != '20260409.101008' ]]; then + echo 'Expected the exact Minecraft 26.1.2 Forge 64.1.5 release target' >&2 exit 1 fi 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 489c869..0539735 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,13 @@ +baseSteam 2.2.17.2601021 (Minecraft 26.1.2 / Forge) + +- Bring baseSteam to Minecraft 26.1.2 with Forge 64.1.5 and Java 25. +- Preserve machine state, registry IDs, recipes, progression and old-world aliases. +- Fix boilers and blast furnaces consuming fuels without a crafting remainder; keep fuel use and container returns unchanged. +- Keep all nine salvage recipes in their original recipe modes without unsafe setup-time item creation. +- Keep small textures from other mods out of Steam's native atlases. +- 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. + baseSteam 2.2.16.121111 (Minecraft 1.21.11 / Forge) - Target Forge 61.1.0, Java 21 and released basePower 2.4.15.121111. diff --git a/README.md b/README.md index 2c4f5ab..a37d067 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # baseSteam -**Minecraft 1.21.11 (Forge), baseSteam 2.2.16.121111.** +**Minecraft 26.1.2 (Forge), baseSteam 2.2.17.2601021. Accepted for release.** + +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. baseSteam adds boilers, steam-powered machines, pumps, storage, elevators and a blackpowder musket to basePower. Items and blocks share the basePower @@ -8,66 +14,72 @@ creative tab. The mod ID and resource namespace remain `basesteam`. ## Build -Target: Forge 61.1.0, official Minecraft 1.21.11 mappings and MCP -20251223.124241. ForgeGradle 7.0.34 and the checksum-pinned Gradle 9.6.1 -wrapper use Temurin 21.0.7+6. Mavenizer uses Java 25; the launcher helper -also needs Java 8. Minecraft itself runs on Java 21. +Target: Forge 64.1.5, official Minecraft 26.1.2 names and MCP +20260409.101008, with reobf=false. ForgeGradle 7.0.34 and the checksum-pinned +Gradle 9.6.1 wrapper use Temurin 25.0.3+9. The compiler, Minecraft, +Mavenizer and Slime launcher also use that Java 25 toolchain. -Install [basePower 2.4.15.121111](https://github.com/MinecraftModDevelopmentMods/basePower/releases/tag/2.4.15.121111), -[Base Metals 3.1.2.121111](https://www.curseforge.com/minecraft/mc-mods/base-metals/files/9566738) -and [OreSpawn 4.1.0.121111](https://www.curseforge.com/minecraft/mc-mods/mmd-orespawn/files/9268464) -as separate Forge 1.21.11 jars on both client and server, not bundled copies. +This port requires released basePower 2.4.16.2601021, +Base Metals 3.1.2.2601021 and OreSpawn 4.1.1.2601021. +They remain separate Forge jars on both client and server, not bundled copies. Base Metals is REQUIRED on both client and server, not an optional integration. -The default build paths use the coordinated 1.21.11 checkouts. Exact released -source, jar and test-bootstrap pins are in `gradle.properties` and are checked -before compilation and tests. - -This version has passed automated qualification and manual acceptance. The -[qualification record](https://github.com/MinecraftModDevelopmentMods/basePower/blob/master-1.21.11/docs/1.21.11/QUALIFICATION.md) -records the machine, upgrade, graphical and two-world endurance checks and -their coverage limits. Earlier no-Base-Metals receipts are -historical evidence, not a supported installation profile. -The mod root declares server data format 94.1. Forge supplies format 75.x -metadata for its hidden client resource aggregate. Native resource-reader -tests cover both paths without codec warnings; see -[Forge metadata notes](docs/FORGE61-RESOURCE-METADATA.md) for the separate -Forge 61.1.0 server compatibility-label defect. - -The Mavenizer fixture comes from OreSpawn's OS12111 checkout. Another layout -can use `-Pforge61MavenizerJar=`. The `*DeobfJar` and `*PlayerJar` +The default build paths use the coordinated 26.1.2 checkouts. The public inputs +are Power source a684e71e/player 23ABCC1F (CurseForge 9576332), Base Metals +source 5e708468/player 67862388 (9576007), and OreSpawn 810284EC (9572926). +Their anonymous downloads match the tested jars. Exact source, jar and fixture08 +bootstrap pins are in gradle.properties. No 1.21.11 dependency is substituted. + +The public-pin rebuild retains the accepted Steam jar bytes. Publication +follows Power, then Steam, then Electric; local acceptance does not stand in +for merged-source CI or public downloads. +The new clean release suite and fresh ECJ each pass 276 tests; independent +warm and empty-cache builds in paths with spaces reproduce all three archives. +These are child build gates, not combined-family or manual acceptance. + +The released 1.21.11 baseline and its receipts remain separate historical +evidence. Native 26.1.2 +metadata declares server data format 101.1 and client format 84.0; the hidden +client resource aggregate remains Forge's responsibility. The current kit's +native reader, machine, migration, side-loading and GUI results are recorded in +the family qualification record. Its original automated and manual-only scope +labels remain historical; manual acceptance was confirmed afterward. + +The sealed BB0D Mavenizer fixture comes from OreSpawn's OS2612 checkout. Another +layout can use `-Pforge64MavenizerJar=`. The `*DeobfJar` and `*PlayerJar` properties select separate development and packaged inputs; both must satisfy the committed checksums. -With the pinned released dependencies available locally: +After the coordinated native dependency pins are available locally: ```powershell -$env:JAVA_HOME = "$env:USERPROFILE/.jdks/temurin-21.0.7+6" -$env:GRADLE_USER_HOME = 'D:/GradleCaches/basesteam-12111-port' -$jdk8 = "$env:USERPROFILE/.jdks/temurin-8.0.502+7/jdk8u502-b07" -$jdks = "$env:JAVA_HOME,$env:USERPROFILE/.jdks/jdk-25.0.3+9,$jdk8" -$toolchains = @("-Porg.gradle.java.installations.paths=$jdks", +$env:JAVA_HOME = "$env:USERPROFILE/.jdks/jdk-25.0.3+9" +$env:GRADLE_USER_HOME = 'D:/GradleCaches/basesteam-2612-port' +$toolchains = @("-Porg.gradle.java.installations.paths=$env:JAVA_HOME", '-Porg.gradle.java.installations.auto-detect=false', '-Porg.gradle.java.installations.auto-download=false') ./gradlew.bat @toolchains check build deobfJar verifyReleaseArtifacts writeReleaseChecksums ./gradlew.bat @toolchains verifyEclipseProductionClasspath ./gradlew.bat @toolchains -PsteamGameTestRunId=candidate verifyPlayerDependencies runGameTestServer -./tools/run-packaged-server-smoke.ps1 -ForgeInstall $env:FORGE_12111_SERVER_INSTALL -RunId candidate-with-metals -UseNioTransport -Reopen +./tools/run-packaged-server-smoke.ps1 -ForgeInstall $env:FORGE_2612_SERVER_INSTALL -RunId candidate-with-metals -UseNioTransport -Reopen ``` For a checkout outside the sibling-folder layout, unit tests also need the -verified Power test bootstrap and its two source files. Build Power first, +verified Power test bootstrap and its three source files. Build Power first, then supply these paths as well as the dependency-jar overrides: ```text -PforgeCapabilityBootstrapJar=/build/unit-bootstrap/forge-native-unit-bootstrap.jar -PforgeCapabilityBootstrapSource=/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java -PforgeUnitRegistriesSource=/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/ForgeUnitRegistries.java +-Pforge64UnitLoaderSource=/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/Forge64UnitLoader.java ``` The atlas test uses the same verified Power jar selected for compilation; it does not look for a second copy in a fixed local folder. Missing or -changed inputs fail the checks. The current source check passes 252 unit -tests. Native resource tests disable interactive terminal setup in their +changed inputs fail the checks. The previous candidate09 passed 276 unit +tests and a fresh independent ECJ run of the same complete inventory. The +scoped runtime and reproducibility results are in the port status below; +they do not imply family or manual acceptance. Native resource tests disable interactive terminal setup in their child process, while keeping warnings and errors fatal. Keep Java option environment variables unset when running the packaged family rig; it records its own runtime arguments. @@ -92,6 +104,7 @@ check machines, restarts, redstone, all drill directions, elevators, GUI captions, whole fluid gauges, track transparency and musket effects in-game. Release acceptance does not turn the remaining manual checks into automated passes or qualify every modpack. -See the [1.21.11 notes](docs/PORT12111-STATUS.md). The +See the [26.1.2 port status](docs/PORT2612-STATUS.md). The +[1.21.11 notes](docs/PORT12111-STATUS.md) and the [1.21.1 notes](docs/PORT1211-STATUS.md) describe the accepted source baseline, not qualification of this port. diff --git a/build.gradle b/build.gradle index cbba5bf..0bda469 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ plugins { id 'net.minecraftforge.gradle' version '7.0.34' } -apply from: 'gradle/forge61-mavenizer.gradle' +apply from: 'gradle/forge64-mavenizer.gradle' group = project.mod_group version = project.mod_version @@ -43,9 +43,21 @@ ext.functional_version = versionParts[0..2].join('.') ext.display_version = project.mod_version ext.release_tag = project.mod_version +if (GradleVersion.current() != GradleVersion.version('9.6.1') + || JavaVersion.current() != JavaVersion.VERSION_25) { + throw new GradleException('Use Gradle 9.6.1 running on the pinned Java 25 toolchain') +} + +fgtools.configure('slimelauncher') { + javaLauncher.set(javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(25) + vendor = JvmVendorSpec.ADOPTIUM + }) +} + java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(25) vendor = JvmVendorSpec.ADOPTIUM } withSourcesJar() @@ -54,11 +66,11 @@ java { tasks.withType(JavaCompile).configureEach { javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(25) vendor = JvmVendorSpec.ADOPTIUM } - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 options.encoding = 'UTF-8' options.compilerArgs.addAll(['-Xmaxerrs', '1000']) } @@ -119,7 +131,8 @@ minecraft { .getOrElse('run') runs { configureEach { - jvmArgs '--add-opens', 'java.base/java.lang.invoke=ALL-UNNAMED' + jvmArgs '--add-opens', 'java.base/java.lang.invoke=ALL-UNNAMED', + '-XX:ActiveProcessorCount=4', '-Xmx1536m' workingDir.convention layout.projectDirectory.dir(configuredRunDirectory) systemProperty 'forge.logging.markers', 'REGISTRIES' systemProperty 'forge.logging.console.level', 'info' @@ -152,7 +165,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 61 launches.' + description = 'Merges classes and processed resources for Forge 64 launches.' dependsOn tasks.named('classes') from sourceSets.main.output into forgeRunModClassesDirectory @@ -165,8 +178,15 @@ def prepareForgeGameTestModClasses = tasks.register('prepareForgeGameTestModClas } tasks.withType(JavaExec).configureEach { jvmArgs '-XX:ActiveProcessorCount=4' + maxHeapSize = '1536m' + doFirst { + ['JAVA_TOOL_OPTIONS', 'JDK_JAVA_OPTIONS', '_JAVA_OPTIONS'].each { name -> + if (environment.containsKey(name)) throw new GradleException("${path} must not inherit ${name}") + } + logger.lifecycle("${path}: inherited JVM option variables absent; explicit -XX:ActiveProcessorCount=4 -Xmx1536m") + } javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(25) vendor = JvmVendorSpec.ADOPTIUM } if (name in ['runClient', 'runServer', 'runData', 'runGameTestServer']) { @@ -205,6 +225,18 @@ tasks.withType(JavaExec).configureEach { if (!output.contains('All 4 required tests passed') || (output =~ /\[[^\]\r\n]*\/(?:ERROR|FATAL)\]/).find()) { throw new GradleException("The four Steam GameTests did not pass; see ${receipt}") } + if (!output.contains('BASESTEAM_NATIVE_TRADES_PASS definitions=5 placements=10 samples=1280 codecNegatives=5 fallbackWitnesses=1')) { + throw new GradleException("Missing native villager trade codec and offer witness: ${receipt}") + } + if (!output.contains('BASESTEAM_NATIVE_JVM_PASS inheritedOptions=absent processorCap=4 heapCapMiB=1536')) { + throw new GradleException("Missing actual native JVM environment and cap witness: ${receipt}") + } + def salvage = output =~ /BASESTEAM_NATIVE_SALVAGE_PASS mode=(NORMAL|TECH_PROGRESSION|APOCALYPTIC) present=(\d+) absent=(\d+) nativeCodecs=(\d+) inputNegatives=(\d+)/ + if (!salvage.find()) throw new GradleException("Missing native salvage recipe witness: ${receipt}") + List expectedSalvage = salvage.group(1) == 'APOCALYPTIC' ? ['9', '0', '9', '9'] : ['0', '9', '0', '0'] + if ((2..5).collect { salvage.group(it) } != expectedSalvage || salvage.find()) { + throw new GradleException("Incorrect native salvage recipe witness: ${receipt}") + } File report = new File(steamGameTestRunDirectory.get().asFile, 'steam-gametest-results.xml') if (!report.isFile()) throw new GradleException('Missing native GameTest XML report') def results = new groovy.xml.XmlSlurper().parse(report) @@ -246,12 +278,12 @@ repositories { // Only code and launch tasks require the exact jars; source/resource checks can run independently. def explicitJar = { String property -> providers.gradleProperty(property).map { file(it) }.getOrNull() } def powerRoot = '../../PowerAdvantage' -def baseMetalsRoot = '../../../../BaseMetals/.verification-basemetals12111/Advantage shared hook' +def baseMetalsRoot = '../../../../BaseMetals/BaseMetals 2612/BaseMetals' def powerNativeJar = file("${powerRoot}/build/libs/basePower-${project.poweradvantage_version}.jar") def powerAdvantageDeobfJar = explicitJar('powerAdvantageDeobfJar') ?: powerNativeJar def baseMetalsDeobfJar = explicitJar('baseMetalsDeobfJar') ?: file( "${baseMetalsRoot}/build/libs/BaseMetals-${project.basemetals_version}.jar") -def oreSpawnRoot = '../../../../MinecraftMineralogy/OS12111/OreSpawn' +def oreSpawnRoot = '../../../../MinecraftMineralogy/OS2612/OreSpawn' def oreSpawnJar = { String variant -> explicitJar("oreSpawn${variant}Jar") ?: file( "${oreSpawnRoot}/build/libs/OreSpawn-${project.orespawn_version}.jar") @@ -304,8 +336,31 @@ def unitBootstrapSources = [ poweradvantage_test_bootstrap_source_sha256: explicitJar('forgeCapabilityBootstrapSource') ?: file("${powerRoot}/test-rig/unit-bootstrap/ForgeCapabilityBootstrap.java"), poweradvantage_test_registry_source_sha256: explicitJar('forgeUnitRegistriesSource') ?: - file("${powerRoot}/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/ForgeUnitRegistries.java") + file("${powerRoot}/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/ForgeUnitRegistries.java"), + poweradvantage_test_loader_source_sha256: explicitJar('forge64UnitLoaderSource') ?: + file("${powerRoot}/test-rig/unit-support/zone/moddev/mc/advantageworks/unit/Forge64UnitLoader.java") ] +def nativeUnitSupportDirectory = layout.buildDirectory.dir('native-unit-support-source') +def nativeUnitSupportSources = unitBootstrapSources.findAll { + key, source -> key != 'poweradvantage_test_bootstrap_source_sha256' +} +def prepareNativeUnitSupport = tasks.register('prepareNativeUnitSupport', Sync) { + from(nativeUnitSupportSources.values()) { into 'zone/moddev/mc/advantageworks/unit' } + into nativeUnitSupportDirectory + nativeUnitSupportSources.keySet().each { inputs.property(it, providers.gradleProperty(it)) } + doFirst { + nativeUnitSupportSources.each { key, source -> + String expected = committedPin(key, /[A-F0-9]{64}/) + if (!source.isFile() || MessageDigest.getInstance('SHA-256').digest(source.bytes) + .encodeHex().toString().toUpperCase(Locale.ROOT) != expected) { + throw new GradleException("Native unit-support source pin mismatch: ${source}") + } + } + } +} +sourceSets.test.java.srcDir(nativeUnitSupportDirectory) +tasks.named('compileTestJava') { dependsOn prepareNativeUnitSupport } + tasks.named('test', Test) { useJUnit() jvmArgs '-XX:ActiveProcessorCount=4' @@ -379,7 +434,7 @@ tasks.named('jar', Jar) { } } -// Forge 61 runs official names. The player jar needs no SRG remapping. +// Forge 64 runs official names. The player jar needs no SRG remapping. def releaseJar = tasks.register('releaseJar', Jar) { dependsOn tasks.named('classes'), 'verifyReleaseDependencies' archiveClassifier = null @@ -456,18 +511,18 @@ def expectedReleaseFiles = providers.provider { def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') def verifyPackFormats = { 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')) { - throw new GradleException('The mod root requires data format 94.1; Forge supplies hidden client pack metadata') + throw new GradleException('The mod root requires data format 101.1; Forge supplies hidden client pack metadata') } } def verifyRequiredBaseMetalsMetadata = { String metadata -> String normalized = metadata.replace('\r\n', '\n') - String declaration = 'modId="basemetals"\nmandatory=true\nversionRange="[3.1.2.121111,4.0.0)"\nordering="AFTER"\nside="BOTH"' + String declaration = 'modId="basemetals"\nmandatory=true\nversionRange="[3.1.2.2601021,4.0.0)"\nordering="AFTER"\nside="BOTH"' if (!normalized.contains(declaration) || normalized.readLines().count { it.trim() == 'modId="basemetals"' } != 1) { - throw new GradleException('Base Metals must be declared exactly once as REQUIRED on BOTH sides, ordered AFTER, in [3.1.2.121111,4.0.0)') + throw new GradleException('Base Metals must be declared exactly once as REQUIRED on BOTH sides, ordered AFTER, in [3.1.2.2601021,4.0.0)') } } @@ -483,22 +538,22 @@ tasks.register('verifyReleaseConfiguration') { '9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14') { throw new GradleException('Use the checksum-pinned Gradle 9.6.1 wrapper') } - if (project.mod_version != '2.2.16.121111') { + if (project.mod_version != '2.2.17.2601021') { throw new GradleException("Unexpected baseSteam 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,62)' + 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' || project.reobf != 'false') { 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 != 'basesteam' @@ -511,11 +566,11 @@ 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.basemetals_version != '3.1.2.121111' - || project.orespawn_version != '4.1.0.121111') { + if (project.basemetals_version != '3.1.2.2601021' + || project.orespawn_version != '4.1.1.2601021') { throw new GradleException('Unexpected development API versions') } if (project.poweradvantage_source_repository != 'MinecraftModDevelopmentMods/basePower') { @@ -524,7 +579,7 @@ tasks.register('verifyReleaseConfiguration') { String mainSource = file( 'src/main/java/zone/moddev/mc/basesteam/SteamAdvantage.java').getText('UTF-8') if (!mainSource.contains('MODID = "basesteam"') - || !mainSource.contains('VERSION = "2.2.16.121111"')) { + || !mainSource.contains('VERSION = "2.2.17.2601021"')) { throw new GradleException('baseSteam source identity changed') } String metadata = file('src/main/resources/META-INF/mods.toml').getText('UTF-8').replace('\r\n', '\n') @@ -556,10 +611,12 @@ def auditJar = { File jarFile, String property, String hashPin, String modId, St } } def manifest = new Manifest(zip.getInputStream(zip.getEntry('META-INF/MANIFEST.MF'))).mainAttributes + String expectedBuild = modId in ['basemetals', 'orespawn'] + ? '26.1.2-64.0.9' : "${project.minecraft_version}-${project.forge_version}" if (manifest.getValue('Implementation-Version') != expectedVersion - || manifest.getValue('Built-On') != "${project.minecraft_version}-${project.forge_version}" - || manifest.getValue('Built-On-Java') != '21') { - throw new GradleException("${modId} must be ${expectedVersion} for Forge ${project.forge_version} and Java 21") + || manifest.getValue('Built-On') != expectedBuild + || manifest.getValue('Built-On-Java') != '25') { + throw new GradleException("${modId} must be ${expectedVersion}, built on ${expectedBuild} with Java 25") } String metadata = zip.getInputStream(zip.getEntry('META-INF/mods.toml')).getText('UTF-8') if (!metadata.readLines().any { it.startsWith("modId=\"${modId}\"") }) { @@ -616,7 +673,7 @@ tasks.register('verifyPlayerDependencies') { dependsOn tasks.named('verifyCompanionPlayerDependencies') doLast { committedPin('poweradvantage_source_ref', /[a-f0-9]{40}/) - committedPin('poweradvantage_version', /\d+\.\d+\.\d+\.121111/) + committedPin('poweradvantage_version', /\d+\.\d+\.\d+\.2601021/) auditJar(explicitJar('powerAdvantagePlayerJar') ?: powerNativeJar, 'powerAdvantagePlayerJar', 'poweradvantage_player_sha256', 'basepower', project.poweradvantage_version, powerApi) } @@ -627,7 +684,7 @@ tasks.register('verifyNativeDevelopmentDependencies') { description = 'Resolves Forge and audits the exact development JARs.' dependsOn tasks.named('verifyCompanionDevelopmentDependencies') doLast { - committedPin('poweradvantage_version', /\d+\.\d+\.\d+\.121111/) + committedPin('poweradvantage_version', /\d+\.\d+\.\d+\.2601021/) committedPin('poweradvantage_source_ref', /[a-f0-9]{40}/) auditJar(powerAdvantageDeobfJar, 'powerAdvantageDeobfJar', 'poweradvantage_deobf_sha256', 'basepower', project.poweradvantage_version, powerApi) @@ -668,12 +725,52 @@ tasks.register('verifyReleaseDependencies') { def blocksAtlasPath = 'assets/minecraft/atlases/blocks.json' def itemsAtlasPath = 'assets/minecraft/atlases/items.json' def steamAtlasPaths = [blocksAtlasPath, itemsAtlasPath] -def steamDrillAtlas = [sources: [[type: 'single', resource: 'basesteam:entity/drill_bit']]] -def steamItemsAtlas = [sources: [[type: 'directory', source: 'items', prefix: 'items/']]] +def steamAtlasResources = [ + (blocksAtlasPath): [ + 'blocks/coal_boiler_front', 'blocks/electric_boiler_back', 'blocks/electric_boiler_front', + 'blocks/geothermal_boiler_side', 'blocks/geothermal_boiler_top', 'blocks/oil_tank_end', + 'blocks/platform_top', 'blocks/rock_crusher_front', 'blocks/rock_crusher_top', + 'blocks/steam_drill_bottom', 'blocks/steam_drill_top', 'blocks/steam_elevator_side', + 'blocks/steam_furnace_front', 'blocks/steam_furnace_top', 'blocks/steam_pipe', + 'blocks/steam_pump_front', 'blocks/steam_pump_side', 'blocks/steam_switch_off', + 'blocks/steam_switch_on', 'entity/drill_bit', 'materials/bolted_copper', + 'materials/bolted_iron', 'materials/iron', 'materials/steel_dark_hole' + ].collect { "basesteam:${it}".toString() } as Set, + (itemsAtlasPath): ['items/blackpowder_cartridge', 'items/steam_drill_bit', 'items/steam_governor'] + .collect { "basesteam:${it}".toString() } as Set +] +def verifyOwnedAtlas = { String path, Object atlas, String owner, Set registered, Closure exists -> + if (!(atlas instanceof Map) || atlas.keySet() != ['sources'] as Set + || !(atlas.sources instanceof List) || atlas.sources.isEmpty()) { + throw new GradleException("${owner} ${path} requires nonempty explicit native sprite sources") + } + Set sprites = [] as Set + atlas.sources.each { source -> + if (!(source instanceof Map) || source.keySet() != ['type', 'resource'] as Set + || source.type != 'minecraft:single' || !(source.resource instanceof String) + || !(source.resource ==~ /${owner}:[a-z0-9_\/-]+/) + || source.resource.contains('..')) { + throw new GradleException("${owner} ${path} must contain only owned, unaliased native single sprites") + } + String sprite = source.resource + if (!registered.add(sprite)) throw new GradleException("Duplicate sprite across native atlases: ${sprite}") + String spritePath = sprite.substring(sprite.indexOf(':') + 1) + if (path == itemsAtlasPath && !spritePath.startsWith('items/')) { + throw new GradleException("${owner} item atlas must not register block/material sprites: ${sprite}") + } + if (!exists("assets/${owner}/textures/${spritePath}.png".toString())) { + throw new GradleException("${owner} ${path} registers missing sprite ${sprite}") + } + sprites.add(sprite) + } + sprites +} def verifySteamAtlas = { String path, Object atlas -> - def expected = path == blocksAtlasPath ? steamDrillAtlas : steamItemsAtlas - if (!steamAtlasPaths.contains(path) || atlas != expected) { - throw new GradleException("Steam ${path} must contain only ${expected}; block and item sources must remain disjoint") + Set actual = verifyOwnedAtlas(path, atlas, 'basesteam', [] as Set) { String resource -> + file("src/main/resources/${resource}").isFile() + } + if (!steamAtlasPaths.contains(path) || actual != steamAtlasResources[path]) { + throw new GradleException("Steam ${path} must retain its exact owned native sprite inventory") } } def verifyPackagedAssetNamespaces = { ZipFile zip -> @@ -684,9 +781,14 @@ def verifyPackagedAssetNamespaces = { ZipFile zip -> if (foreignAssets.sort() != steamAtlasPaths.sort(false)) { throw new GradleException("${zip.name} must contain only ${steamAtlasPaths} outside ${steamAssets}; found ${foreignAssets}") } + Set registered = [] as Set steamAtlasPaths.each { path -> zip.getInputStream(zip.getEntry(path)).withCloseable { input -> - verifySteamAtlas(path, new JsonSlurper().parse(input, 'UTF-8')) + def atlas = new JsonSlurper().parse(input, 'UTF-8') + verifySteamAtlas(path, atlas) + verifyOwnedAtlas(path, atlas, 'basesteam', registered) { String resource -> + zip.getEntry(resource) != null + } } } } @@ -817,8 +919,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") } } } @@ -1016,7 +1118,7 @@ eclipse { synchronizationTasks 'verifyJavaSourceLayout', 'isolateEclipseProductionRuns' } -tasks.named('eclipseClasspath') { dependsOn tasks.named('processResources') } +tasks.named('eclipseClasspath') { dependsOn tasks.named('processResources'), prepareNativeUnitSupport } tasks.register('configureEclipseBuildship') { group = 'ide' @@ -1030,6 +1132,8 @@ tasks.register('configureEclipseBuildship') { 'connection.gradle.user.home' : gradle.gradleUserHomeDir.canonicalPath, 'connection.project.dir' : '', 'gradle.user.home' : gradle.gradleUserHomeDir.canonicalPath, + 'connection.java.home' : javaToolchains.launcherFor(java.toolchain) + .get().metadata.installationPath.asFile.canonicalPath, 'java.home' : javaToolchains.launcherFor(java.toolchain) .get().metadata.installationPath.asFile.canonicalPath, 'override.workspace.settings' : 'true' @@ -1159,7 +1263,7 @@ tasks.register('isolateEclipseProductionRuns') { classpath.children().addAll(runtimeIndex, runtimeEntries) contents = groovy.xml.XmlUtil.serialize(launchXml) - ['--cache', '--metadata', '--to-srg', '--to-obf'].each { option -> + ['--cache', '--metadata'].each { option -> String prefix = option + ' ' int valueStart = contents.indexOf(prefix) if (valueStart < 0) { @@ -1176,6 +1280,38 @@ tasks.register('isolateEclipseProductionRuns') { + '"' + value + '"' + contents.substring(valueEnd)) } } + def resolvedLaunch = parser.parseText(contents) + def programAttribute = resolvedLaunch.stringAttribute.find { + it.@key == 'org.eclipse.jdt.launching.PROGRAM_ARGUMENTS' + } + String program = programAttribute.@value + if (program.contains('--to-srg') || program.contains('--to-obf')) { + throw new GradleException("${name} must not remap the official Forge 64 runtime") + } + if (name == 'runClient.launch') { + File nativeJar = productionRuntime.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 assetIndex + new ZipFile(metadata).withCloseable { zip -> + assetIndex = new groovy.json.JsonSlurper().parse(zip.getInputStream( + zip.getEntry('minecraft/version.json'))).assetIndex + } + if (assetIndex.id.toString() != '30' + || assetIndex.sha1 != '49107cf4e5f961b086ac4f06f3c2c56142b023dc') { + throw new GradleException('Unexpected Minecraft 26.1.2 native asset index') + } + File assets = file(providers.gradleProperty('minecraftAssetsDir').getOrElse( + new File(System.getenv('APPDATA') ?: System.getProperty('user.home'), + '.minecraft/assets').path)) + // Resolve launch metadata without downloading assets during Eclipse generation. + programAttribute.@value = program.replace('{asset_index}', assetIndex.id.toString()) + .replace('{assets_root}', '"' + assets.canonicalPath + '"') + } + contents = groovy.xml.XmlUtil.serialize(resolvedLaunch) launch.setText(contents.replace('\r\n', '\n'), 'UTF-8') } } @@ -1216,6 +1352,10 @@ tasks.register('verifyEclipseProductionClasspath') { != gradle.gradleUserHomeDir.canonicalFile) { throw new GradleException('Eclipse Buildship does not use this wrapper and Gradle home') } + File javaHome = javaToolchains.launcherFor(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') + } String projectMetadata = file('.project').getText('UTF-8') if (!projectMetadata.contains('org.eclipse.buildship.core.gradleprojectnature') || !projectMetadata.contains('org.eclipse.buildship.core.gradleprojectbuilder')) { @@ -1229,16 +1369,16 @@ tasks.register('verifyEclipseProductionClasspath') { } def eclipseClasspath = new groovy.xml.XmlSlurper().parse(file('.classpath')) if (!eclipseClasspath.classpathentry.any { - it.@kind.text() == 'con' && it.@path.text().tokenize('/').last() == 'JavaSE-21' + it.@kind.text() == 'con' && it.@path.text().tokenize('/').last() == 'JavaSE-25' }) { - throw new GradleException('Eclipse must use the Java 21 production execution environment') + throw new GradleException('Eclipse must use the Java 25 production execution environment') } Properties jdt = new Properties() file('.settings/org.eclipse.jdt.core.prefs').withInputStream { jdt.load(it) } ['org.eclipse.jdt.core.compiler.codegen.targetPlatform', 'org.eclipse.jdt.core.compiler.source', 'org.eclipse.jdt.core.compiler.compliance'].each { key -> - if (jdt.getProperty(key) != '21') { - throw new GradleException("Eclipse ${key} must remain Java 21") + if (jdt.getProperty(key) != '25') { + throw new GradleException("Eclipse ${key} must remain Java 25") } } def processedResources = eclipseClasspath.classpathentry.findAll { @@ -1276,6 +1416,18 @@ tasks.register('verifyEclipseProductionClasspath') { String vmArguments = launch.stringAttribute.find { it.@key.text() == 'org.eclipse.jdt.launching.VM_ARGUMENTS' }.@value.text() + if (arguments.contains('--to-srg') || arguments.contains('--to-obf') + || arguments.contains('{asset_index}') || arguments.contains('{assets_root}')) { + throw new GradleException("${name} contains remap flags or unresolved native asset placeholders") + } + ['--cache', '--metadata'].each { option -> + if (!arguments.contains(option + ' ')) { + throw new GradleException("${name} is missing Slime Launcher argument ${option}") + } + } + if (name == 'runClient.launch' && !arguments.contains('--assetIndex 30 ')) { + throw new GradleException('Eclipse client must use the actual Minecraft 26.1.2 asset index') + } def legacyFile = vmArguments =~ /(?:^|\s)"-DlegacyClassPath\.file=([^"]+)"(?=\s|$)/ if (vmArguments.contains('-DlegacyClassPath.file=') && (!legacyFile.find() || !file(legacyFile.group(1)).isFile())) { @@ -1285,7 +1437,7 @@ tasks.register('verifyEclipseProductionClasspath') { || (name == 'runClient.launch' && !arguments.startsWith('--launcher-side client ')) || !vmArguments.contains('--add-opens java.base/java.lang.invoke=ALL-UNNAMED') || contents.contains('net.minecraftforge.userdev.LaunchTesting')) { - throw new GradleException("${name} must retain the native Forge 61 bootstrap and Java 21 module access") + throw new GradleException("${name} must retain the native Forge 64 bootstrap and Java 25 module access") } if (launch.booleanAttribute.find { it.@key.text() == 'org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE' @@ -1329,9 +1481,11 @@ 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}" ] + if (archives.any { it.name.startsWith('mappings_') }) { + throw new GradleException("${name} includes a mapping archive in an official-name runtime") + } List bootstrapPositions = bootstrapArtifacts.collect { artifact -> contents.indexOf(artifact) } @@ -1343,7 +1497,7 @@ tasks.register('verifyEclipseProductionClasspath') { String target = name == 'runClient.launch' ? 'forge_userdev_client' : 'forge_userdev_server' if (!arguments.contains("--launchTarget ${target}") || !arguments.contains("forge-${project.minecraft_version}-${project.forge_version}-metadata.zip")) { - throw new GradleException("${name} does not target the exact Forge 61 development runtime") + throw new GradleException("${name} does not target the exact Forge 64 development runtime") } } } @@ -1367,42 +1521,27 @@ tasks.register('verifyModelAssets') { new JsonSlurper().parse(jsonFile) } - Set atlasPrefixes = ['block/', 'item/'] as Set - Set singleSprites = [] as Set + Set registeredSprites = [] as Set steamAtlasPaths.each { String atlasPath -> File steamAtlas = new File(resourceRoot, atlasPath) if (!steamAtlas.isFile()) throw new GradleException("Missing native Steam atlas ${steamAtlas}") def atlas = new JsonSlurper().parse(steamAtlas) verifySteamAtlas(atlasPath, atlas) - singleSprites.addAll(atlas.sources.findAll { it.type == 'single' }.collect { it.resource }) - atlasPrefixes.addAll(atlas.sources.findAll { it.type == 'directory' }.collect { it.prefix }) + verifyOwnedAtlas(atlasPath, atlas, 'basesteam', registeredSprites) { String resource -> + new File(resourceRoot, resource).isFile() + } } - Set itemAtlasPrefixes = ['item/', 'items/'] as Set Set powerAdvantageAssets = new ZipFile(powerAdvantageDeobfJar).withCloseable { zip -> - Map> sharedPrefixes = [:] steamAtlasPaths.each { atlasPath -> def atlasEntry = zip.getEntry(atlasPath) if (atlasEntry == null) throw new GradleException("basePower is missing native atlas ${atlasPath}") zip.getInputStream(atlasEntry).withCloseable { input -> def atlas = new JsonSlurper().parse(input, 'UTF-8') - if (atlasPath == itemsAtlasPath) verifySteamAtlas(atlasPath, atlas) - // Power's directory sources preserve the existing texture IDs across namespaces. - Set prefixes = atlas.sources.findAll { - it.type == 'directory' && it.source instanceof String && it.prefix == it.source + '/' - }.collect { it.prefix } as Set - sharedPrefixes[atlasPath] = prefixes + verifyOwnedAtlas(atlasPath, atlas, 'basepower', registeredSprites) { String resource -> + zip.getEntry(resource) != null + } } } - Set collisions = sharedPrefixes[blocksAtlasPath].intersect(sharedPrefixes[itemsAtlasPath]) - collisions.retainAll(['blocks/', 'materials/']) - if (!collisions.isEmpty()) { - throw new GradleException("basePower shadows block/material directories in the item atlas: ${collisions}") - } - if (!sharedPrefixes[blocksAtlasPath].containsAll(['blocks/', 'materials/', 'items/'])) { - throw new GradleException('basePower block atlas must retain legacy blocks/, materials/ and generated-item items/ sources') - } - sharedPrefixes.values().each { atlasPrefixes.addAll(it) } - itemAtlasPrefixes.addAll(sharedPrefixes[itemsAtlasPath]) zip.entries().collect { it.name } as Set } def verifyReference = { String reference, String assetType, String sourcePath -> @@ -1421,13 +1560,9 @@ tasks.register('verifyModelAssets') { throw new GradleException( "${sourcePath} references missing ${assetType} asset ${reference}") } - if (assetType == 'textures' && !singleSprites.contains(reference) - && !atlasPrefixes.any { path.startsWith(it) }) { - throw new GradleException("${sourcePath} references unstitched blocks atlas texture ${reference}") - } - if (assetType == 'textures' && new File(sourcePath).toPath().startsWith(new File(modelRoot, 'block').toPath()) - && itemAtlasPrefixes.any { path.startsWith(it) }) { - throw new GradleException("${sourcePath} references an item-atlas texture from a block model: ${reference}") + // Native 26 Material stores only a sprite ID; ownership and disjointness were checked above. + if (assetType == 'textures' && !registeredSprites.contains("${namespace}:${path}".toString())) { + throw new GradleException("${sourcePath} references unregistered native sprite ${reference}") } } diff --git a/docs/PORT2612-STATUS.md b/docs/PORT2612-STATUS.md new file mode 100644 index 0000000..aeddc2b --- /dev/null +++ b/docs/PORT2612-STATUS.md @@ -0,0 +1,405 @@ +# Steam on 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/basesteam-26.1.2-port. Public file IDs +and final source pins must come from the actual released dependencies; no local +pin or guessed ID satisfies the hosted gate. Earlier checkpoint notes, old holds +and qualification receipts below remain historical and bound to their original +inputs. They are not rewritten as proof of a later public-pin rebuild. + +## 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 actual public jar still declares Built-On 26.1.2-64.0.9 and Java 25; +those provenance checks remain unchanged. Its hash and manifest were checked +locally against both the supplied public download and the parent's frozen copy. + +The parent handed off exact Power source +d0bd0bdbde6c499e800e5a5b4ecc7f9ac63d2055, player/development SHA-256 +23ABCC1FEFDAA6988BF709437EB165C8948C757E773ADAB738092EB93CA8D2B0. +Those bytes and all four unit-helper inputs were verified and frozen locally. +The clean full release suite executes 45 tasks and 276 tests across 47 suites, +with zero failures, errors or skips. Javadoc, exact dependencies, archive, +checksum and Eclipse production-classpath audits pass. Fresh ECJ compiles +60 main, 54 test and three GameTest sources and passes the complete same +276-test inventory, including the three inventory-rejection controls. + +Independent path-with-spaces warm and genuinely empty-cache exports both pass +all 45 tasks and reproduce player, sources and Javadoc archives exactly. +The cold export downloads Gradle and regenerates the native Forge SDK without +seeding its cache. Two failed cache-only export setups and the first cold +attempt's Windows CreateProcess error 267 remain unchanged. The latter had a +268-character decompiler working directory; a new, shorter cache path inside +this checkout passed in 6m33s. Both source and cache paths contain spaces. + +Final player/development SHA-256 remains +4A4E7D8C3704DCF17F46EBB55A7A908A6E1B547FCAB93E3B6782EE32E49301CB. +All 333 player entries are identical to candidate09. The profile12 actor/order +campaign is separate; no new game launch, combined-family or manual-acceptance +result is claimed by these child build receipts. +Historical candidate receipts below remain bound to their original inputs. +New Steam evidence is kept under +D:/PowerAdvantage/_verification/base-2612-port-20261112/resume-orespawn411-20261112/steam. +The parent owns the combined profiles and broader family campaigns. The +separately assigned profile12 actor/order task completes all nine routes: +original110, maintained112 and native1211-stopped-source pass strictly; all six +dimension routes retain PASS_WITH_NATIVE_DIFFERENCE for independently proved +native wolf health/max-health scaling. Their original strict differences and +the no-Power native control remain intact; all other fields match strictly. + +Both actual machine orders and both strict actor-inclusive orders pass. Each +actor order preserves the two saved musket witnesses through five semantic +phases, rejects all 14 entity mutations and passes three stopped raw audits +matching 15 entities each. Ten guard and eleven woven-code mutations reject +per actual-order campaign. Six strict profile-boot audits and all 39 game +process closures pass; assigned ports 29240-29249 are free. Instrumented +order qualificationAllowed remains false. + +The first order attempt was correctly rejected before Minecraft because the +parent regenerated its final Eclipse classpath after sidecar01 was built. +That failure is preserved. Sidecar02 binds the final classpath with no guard +relaxation; no parent source or classpath was edited by this task. +The assigned handoff is actors-orders/final-owned-handoff-01.json under the +resume directory, SHA-256 +53E74959A4B3DF78F92652FB4C79C77BC185E326F23F525CB12E1CDBB2B48020. +Source fixtures, ordinary seed12, frozen family jars and all sealed inputs are +unchanged. These assigned scopes do not claim human or global family acceptance. + +## Baseline And Toolchain + +The checkout starts from released MMD source +ce1601aa6c9f13649ac8ee0b05c4d6c01ef7406e on the seeded master-26.1.2 branch. +The active local branch is feature/basesteam-26.1.2-port. The 1.21.11 source and +publisher remain untouched. + +Target: baseSteam 2.2.17.2601021, Forge 64.1.5, MCP 20260409.101008, +ForgeGradle 7.0.34 and Gradle 9.6.1, official runtime names, reobf=false. +Temurin 25.0.3+9 runs Gradle, compilation, Minecraft, Mavenizer and Slime. +The installer SHA-256 is +6E01323837051176762BD466340A601DA122F179FE6A00AC616E05429AE07648. + +The sealed Mavenizer and rule-manifest hashes are BB0DACB3 and 21D1F598; +the full checksums are enforced by gradle/forge64-mavenizer.gradle. The exact +64.1.5 SDK generated successfully without target source-repair rules. +Two focused toolchain regressions failed against the inherited build and pass +after the configuration change. These are not a production compile or a full +unit-test result. + +## Dependency Gate + +Current local integration inputs are Power 2.4.16.2601021 source +b947b4b81f3508fa220a212b41157ed2f801909f/player 5309E7C4, the approved Base +Metals 3.1.2.2601021 hook source 331fa46914f854db71a8c33500cc559a8878b7d3/ +player 67862388, and the exact parent fixture08 jar and three audited Java +sources. The generated unit-support copies are test-only, hash-checked and never +packaged or committed. Old Power/BM jars are not target inputs or substitutes. + +The first eight-task default-path releaseJar/deobf build passes all exact +dependency audits and produces identical player/deobf bytes: +B5F2C2C310240FB2E052E7C81E2BB35940C4045643D0F7E2BE1B484073E22999. +An immutable copy is held under build/native2612-first-candidate/held. This is +available for DEVELOPMENT-ONLY profile checks, not yet a family-qualified jar. + +Released OreSpawn 4.1.0.2601021 remains unchanged, source +c45ea2a111520ef904e994dd8dcd90babfcef29b, SHA-256 +6D5CE2017C3314968083D140309E2F1A54BC239AC65C48944C995E9ECF937948. +Both BM and OS truthfully record their original 26.1.2-64.0.9 builds. Compatibility with +64.1.5 must be proved by new runs, not by changing that manifest or its bytes. + +## Native API Work + +The actual 64.1.5 SDK confirms Recipe.assemble takes only the input, the +renderer camera lives under renderer.state.level, and pack formats are client +84.0/server 101.1. Source/resource changes preserve all logical IDs and payloads. +The root pack keeps the native data-only range while Forge supplies the hidden +client aggregate. Both real native resource paths and negative controls remain +required; the earlier 1.21.11 receipts are not relabelled. + +Forge 64's actual RuntimeDistCleaner reproduced the client type and common-menu +method annotation failures. The three helper type markers and eleven method +markers are removed while retaining all legacy decoration signatures and the +actual distribution-guarded renderer registration. Native cleaner and linkage +checks remain distinct from actual dedicated-server/client startup proof. + +Five native trade definitions retain all ten profession/level placements and +legacy inclusive ranges, seven uses, one XP and zero discount. The cleric's +native enchant_with_levels modifier carries the exact generated level as an +additional trade cost. Ten deliberate source-contract corruptions are rejected; +native codec and generated-offer witnesses are recorded in the current results below. + +## Candidate01 Checkpoint + +The first full native unit run executed 258 tests with seven failures. The +unchanged player sources then passed all 258 after the test fixtures used the +native post-registration component-binding lifecycle and the pack-format oracle +was updated. Both XML inventories and the original failure logs are retained. + +After six complete-inventory regressions were added, all 264 tests passed with +zero failures, errors or skips. Fresh ECJ 3.46.0 on Temurin 25.0.3+9 compiled +61 main, 54 test and three GameTest sources, then ran all 264 tests without +failures, ignored tests or assumptions. Its ASM scan compares every compiled +JUnit class/method against Gradle XML, including tests outside conventional +package/filename filters. Only the three reviewed existing Parameterized suites +use JUnit's parameter discovery; other custom runners are rejected. Actual +omitted-suite, omitted-method and stale-method XML controls all fail the guard. +Evidence: build/ecj-native2612-full-inventory-02/receipt.json. + +The native Eclipse audit passes with official-name cache/metadata arguments, +no remap flags or mapping archive, literal asset index 30 and assets root, +JavaSE-25 and the pinned Buildship connection.java.home. Eclipse generation does +not download client assets. The original obsolete mappings-archive requirement +failure is preserved in build/native2612-qualification/third-full-unit-eclipse.log. + +The first actual source GameTest launch ran the four named Steam cases: three +passed and the native-resource case failed on an added number-provider negative +control. All 1,280 generated trade offers had already passed their exact range, +enchantment and correlated-cost checks. The native NumberProviders codec has an +explicit untyped uniform fallback when min/max remain present. That case is now +an exact round-trip witness, while an unknown provider without fallback bounds +must be rejected. Three focused toolchain/codec tests pass and the updated +GameTest source compiled; its runtime rerun was not yet claimed at that checkpoint. +The compiled full inventory then contained 265 tests, not a new full-run result. + +The original four-case XML and logs remain under +build/steam-gametest-native2612-first-20261112-01 and +build/native2612-qualification/gametest-first.log. No failure is overwritten or +relabelled. The source GameTest launch is not a frozen player-jar proof. + +Parent found an obsolete SRG InventoryLocks field lookup in Power candidate01. +Production qualification, packaged launches and endurance were held for the +repaired candidate02 source/hash handoff. Steam's held B5F2 player bytes were +unchanged; no further production source or resource edits were needed for these +fixture, adapter and verification changes. No local port commit existed at that checkpoint. + +## Candidate02 Inputs + +The repaired Power source is 934bf3fa287c17f38a24e89f754087722c2c0ad0, +player SHA-256 F39FF5EC90706AE9109D29A254FDBCC5CA47D333E8589BD33FF98061EAF0FAB7. +The fixture08 bootstrap jar and all three source checksums are unchanged and +were reverified before repinning. Base Metals and OreSpawn retain the exact +inputs above. The old property file and player jar are held under +build/native2612-candidate02-qualification/held-inputs. Candidate02-bound child +results are recorded separately; candidate01 failures are not relabelled. + +## Candidate05 Child Results + +Power source at this historical checkpoint: e49a2e947e066a0f0e497982c2d5db596dfe11d9. +Power player: 3D9ACF8B86DD1B443B138C61E5E1C5BAAC3059D83EED83B8E6E927BC501CA446. +Steam player and deobf: +51DE7B6C58DA2DF5116352364B34E5B12FE5A22AA1DD2A487D472EF2E83A0AC3. +All bootstrap/registry/loader source pins are unchanged. This checkpoint used +BM source 13d4c4e38bf74d76f2f69f307dc55d13a81e8674/player 4911669B and the +unchanged OS input. This is local source integration, not publication. + +Actual family operations exposed a nullable crafting-remainder NPE in the coal +boiler and blast furnace. Four actual-method cases failed with that NPE before +the fix; all nine focused fuel tests pass afterward. Single coal becomes EMPTY, +larger stacks consume exactly one per call, valid lava fuel returns one fresh +bucket, and water buckets remain nonfuel. Remainders are captured before replacement. + +APOCALYPTIC common setup also attempted to build ItemStacks solely for nine +obsolete no-op crushing calls. The internal bridge and setup call are removed. +The public compatibility methods remain, and all nine authoritative JSON files, +conditions, inputs, output identities and quantities are byte-identical to B5. +The initial lifecycle-test failure expecting the removed class is retained; +its replacement requires that neither the class nor the unsafe setup call exists. + +The final default-path release run executes 46 tasks and 273 tests across 47 +suites, with zero failures, errors or skips. Dependency, archive, checksum, +resource, recipe, Javadoc and Eclipse metadata audits pass. Fresh ECJ compiles +60 main, 54 test and three GameTest sources and executes the same complete 273-test +inventory with zero failures, ignored tests or assumptions. Its omitted-suite, +omitted-method and stale-method negative controls remain strict. + +Each of NORMAL, APOCALYPTIC and TECH_PROGRESSION passes the four named native +GameTests, ten villager-trade placements, 1,280 generated offers and five trade +codec negatives. APOCALYPTIC additionally proves nine loaded salvage recipes, +native recipe-codec round trips, exact outputs and unrelated-input negatives; +the other two modes require all nine salvage recipes to be absent. + +The actual native JVMs assert explicit four-processor/1536-MiB caps and absence +of JAVA_TOOL_OPTIONS, JDK_JAVA_OPTIONS and _JAVA_OPTIONS. ECJ and the packaged +startup/save/stop/reopen receipt record those variables as absent. Both packaged +processes save a nonempty world, stop with exit zero and are closed. These are +child lifecycle checks, not the family's operating-machine/endurance proof. + +An isolated path-with-spaces verification checkout executes the same full +46-task/273-test suite and reproduces player, source and Javadoc jars exactly. +It reuses the checksum-verified native engine/dependency cache and makes no +unseeded cold-download claim. All failed and intermediate B5/7E9 receipts remain. +Current evidence is under build/native2612-apocalyptic-repair, the ECJ receipt is +build/ecj-native2612-frozen51de-final-02/receipt.json, and the packaged receipt is +evidence/packaged-server-native2612-frozen51de-final-01/receipt.json. + +## Candidate06 Atlas And Dependency Checks + +Power source at this historical checkpoint is 0ac874de66f792c6efca5bccd0536b1b113fb786, player +618C3F96736392CE2853575F5A6A076EAC2A9B9F76F9303D86DEF58E418857D0. +The BM source is 331fa46914f854db71a8c33500cc559a8878b7d3, player +6786238835D4D8A653C769F3B0E61D226959146300E17523799CFDF17540FD19. +OS and the three frozen unit-helper sources remain unchanged. + +Steam player and deobf are +4A4E7D8C3704DCF17F46EBB55A7A908A6E1B547FCAB93E3B6782EE32E49301CB. +Only the two atlas JSON entries differ from 51DE; all compiled classes and the +manifest are byte-identical. Steam registers 24 block/entity/material sprites +and three item sprites as explicit native singles. It does not register foreign +namespaces or global directory sources. IDs are disjoint across both atlases. + +The obsolete directory-only test and build audit both failed against the new +resources before their validators were updated. The new checks validate exact +source shape, ownership, physical files and disjoint IDs before combining the +native sprite inventory. Actual Forge 64 SpriteSources and Material codecs, +resource managers and source loaders prove all model references resolve; the +native Material identifies a sprite rather than a fixed block/item atlas. +Directory, alias, foreign namespace, missing-file, duplicate, malformed-type and +cross-atlas controls remain strict, as do missing/wrong-hash Power inputs. + +The default-path full release suite passes 46 executed tasks and 276 tests with +zero failures, errors or skips. Fresh ECJ compiles all three Eclipse scopes and +runs the complete same inventory with zero ignored tests or assumptions. +Evidence: build/native2612-atlas-context-qualification and +build/ecj-native2612-candidate06-atlas-full-01/receipt.json. The earlier 05 +receipts remain historical and are not qualification of candidate06. + +The isolated path-with-spaces verification build also passes all 46 tasks and +276 tests. All three release jars match the ordinary build byte-for-byte. It +reuses the verified native engine/dependency cache, not a new cold download. +Each recipe mode passes the four named source GameTests and the ten native +trade placements with 1,280 generated offers. APOCALYPTIC retains all nine +salvage recipe codec/output/negative checks; NORMAL and TECH_PROGRESSION require +their absence. The current packaged Steam jar passes a separate two-process +startup, save, stop and reopen check. JVM caps are explicit command-line options +and all three inherited JVM option variables are absent in these receipts. + +The five assigned finite campaigns were rerun against the separately frozen +candidate06 profile 9F4C019B, not relabelled from 05. Harvest checks 271 cases +before and after restart. Lifecycle covers twelve movement/mining cases, five +physical chunk cycles and saved-progress/legacy-read/changed-target controls. +Partial networks and bridge reconnection cover all three power lanes. Extended +drills cover eighteen routes, 72 moves, four partial-unload cycles, both reload +orders, eighteen blocked/energy/redstone cases and 21 restart cases. All pass. +Harvest, lifecycle and extended include cold JVM restarts; partial and bridge +are physical-unload campaigns without an additional JVM restart. + +The read-only raw audit covers 471 JSON packets, native ticket geometry, all +nine real JVMs closed and unchanged profile, six jars, seed and imported oracle +sources. Seventeen adapter unit tests pass. The scoped finite receipt is +_verification/base-2612-port-20261112/native2612-machines-networks-06/ +finite-candidate06-handoff-01.json, SHA-256 +7536216856051A46CD22CF6F12AF39CD12D6CF475C37F2AC27DF405A7B1383D2. +These results do not claim global family qualification or manual acceptance. + +## Candidate07 Dependency Qualification + +Power source is 9f6ffb30a14efe4360fee0cbdfa52dc8090a9819, player +05031F1A3DCF25A405D9222E65619F7173C37CCF57B76308B72F741CA0E5198C. +The bootstrap jar and all three helper-source hashes are unchanged and were +reverified before retargeting. BM and OS keep their exact candidate06 pins. +This update changes dependency pins and notes only, not Steam gameplay code or +resources. New child qualification is recorded separately from candidate06. + +The default-path release build executes all 46 tasks and all 276 unit tests +with zero failures, errors or skips. Native dependency, artifact, checksum, +resource, Javadoc and Eclipse audits pass against the new Power bytes. Fresh +independent ECJ compiles 60 main, 54 test and three GameTest sources and runs +the full 276-test inventory with zero failures, ignored tests or assumptions. +The complete compiled-inventory guard remains enabled. + +The isolated path-with-spaces build also executes 46 tasks and 276 tests and +reproduces all three release archives exactly. Its verified native engine and +dependency cache are reused; this is not an unseeded cold-download claim. +Player SHA-256 remains +4A4E7D8C3704DCF17F46EBB55A7A908A6E1B547FCAB93E3B6782EE32E49301CB, +sources FFA80BE16EF72CAEF38B16B6A79FB46952C7369E2F1E3770F2A184E805F6A03B, +and Javadoc 4C9BFA077DF88E78C480ED058FF8C02F85C31DFB1834E78D9DA7B1777D6DE085. +All 333 player entries, including every class, resource and manifest, match06. +The fresh pin-bound receipts are under build/native2612-candidate07-qualification +and build/ecj-native2612-candidate07-full-01/receipt.json. + +No new GameTest, packaged-world or physical campaign is claimed for07 here. +The five physical campaigns remain held until the parent supplies the sealed07 +profile. The06 receipts remain unchanged historical evidence. + +## Candidate08 Dependency Qualification + +Power source is e565f99284de5e6c7860d378abc43b96d9faa5ee, player +CC94656A75B3E6311E5F9F7CB4E4E60014B94A87665743D5A5D3C84CEA849720. +The bootstrap and three helper-source hashes are unchanged and were reverified. +BM and OS pins remain unchanged. Only dependency pins and notes change in Steam. + +The fresh default-path full release run executes all 46 tasks and 276 tests, +with zero failures, errors or skips. Dependency, artifact, checksum, resource, +Javadoc and Eclipse metadata audits pass. Fresh independent ECJ compiles all +60 main, 54 test and three GameTest sources, then executes the complete 276-test +inventory with zero failures, ignored tests or assumptions. + +An isolated path-with-spaces verification build also passes 46 tasks and 276 +tests and reproduces the player, sources and Javadoc archives byte-for-byte. +It reuses the verified native engine/dependency cache; no unseeded cold-download +claim is made. All three hashes remain the exact values in the07 child results, +including player +4A4E7D8C3704DCF17F46EBB55A7A908A6E1B547FCAB93E3B6782EE32E49301CB. +All 333 player entries match07: no class, resource or manifest changed. +Evidence is under build/native2612-candidate08-qualification and +build/ecj-native2612-candidate08-full-01/receipt.json. These are fresh pin-bound +child checks, not relabelled07 tests or an08 family/world qualification. + +The parent found a real provider-schema regression in07 legacy generation. +The five07 finite campaigns finished normally with 470 raw JSON packets and +nine closed JVMs, but those scoped passes do not qualify07 or waive that failure. +All07 results and failures remain preserved. No further07 campaign or preseal +is permitted. The08 physical reruns await both its frozen profile and the short +legacy-generation gate. + +## Candidate09 Dependency Qualification + +Power source is b947b4b81f3508fa220a212b41157ed2f801909f, player +5309E7C433933F1426543310611292BF80388A476836139F0117E6A7E5C9AFC1. +The bootstrap and three helper-source hashes were reverified and are unchanged. +BM and OS retain their exact existing pins. Steam changes only its dependency +pin and notes, not production code, resources, version or gameplay. + +The default-path full release run passes all 46 executed tasks and 276 unit +tests with zero failures, errors or skips. Dependency, resource, artifact, +checksum, Javadoc and Eclipse metadata audits pass. Fresh independent ECJ +compiles all 60 main, 54 test and three GameTest sources, verifies the complete +compiled JUnit inventory, and executes all 276 tests with zero failures, +ignored tests or assumptions. + +The isolated path-with-spaces build also passes 46 tasks and 276 tests and +reproduces all three release archives exactly. The verified native engine and +dependency cache are reused; no unseeded cold-download claim is made. Player +SHA-256 stays +4A4E7D8C3704DCF17F46EBB55A7A908A6E1B547FCAB93E3B6782EE32E49301CB. +Every one of its 333 entries matches08, including all classes, resources and +the manifest. Sources and Javadoc retain the same exact hashes recorded above. +Evidence: build/native2612-candidate09-qualification and +build/ecj-native2612-candidate09-full-01/receipt.json. + +The five08 physical campaigns subsequently passed with 469 audited raw JSON +packets, nine closed JVMs and all 34 imported oracle files unchanged. Those +bounded results remain historical. The parent's08 released110/112 first-boot +quartz-import failures are preserved; restart passes do not waive them. +New09 physical campaigns await the parent's frozen profile and early gate. +The optional Mineralogy assessment is read-only. Core and versioned endurance +guards still require exactly six jars. The three Mineralogy-only scopes remain +explicitly SKIPPED; no full interoperability claim includes them. + +## Remaining Qualification + +The parent owns the combined family graphical, operating-machine, migration and +endurance campaigns, including every machine UI and the unified creative tab. +Those receipts must bind the frozen family jars; no global pass or manual-only +status is inferred from this child handoff. No publication is authorized. diff --git a/gradle.properties b/gradle.properties index 56111c0..4f6d766 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,60 +6,61 @@ org.gradle.caching=true org.gradle.parallel=false net.minecraftforge.gradle.merge-source-sets=false -minecraft_version=1.21.11 -minecraft_version_range=[1.21.11] -forge_version=61.1.0 -# Verified against the parent's authentic Forge 61 runtime seed. -forge_installer_sha256=8BD1724EAEAF26F51DACFB7FDC46D79E9B640705B70D5F0019ED38907145EB48 -forge_server_shim_sha256=09008DB9881E35E62669865F3B7420741EDB1A52BC59C67A5FA03D24C6C8E6E3 -forge_server_sha256=8D997159003977A22DB8E4EA6DAAB6CBB0E76F752A196A1821C74734A1CF71D9 -forge_universal_sha256=101041931319D508D94EF9ED6643CA4374EA3950237443301F7AA5A3B2631CC4 -forge_version_range=[61.1.0,62) -loader_version_range=[61,62) +minecraft_version=26.1.2 +minecraft_version_range=[26.1.2] +forge_version=64.1.5 +# Verified against the authentic, installed Forge 64.1.5 runtime. +forge_installer_sha256=6E01323837051176762BD466340A601DA122F179FE6A00AC616E05429AE07648 +forge_server_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 +reobf=false 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=1781373 mod_id=basesteam mod_name=baseSteam mod_license=GPL-3.0-only -mod_version=2.2.16.121111 +mod_version=2.2.17.2601021 mod_group=zone.moddev.mc.basesteam mod_authors=DrCyano mod_description=Steam-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_source_repository=MinecraftModDevelopmentMods/basePower -# Exact merged source and independently verified public Forge 1.21.11 release. -poweradvantage_source_ref=9b0043483d79d3e575895f12aeb0dde7ef6c4890 -poweradvantage_public_file_id=9566998 -poweradvantage_deobf_sha256=C98D2CF163BE2480D9AFB0023BBD2D92D401AE0D50BEF73191235BF3F83774A7 -# Forge 61 uses the same native jar for development and packaged runtime. -poweradvantage_player_sha256=C98D2CF163BE2480D9AFB0023BBD2D92D401AE0D50BEF73191235BF3F83774A7 -poweradvantage_test_bootstrap_sha256=FDE28801E41787E05212F3DEF7FAF34C6859402A22B71866D15B468C300DA883 -poweradvantage_test_bootstrap_source_sha256=CDE40D09A40DB95F293BC56723E70B321C205850275696BEBFC2A575A8B7FE48 +# Exact merged release inputs; public downloads are checked before hosted builds. +poweradvantage_source_ref=a684e71e5397cb5a1a4879dcf0b6a2be0815e85d +poweradvantage_public_file_id=9576332 +poweradvantage_deobf_sha256=23ABCC1FEFDAA6988BF709437EB165C8948C757E773ADAB738092EB93CA8D2B0 +poweradvantage_player_sha256=23ABCC1FEFDAA6988BF709437EB165C8948C757E773ADAB738092EB93CA8D2B0 +poweradvantage_test_bootstrap_sha256=088FC137BEBB9B2E75C596DE868499E6099CF228BECFCF07EAE36791245FD1B6 +poweradvantage_test_bootstrap_source_sha256=D4B72FD65D9F8549B142507C6446E3B1E047B637784AF9FA74E8F28413E14A51 poweradvantage_test_registry_source_sha256=01DDE50C9874C2DC501BE2F699CD8D16D6974FACFAAC1B803434CA8C64227ABF +poweradvantage_test_loader_source_sha256=455CA9403D9CBEA72E7D055966D6CDC822E634418C14B3CAD0500588DEE35E5B -basemetals_version=3.1.2.121111 -basemetals_source_commit=6f4c44e4d0a68bca3838fb025c09cddfaaa08fe8 -basemetals_public_file_id=9566738 -basemetals_deobf_sha256=598D3FB0FF956EBCC67FE477A01CE393C75D0013AF0CE00A79E2072B63E9F2BA -basemetals_player_sha256=598D3FB0FF956EBCC67FE477A01CE393C75D0013AF0CE00A79E2072B63E9F2BA +basemetals_version=3.1.2.2601021 +basemetals_source_commit=5e708468302a7fb3b35aca09d5f832f8882f88f3 +basemetals_public_file_id=9576007 +basemetals_deobf_sha256=6786238835D4D8A653C769F3B0E61D226959146300E17523799CFDF17540FD19 +basemetals_player_sha256=6786238835D4D8A653C769F3B0E61D226959146300E17523799CFDF17540FD19 -orespawn_version=4.1.0.121111 -orespawn_source_commit=ecad75b24309a344f712d898d717c6a8823fc9f2 -orespawn_public_file_id=9268464 -orespawn_deobf_sha256=DE39929057288CEA110196C9E9D708E129B80BC360A2E53976337FFA6A84A67B -orespawn_player_sha256=DE39929057288CEA110196C9E9D708E129B80BC360A2E53976337FFA6A84A67B +orespawn_version=4.1.1.2601021 +orespawn_source_commit=101fd12b598934c353e417c57edc5c2bbefd5f53 +orespawn_public_file_id=9572926 +orespawn_deobf_sha256=810284ECB32D5249381F246498CEFA57E70FFF7CCC40110F799FC276CBC42BF7 +orespawn_player_sha256=810284ECB32D5249381F246498CEFA57E70FFF7CCC40110F799FC276CBC42BF7 diff --git a/gradle/forge61-mavenizer.gradle b/gradle/forge64-mavenizer.gradle similarity index 60% rename from gradle/forge61-mavenizer.gradle rename to gradle/forge64-mavenizer.gradle index f6bad96..16440b4 100644 --- a/gradle/forge61-mavenizer.gradle +++ b/gradle/forge64-mavenizer.gradle @@ -1,24 +1,24 @@ import java.security.MessageDigest import org.gradle.jvm.toolchain.JvmVendorSpec -// Use OreSpawn's checksum-verified fixture for the native Forge 61 toolchain. +// Use OreSpawn's checksum-verified fixture; Forge 64 needs no source repair rules. def tool = 'ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar' -def defaultJar = "../../../../MinecraftMineralogy/OS12111/OreSpawn/${tool}" -def fixture = file(providers.gradleProperty('forge61MavenizerJar').getOrElse(defaultJar)) -def expected = 'B36CA046DB72E30F27BD38E02B4AE34274ADB51AE1D65BAF9E1D07763755682E' +def defaultJar = "../../../../MinecraftMineralogy/OS2612/OreSpawn/${tool}" +def fixture = file(providers.gradleProperty('forge64MavenizerJar').getOrElse(defaultJar)) +def expected = 'BB0DACB3DEB2A711CA67DD402D5FDAB5A9847C37C7B9B596DEC7CF37378FA085' if (!fixture.isFile()) { - throw new GradleException("Forge 61 requires OreSpawn's Mavenizer fixture: ${fixture}. " - + "Use -Pforge61MavenizerJar= for another checkout layout.") + throw new GradleException("Forge 64 requires OreSpawn's Mavenizer fixture: ${fixture}. " + + "Use -Pforge64MavenizerJar= for another checkout layout.") } 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 rulesHash = '88F2EE4C6B7C903EB1C7D693C0BEEFC0EFC601C1023FB197F7CBA187B1142DB7' +def rulesHash = '21D1F5984E735BCEA1B1EFB9F965655796B39538FC91AAED1C3474004710423D' if (!rules.isFile() || MessageDigest.getInstance('SHA-256').digest(rules.bytes) .encodeHex().toString().toUpperCase(Locale.ROOT) != rulesHash) { - throw new GradleException("Forge 61 Mavenizer source rules mismatch: ${rules}") + throw new GradleException("Forge 64 Mavenizer source rules mismatch: ${rules}") } fgtools.configure('mavenizer') { diff --git a/src/gametest/java/zone/moddev/mc/basesteam/gametest/NativeVillagerTradeProof.java b/src/gametest/java/zone/moddev/mc/basesteam/gametest/NativeVillagerTradeProof.java new file mode 100644 index 0000000..d3156bf --- /dev/null +++ b/src/gametest/java/zone/moddev/mc/basesteam/gametest/NativeVillagerTradeProof.java @@ -0,0 +1,143 @@ +package zone.moddev.mc.basesteam.gametest; + +import com.google.gson.JsonObject; +import com.mojang.serialization.JsonOps; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import net.minecraft.core.BlockPos; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.Registries; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.tags.EnchantmentTags; +import net.minecraft.tags.TagKey; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.EnchantmentHelper; +import net.minecraft.world.item.trading.VillagerTrade; +import net.minecraft.world.level.storage.loot.LootContext; +import net.minecraft.world.level.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import zone.moddev.mc.basesteam.SteamAdvantage; +import zone.moddev.mc.basesteam.init.Items; + +final class NativeVillagerTradeProof { + private NativeVillagerTradeProof() {} + + static void verify(GameTestHelper helper) { + var level = helper.getLevel(); + var registries = level.registryAccess(); + var registry = registries.lookupOrThrow(Registries.VILLAGER_TRADE); + var ops = registries.createSerializationContext(JsonOps.INSTANCE); + Map> placements = Map.of( + "armorer/level_1", List.of("steam_governor"), + "toolsmith/level_1", List.of("steam_governor", "blackpowder_cartridge", "musket"), + "weaponsmith/level_1", List.of("steam_governor", "blackpowder_cartridge", "musket"), + "librarian/level_1", List.of("steam_governor", "steam_pipe"), + "cleric/level_3", List.of("enchanted_musket")); + int placementCount = 0; + for (var placement : placements.entrySet()) { + var tag = registry.getOrThrow(TagKey.create(Registries.VILLAGER_TRADE, + Identifier.withDefaultNamespace(placement.getKey()))); + Set own = tag.stream().map(holder -> holder.unwrapKey().orElseThrow().identifier()) + .filter(id -> id.getNamespace().equals(SteamAdvantage.MODID)) + .map(Identifier::getPath).collect(Collectors.toSet()); + helper.assertTrue(own.equals(Set.copyOf(placement.getValue())), "Wrong native trade tag " + placement.getKey()); + placementCount += own.size(); + } + helper.assertTrue(placementCount == 10, "Missing a legacy trade placement"); + var trader = helper.spawnWithNoFreeWill(EntityType.VILLAGER, new BlockPos(1, 1, 1)); + var params = new LootParams.Builder(level) + .withParameter(LootContextParams.ORIGIN, trader.position()) + .withParameter(LootContextParams.THIS_ENTITY, trader) + .withParameter(LootContextParams.ADDITIONAL_COST_COMPONENT_ALLOWED, net.minecraft.util.Unit.INSTANCE) + .create(LootContextParamSets.VILLAGER_TRADE); + String[] names = {"steam_governor", "steam_pipe", "blackpowder_cartridge", "musket", "enchanted_musket"}; + int[][] bounds = {{1, 4, 1, 1}, {4, 8, 1, 1}, {5, 7, 1, 1}, {1, 1, 10, 15}, {1, 1, 20, 44}}; + JsonObject enchantedData = null; + for (int index = 0; index < names.length; index++) { + String name = names[index]; + var nativeTrade = registry.getOrThrow(ResourceKey.create(Registries.VILLAGER_TRADE, id(name))).value(); + var encoded = VillagerTrade.CODEC.encodeStart(ops, nativeTrade).getOrThrow(); + var parsed = VillagerTrade.CODEC.parse(ops, encoded); + helper.assertTrue(parsed.error().isEmpty() && parsed.result().isPresent(), "Native codec rejected " + name); + if (name.equals("enchanted_musket")) enchantedData = encoded.getAsJsonObject(); + Set counts = new HashSet<>(); + for (int sample = 1; sample <= 256; sample++) { + long seed = 0x9E3779B97F4A7C15L * sample; + var context = new LootContext.Builder(params).withOptionalRandomSeed(seed).create(Optional.empty()); + var offer = parsed.getOrThrow().getOffer(context); + helper.assertTrue(offer != null, "Native trade produced no offer: " + name); + ItemStack output = offer.getResult(); + int price = offer.getBaseCostA().getCount(); + int[] expected = bounds[index]; + helper.assertTrue(output.getCount() >= expected[0] && output.getCount() <= expected[1], "Wrong count for " + name); + helper.assertTrue(price >= expected[2] && price <= expected[3], "Wrong price for " + name); + helper.assertTrue(offer.getBaseCostA().is(net.minecraft.world.item.Items.EMERALD), "Wrong currency for " + name); + helper.assertTrue(offer.getMaxUses() == 7 && offer.getXp() == 1 && offer.getPriceMultiplier() == 0.0F, + "Changed legacy uses, XP or price multiplier: " + name); + helper.assertTrue(output.get(DataComponents.ADDITIONAL_TRADE_COST) == null, "Temporary trade cost leaked to output"); + if (name.equals("enchanted_musket")) { + RandomSource random = RandomSource.create(seed); + int enchantmentLevel = 5 + random.nextInt(15); + ItemStack legacy = EnchantmentHelper.enchantItem(random, new ItemStack(Items.blackpowder_musket), + enchantmentLevel, registries, Optional.of(registries.lookupOrThrow(Registries.ENCHANTMENT) + .getOrThrow(EnchantmentTags.NON_TREASURE))); + int legacyPrice = Math.min(15 + random.nextInt(11) + enchantmentLevel, 64); + helper.assertTrue(price == legacyPrice && ItemStack.isSameItemSameComponents(legacy, output), + "Native enchantment/correlated surcharge differs from the legacy algorithm at seed " + seed); + } else { + String itemId = index < 3 ? name : "musket"; + helper.assertTrue(output.is(registries.lookupOrThrow(Registries.ITEM) + .getOrThrow(ResourceKey.create(Registries.ITEM, id(itemId))).value()), "Wrong trade item: " + name); + } + counts.add(output.getCount()); + } + Set expectedCounts = IntStream.rangeClosed(bounds[index][0], bounds[index][1]).boxed().collect(Collectors.toSet()); + helper.assertTrue(counts.equals(expectedCounts), "Native sampling did not cover every inclusive count for " + name); + } + helper.assertTrue(enchantedData != null, "Missing actual native enchanted-trade codec data"); + JsonObject fallbackData = enchantedData.deepCopy(); + fallbackData.getAsJsonObject("wants").getAsJsonObject("count") + .addProperty("type", "missing_mod:unregistered_number_provider"); + var fallback = VillagerTrade.CODEC.parse(ops, fallbackData); + helper.assertTrue(fallback.error().isEmpty() && fallback.result().isPresent() + && VillagerTrade.CODEC.encodeStart(ops, fallback.getOrThrow()).getOrThrow().equals(enchantedData), + "Native uniform-number fallback changed the exact trade definition"); + for (int control = 0; control < 5; control++) { + JsonObject invalid = enchantedData.deepCopy(); + switch (control) { + case 0 -> invalid.getAsJsonObject("wants").addProperty("id", "missing_mod:unregistered_item"); + case 1 -> invalid.getAsJsonObject("gives").addProperty("count", 0); + case 2 -> invalid.getAsJsonArray("given_item_modifiers").get(0).getAsJsonObject() + .addProperty("function", "missing_mod:unregistered_function"); + case 3 -> invalid.getAsJsonArray("given_item_modifiers").get(0).getAsJsonObject() + .addProperty("options", "#missing_mod:unbound_enchantment_tag"); + case 4 -> { + JsonObject count = invalid.getAsJsonObject("wants").getAsJsonObject("count"); + count.addProperty("type", "missing_mod:unregistered_number_provider"); + count.remove("min"); + count.remove("max"); + } + default -> throw new AssertionError("Unexpected negative control"); + } + var rejected = VillagerTrade.CODEC.parse(ops, invalid); + helper.assertTrue(rejected.error().isPresent() && rejected.result().isEmpty(), + "Native codec accepted invalid trade control " + control); + } + com.mojang.logging.LogUtils.getLogger().info( + "BASESTEAM_NATIVE_TRADES_PASS definitions=5 placements=10 samples=1280 codecNegatives=5 fallbackWitnesses=1"); + } + + private static Identifier id(String path) { + return Identifier.fromNamespaceAndPath(SteamAdvantage.MODID, path); + } +} diff --git a/src/gametest/java/zone/moddev/mc/basesteam/gametest/SteamNativeGameTests.java b/src/gametest/java/zone/moddev/mc/basesteam/gametest/SteamNativeGameTests.java index 9b80abf..0f2c3ec 100644 --- a/src/gametest/java/zone/moddev/mc/basesteam/gametest/SteamNativeGameTests.java +++ b/src/gametest/java/zone/moddev/mc/basesteam/gametest/SteamNativeGameTests.java @@ -120,6 +120,16 @@ public static void olderMusketAliasAndTopLevelWearSurvivePowerMigration(GameTest @GameTest(structure = "basesteam:basesteam_empty") public static void nativeResourcesLoadWithExpectedIds(GameTestHelper helper) { + var jvmArguments = java.lang.management.ManagementFactory.getRuntimeMXBean().getInputArguments(); + for (String name : java.util.List.of("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS")) { + helper.assertTrue(System.getenv(name) == null, "Native JVM inherited " + name); + } + helper.assertTrue(jvmArguments.contains("-XX:ActiveProcessorCount=4") + && jvmArguments.contains("-Xmx1536m") && Runtime.getRuntime().availableProcessors() == 4 + && Runtime.getRuntime().maxMemory() <= 1536L * 1024 * 1024, + "Native JVM did not retain explicit CPU/heap caps: " + jvmArguments); + com.mojang.logging.LogUtils.getLogger().info( + "BASESTEAM_NATIVE_JVM_PASS inheritedOptions=absent processorCap=4 heapCapMiB=1536 args={}", jvmArguments); var level = helper.getLevel(); var registries = level.registryAccess(); var nativeEnchantments = registries.lookupOrThrow(Registries.ENCHANTMENT); @@ -173,15 +183,63 @@ public static void nativeResourcesLoadWithExpectedIds(GameTestHelper helper) { var brass = net.minecraft.tags.ItemTags.create(Identifier.fromNamespaceAndPath("forge", "ingots/brass")); if (registries.lookupOrThrow(Registries.ITEM).get(brass).filter(tag -> tag.size() > 0).isPresent()) { var recipe = (net.minecraft.world.item.crafting.ShapedRecipe)pipe.orElseThrow().value(); - var result = recipe.assemble(net.minecraft.world.item.crafting.CraftingInput.EMPTY, registries); + var result = recipe.assemble(net.minecraft.world.item.crafting.CraftingInput.EMPTY); helper.assertTrue(result.is(Blocks.steam_pipe.asItem()) && result.getCount() == 6, "Wrong Steam pipe recipe output"); } else { helper.assertTrue(pipe.isEmpty(), "Recipe must not load with an empty brass tag"); } + NativeVillagerTradeProof.verify(helper); + assertSalvageRecipes(helper); helper.succeed(); } + private static void assertSalvageRecipes(GameTestHelper helper) { + var level = helper.getLevel(); + var registries = level.registryAccess(); + var ops = registries.createSerializationContext(com.mojang.serialization.JsonOps.INSTANCE); + var mode = zone.moddev.mc.basepower.PowerAdvantage.recipeMode; + boolean enabled = mode == zone.moddev.mc.basepower.RecipeMode.APOCALYPTIC; + String[] inputs = {"steam_boiler_coal", "steam_boiler_electric", "steam_boiler_geothermal", + "steam_crusher", "steam_drill", "steam_elevator", "steam_furnace", "steam_governor", "steam_tank"}; + for (String input : inputs) { + var loaded = level.recipeAccess().byKey(ResourceKey.create(Registries.RECIPE, id("crushing_" + input))); + if (!enabled) { + helper.assertTrue(loaded.isEmpty(), "Salvage recipe loaded outside APOCALYPTIC mode: " + input); + continue; + } + var actual = loaded.orElseThrow().value(); + helper.assertTrue(actual instanceof zone.moddev.mc.basemetals.recipe.CrushingRecipe, + "Missing native crushing serializer: " + input); + var encoded = net.minecraft.world.item.crafting.Recipe.CODEC.encodeStart(ops, actual).getOrThrow(); + var parsed = net.minecraft.world.item.crafting.Recipe.CODEC.parse(ops, encoded).getOrThrow(); + helper.assertTrue(parsed instanceof zone.moddev.mc.basemetals.recipe.CrushingRecipe, + "Native salvage recipe codec did not round-trip: " + input); + var inputItem = registries.lookupOrThrow(Registries.ITEM) + .getOrThrow(ResourceKey.create(Registries.ITEM, id(input))).value(); + var inventory = new net.minecraft.world.item.crafting.SingleRecipeInput(new ItemStack(inputItem)); + Identifier expectedId = input.equals("steam_governor") + ? Identifier.fromNamespaceAndPath("basepower", "sprocket") : id("steam_governor"); + var expectedItem = registries.lookupOrThrow(Registries.ITEM) + .getOrThrow(ResourceKey.create(Registries.ITEM, expectedId)).value(); + int count = input.equals("steam_tank") ? 1 : input.equals("steam_elevator") ? 3 : 2; + for (var recipe : java.util.List.of((zone.moddev.mc.basemetals.recipe.CrushingRecipe)actual, + (zone.moddev.mc.basemetals.recipe.CrushingRecipe)parsed)) { + helper.assertTrue(recipe.matches(inventory, level), "Native salvage input did not match: " + input); + helper.assertTrue(!recipe.matches(new net.minecraft.world.item.crafting.SingleRecipeInput( + new ItemStack(net.minecraft.world.item.Items.DIAMOND)), level), "Unrelated salvage input matched: " + input); + ItemStack result = recipe.assemble(inventory); + helper.assertTrue(result.is(expectedItem) && result.getCount() == count, + "Salvage output identity or exact quantity changed: " + input); + } + helper.assertTrue(inventory.item().is(inputItem) && inventory.item().getCount() == 1, + "Salvage matching/assembly mutated the supplied input: " + input); + } + com.mojang.logging.LogUtils.getLogger().info( + "BASESTEAM_NATIVE_SALVAGE_PASS mode={} present={} absent={} nativeCodecs={} inputNegatives={}", + mode, enabled ? 9 : 0, enabled ? 0 : 9, enabled ? 9 : 0, enabled ? 9 : 0); + } + private static Identifier id(String path) { return Identifier.fromNamespaceAndPath(SteamAdvantage.MODID, path); } diff --git a/src/main/java/zone/moddev/mc/basesteam/SteamAdvantage.java b/src/main/java/zone/moddev/mc/basesteam/SteamAdvantage.java index 7ad2d4e..bac08a7 100644 --- a/src/main/java/zone/moddev/mc/basesteam/SteamAdvantage.java +++ b/src/main/java/zone/moddev/mc/basesteam/SteamAdvantage.java @@ -19,19 +19,18 @@ import cyano.poweradvantage.api.migration.LegacyConfigMigration.ValueType; import zone.moddev.mc.basesteam.config.SteamAdvantageConfig; import zone.moddev.mc.basesteam.init.GUI; -import zone.moddev.mc.basesteam.init.Recipes; import zone.moddev.mc.basesteam.init.TreasureChests; import zone.moddev.mc.basesteam.init.Villages; import zone.moddev.mc.basesteam.migration.SteamLegacyMigrations; import zone.moddev.mc.basesteam.migration.SteamConfigMigration; import zone.moddev.mc.basesteam.recipe.RecipeConditions; -/** Forge 1.21.11 entry point for baseSteam. */ +/** Forge 26.1.2 entry point for baseSteam. */ @Mod(SteamAdvantage.MODID) public final class SteamAdvantage { public static final String MODID = "basesteam"; public static final String NAME = "baseSteam"; - 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 float MUSKET_DAMAGE = 20.0F; @@ -79,7 +78,6 @@ private static void migrateLegacyConfiguration() { private void commonSetup(FMLCommonSetupEvent event) { event.enqueueWork(GUI::init); SteamAdvantageConfig.apply(); - Recipes.init(); TreasureChests.init(FMLPaths.CONFIGDIR.get()); Villages.init(); LOGGER.info("baseSteam {} common setup complete", VERSION); diff --git a/src/main/java/zone/moddev/mc/basesteam/compat/BaseMetalsCompat.java b/src/main/java/zone/moddev/mc/basesteam/compat/BaseMetalsCompat.java index 67de180..10c24f2 100644 --- a/src/main/java/zone/moddev/mc/basesteam/compat/BaseMetalsCompat.java +++ b/src/main/java/zone/moddev/mc/basesteam/compat/BaseMetalsCompat.java @@ -13,7 +13,7 @@ public final class BaseMetalsCompat { private BaseMetalsCompat() {} - public static boolean isLoaded() { return ModList.get().isLoaded("basemetals"); } + public static boolean isLoaded() { return ModList.isLoaded("basemetals"); } /** 1.13 additions are supplied as JSON crushing recipes, not mutable runtime registrations. */ public static void addCrusherRecipe(String input, ItemStack output) {} diff --git a/src/main/java/zone/moddev/mc/basesteam/graphics/ClientRegistration.java b/src/main/java/zone/moddev/mc/basesteam/graphics/ClientRegistration.java index d67e505..567923c 100644 --- a/src/main/java/zone/moddev/mc/basesteam/graphics/ClientRegistration.java +++ b/src/main/java/zone/moddev/mc/basesteam/graphics/ClientRegistration.java @@ -1,13 +1,10 @@ package zone.moddev.mc.basesteam.graphics; import net.minecraftforge.client.event.EntityRenderersEvent; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.eventbus.api.bus.BusGroup; import zone.moddev.mc.basesteam.init.Entities; /** Client-only renderer registration. */ -@OnlyIn(Dist.CLIENT) public final class ClientRegistration { private ClientRegistration() {} diff --git a/src/main/java/zone/moddev/mc/basesteam/graphics/DrillBitRenderer.java b/src/main/java/zone/moddev/mc/basesteam/graphics/DrillBitRenderer.java index 237a72b..2d8030a 100644 --- a/src/main/java/zone/moddev/mc/basesteam/graphics/DrillBitRenderer.java +++ b/src/main/java/zone/moddev/mc/basesteam/graphics/DrillBitRenderer.java @@ -6,19 +6,16 @@ import net.minecraft.client.renderer.rendertype.RenderTypes; 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 com.mojang.math.Axis; import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider.Context; import net.minecraft.core.Direction; import net.minecraft.resources.Identifier; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; import zone.moddev.mc.basesteam.SteamAdvantage; import zone.moddev.mc.basesteam.blocks.DrillBitTileEntity; -@OnlyIn(Dist.CLIENT) public class DrillBitRenderer implements BlockEntityRenderer { private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(SteamAdvantage.MODID, "textures/entity/drill_bit.png"); diff --git a/src/main/java/zone/moddev/mc/basesteam/gui/BlastFurnaceGUI.java b/src/main/java/zone/moddev/mc/basesteam/gui/BlastFurnaceGUI.java index a4b5dcd..224a456 100644 --- a/src/main/java/zone/moddev/mc/basesteam/gui/BlastFurnaceGUI.java +++ b/src/main/java/zone/moddev/mc/basesteam/gui/BlastFurnaceGUI.java @@ -55,7 +55,6 @@ public void onTake(Player player, ItemStack stack) * the GUI * @param z This is the z coordinate (no units) into the depth of the screen */ - @net.minecraftforge.api.distmarker.OnlyIn(net.minecraftforge.api.distmarker.Dist.CLIENT) @Override public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z){ diff --git a/src/main/java/zone/moddev/mc/basesteam/gui/CoalBoilerGUI.java b/src/main/java/zone/moddev/mc/basesteam/gui/CoalBoilerGUI.java index 68339d9..5c8be6d 100644 --- a/src/main/java/zone/moddev/mc/basesteam/gui/CoalBoilerGUI.java +++ b/src/main/java/zone/moddev/mc/basesteam/gui/CoalBoilerGUI.java @@ -42,7 +42,6 @@ public CoalBoilerGUI() { * the GUI * @param z This is the z coordinate (no units) into the depth of the screen */ - @net.minecraftforge.api.distmarker.OnlyIn(net.minecraftforge.api.distmarker.Dist.CLIENT) @Override public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z){ diff --git a/src/main/java/zone/moddev/mc/basesteam/gui/ElectricBoilerGUI.java b/src/main/java/zone/moddev/mc/basesteam/gui/ElectricBoilerGUI.java index 1810810..5308e51 100644 --- a/src/main/java/zone/moddev/mc/basesteam/gui/ElectricBoilerGUI.java +++ b/src/main/java/zone/moddev/mc/basesteam/gui/ElectricBoilerGUI.java @@ -42,7 +42,6 @@ public ElectricBoilerGUI() { * the GUI * @param z This is the z coordinate (no units) into the depth of the screen */ - @net.minecraftforge.api.distmarker.OnlyIn(net.minecraftforge.api.distmarker.Dist.CLIENT) @Override public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z){ diff --git a/src/main/java/zone/moddev/mc/basesteam/gui/GUIHelper.java b/src/main/java/zone/moddev/mc/basesteam/gui/GUIHelper.java index 80c2f9d..da6d7cf 100644 --- a/src/main/java/zone/moddev/mc/basesteam/gui/GUIHelper.java +++ b/src/main/java/zone/moddev/mc/basesteam/gui/GUIHelper.java @@ -6,7 +6,6 @@ import net.minecraft.resources.Identifier; import cyano.poweradvantage.api.fluid.FluidStack; -@net.minecraftforge.api.distmarker.OnlyIn(net.minecraftforge.api.distmarker.Dist.CLIENT) public abstract class GUIHelper { diff --git a/src/main/java/zone/moddev/mc/basesteam/gui/GeothermalBoilerGUI.java b/src/main/java/zone/moddev/mc/basesteam/gui/GeothermalBoilerGUI.java index 067458e..72aadf2 100644 --- a/src/main/java/zone/moddev/mc/basesteam/gui/GeothermalBoilerGUI.java +++ b/src/main/java/zone/moddev/mc/basesteam/gui/GeothermalBoilerGUI.java @@ -42,7 +42,6 @@ public GeothermalBoilerGUI() { * the GUI * @param z This is the z coordinate (no units) into the depth of the screen */ - @net.minecraftforge.api.distmarker.OnlyIn(net.minecraftforge.api.distmarker.Dist.CLIENT) @Override public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z){ diff --git a/src/main/java/zone/moddev/mc/basesteam/gui/OilBoilerGUI.java b/src/main/java/zone/moddev/mc/basesteam/gui/OilBoilerGUI.java index 042b938..327c404 100644 --- a/src/main/java/zone/moddev/mc/basesteam/gui/OilBoilerGUI.java +++ b/src/main/java/zone/moddev/mc/basesteam/gui/OilBoilerGUI.java @@ -42,7 +42,6 @@ public OilBoilerGUI() { * the GUI * @param z This is the z coordinate (no units) into the depth of the screen */ - @net.minecraftforge.api.distmarker.OnlyIn(net.minecraftforge.api.distmarker.Dist.CLIENT) @Override public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z){ diff --git a/src/main/java/zone/moddev/mc/basesteam/gui/RockCrusherGUI.java b/src/main/java/zone/moddev/mc/basesteam/gui/RockCrusherGUI.java index 8f44c70..e368f1c 100644 --- a/src/main/java/zone/moddev/mc/basesteam/gui/RockCrusherGUI.java +++ b/src/main/java/zone/moddev/mc/basesteam/gui/RockCrusherGUI.java @@ -41,7 +41,6 @@ public RockCrusherGUI() { * the GUI * @param z This is the z coordinate (no units) into the depth of the screen */ - @net.minecraftforge.api.distmarker.OnlyIn(net.minecraftforge.api.distmarker.Dist.CLIENT) @Override public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z){ diff --git a/src/main/java/zone/moddev/mc/basesteam/gui/SteamDrillGUI.java b/src/main/java/zone/moddev/mc/basesteam/gui/SteamDrillGUI.java index 629b467..1ce7ebd 100644 --- a/src/main/java/zone/moddev/mc/basesteam/gui/SteamDrillGUI.java +++ b/src/main/java/zone/moddev/mc/basesteam/gui/SteamDrillGUI.java @@ -41,7 +41,6 @@ public SteamDrillGUI() { * the GUI * @param z This is the z coordinate (no units) into the depth of the screen */ - @net.minecraftforge.api.distmarker.OnlyIn(net.minecraftforge.api.distmarker.Dist.CLIENT) @Override public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z){ diff --git a/src/main/java/zone/moddev/mc/basesteam/gui/SteamPumpGUI.java b/src/main/java/zone/moddev/mc/basesteam/gui/SteamPumpGUI.java index 53d6de4..5256a03 100644 --- a/src/main/java/zone/moddev/mc/basesteam/gui/SteamPumpGUI.java +++ b/src/main/java/zone/moddev/mc/basesteam/gui/SteamPumpGUI.java @@ -42,7 +42,6 @@ public SteamPumpGUI() { * the GUI * @param z This is the z coordinate (no units) into the depth of the screen */ - @net.minecraftforge.api.distmarker.OnlyIn(net.minecraftforge.api.distmarker.Dist.CLIENT) @Override public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z){ diff --git a/src/main/java/zone/moddev/mc/basesteam/gui/SteamStillGUI.java b/src/main/java/zone/moddev/mc/basesteam/gui/SteamStillGUI.java index 18f667e..b43db05 100644 --- a/src/main/java/zone/moddev/mc/basesteam/gui/SteamStillGUI.java +++ b/src/main/java/zone/moddev/mc/basesteam/gui/SteamStillGUI.java @@ -42,7 +42,6 @@ public SteamStillGUI() { * the GUI * @param z This is the z coordinate (no units) into the depth of the screen */ - @net.minecraftforge.api.distmarker.OnlyIn(net.minecraftforge.api.distmarker.Dist.CLIENT) @Override public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z){ diff --git a/src/main/java/zone/moddev/mc/basesteam/gui/SteamTankGUI.java b/src/main/java/zone/moddev/mc/basesteam/gui/SteamTankGUI.java index 7ec65cc..ec4061b 100644 --- a/src/main/java/zone/moddev/mc/basesteam/gui/SteamTankGUI.java +++ b/src/main/java/zone/moddev/mc/basesteam/gui/SteamTankGUI.java @@ -41,7 +41,6 @@ public SteamTankGUI() { * the GUI * @param z This is the z coordinate (no units) into the depth of the screen */ - @net.minecraftforge.api.distmarker.OnlyIn(net.minecraftforge.api.distmarker.Dist.CLIENT) @Override public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z){ diff --git a/src/main/java/zone/moddev/mc/basesteam/init/Recipes.java b/src/main/java/zone/moddev/mc/basesteam/init/Recipes.java deleted file mode 100644 index 9440183..0000000 --- a/src/main/java/zone/moddev/mc/basesteam/init/Recipes.java +++ /dev/null @@ -1,36 +0,0 @@ -package zone.moddev.mc.basesteam.init; - -import net.minecraft.world.item.ItemStack; -import zone.moddev.mc.basepower.PowerAdvantage; -import zone.moddev.mc.basepower.RecipeMode; -import zone.moddev.mc.basesteam.compat.BaseMetalsCompat; - -public class Recipes { - - private static boolean initDone = false; - - public static void init(){ - if(initDone) return; - - - - - RecipeMode recipeMode = PowerAdvantage.recipeMode; - if(recipeMode == RecipeMode.APOCALYPTIC){ - BaseMetalsCompat.addCrusherRecipe(Items.steam_governor, new ItemStack(zone.moddev.mc.basepower.init.Items.sprocket,2)); - - BaseMetalsCompat.addCrusherRecipe(Blocks.steam_crusher, new ItemStack(Items.steam_governor,2)); - BaseMetalsCompat.addCrusherRecipe(Blocks.steam_furnace, new ItemStack(Items.steam_governor,2)); - BaseMetalsCompat.addCrusherRecipe(Blocks.steam_boiler_coal, new ItemStack(Items.steam_governor,2)); - BaseMetalsCompat.addCrusherRecipe(Blocks.steam_boiler_electric, new ItemStack(Items.steam_governor,2)); - BaseMetalsCompat.addCrusherRecipe(Blocks.steam_boiler_geothermal, new ItemStack(Items.steam_governor,2)); - BaseMetalsCompat.addCrusherRecipe(Blocks.steam_drill, new ItemStack(Items.steam_governor,2)); - BaseMetalsCompat.addCrusherRecipe(Blocks.steam_elevator, new ItemStack(Items.steam_governor,3)); - BaseMetalsCompat.addCrusherRecipe(Blocks.steam_tank, new ItemStack(Items.steam_governor,1)); - } - - - - initDone = true; - } -} diff --git a/src/main/java/zone/moddev/mc/basesteam/init/Villages.java b/src/main/java/zone/moddev/mc/basesteam/init/Villages.java index 0f9995b..dc92a93 100644 --- a/src/main/java/zone/moddev/mc/basesteam/init/Villages.java +++ b/src/main/java/zone/moddev/mc/basesteam/init/Villages.java @@ -1,65 +1,7 @@ package zone.moddev.mc.basesteam.init; -import java.util.List; -import net.minecraft.core.registries.Registries; -import net.minecraft.tags.EnchantmentTags; -import net.minecraft.util.RandomSource; -import net.minecraft.world.item.enchantment.EnchantmentHelper; -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; -import net.minecraftforge.eventbus.api.listener.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; -import zone.moddev.mc.basesteam.SteamAdvantage; - -/** Legacy smith careers map to their native 1.14 professions; prices and levels are unchanged. */ -@Mod.EventBusSubscriber(modid = SteamAdvantage.MODID) +/** Native villager_trade data and profession tags retain the legacy offers. */ public final class Villages { private Villages() {} public static void init() {} - - @SubscribeEvent - public static void addTrades(VillagerTradesEvent event) { - net.minecraft.resources.ResourceKey profession = event.getType(); - List novice = event.getTrades().get(1); - if (profession == VillagerProfession.ARMORER || profession == VillagerProfession.TOOLSMITH - || profession == VillagerProfession.WEAPONSMITH || profession == VillagerProfession.LIBRARIAN) { - novice.add(sell(Items.steam_governor, -4, -1)); - } - if (profession == VillagerProfession.LIBRARIAN) novice.add(sell(Blocks.steam_pipe.asItem(), -8, -4)); - if (profession == VillagerProfession.WEAPONSMITH || profession == VillagerProfession.TOOLSMITH) { - novice.add(sell(Items.blackpowder_cartridge, -7, -5)); - novice.add(sell(Items.blackpowder_musket, 10, 15)); - } - if (profession == VillagerProfession.CLERIC) { - event.getTrades().get(3).add((level, trader, random) -> { - int enchantmentLevel = 5 + random.nextInt(15); - ItemStack musket = EnchantmentHelper.enchantItem(random, - new ItemStack(Items.blackpowder_musket), enchantmentLevel, - trader.registryAccess().lookupOrThrow(Registries.ENCHANTMENT) - .getOrThrow(EnchantmentTags.NON_TREASURE).stream()); - int price = Math.min(randomPrice(random, 15, 25) + enchantmentLevel, 64); - return offer(price, musket); - }); - } - } - - private static VillagerTrades.ItemListing sell(Item item, int min, int max) { - return (level, trader, random) -> { - int price = randomPrice(random, min, max); - return offer(price < 0 ? 1 : price, new ItemStack(item, price < 0 ? -price : 1)); - }; - } - - private static int randomPrice(RandomSource random, int min, int max) { - return min + random.nextInt(max - min + 1); - } - - private static MerchantOffer offer(int emeralds, ItemStack output) { - return new MerchantOffer(new ItemCost(net.minecraft.world.item.Items.EMERALD, emeralds), output, 7, 1, 0.0F); - } } diff --git a/src/main/java/zone/moddev/mc/basesteam/machines/BlastFurnaceTileEntity.java b/src/main/java/zone/moddev/mc/basesteam/machines/BlastFurnaceTileEntity.java index 7172f18..7a237d1 100644 --- a/src/main/java/zone/moddev/mc/basesteam/machines/BlastFurnaceTileEntity.java +++ b/src/main/java/zone/moddev/mc/basesteam/machines/BlastFurnaceTileEntity.java @@ -14,8 +14,6 @@ import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; import net.minecraft.core.Direction; import net.minecraft.sounds.SoundSource; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; import java.util.Arrays; @@ -60,7 +58,6 @@ public BlastFurnaceTileEntity(net.minecraft.core.BlockPos pos, net.minecraft.wor } } - @OnlyIn(Dist.CLIENT) public float[] getProgress(){ float[] a = new float[progress.length]; System.arraycopy(progress, 0, a, 0, progress.length); @@ -209,7 +206,8 @@ private int getFuelBurnTime(ItemStack item) { private void decrementFuel() { if(inventory[0].getCount() == 1){ - inventory[0] = inventory[0].getCraftingRemainder(); + var remainder = inventory[0].getCraftingRemainder(); + inventory[0] = remainder == null ? ItemStack.EMPTY : remainder.create(); } else { this.removeItem(0, 1); } @@ -337,7 +335,7 @@ private ItemStack getSmeltingResult(ItemStack input) { || !(getLevel() instanceof net.minecraft.server.level.ServerLevel server)) return ItemStack.EMPTY; SingleRecipeInput inventory = new SingleRecipeInput(input); return server.recipeAccess().getRecipeFor(RecipeType.SMELTING, inventory, server) - .map(recipe -> recipe.value().assemble(inventory, getLevel().registryAccess())).orElse(ItemStack.EMPTY); + .map(recipe -> recipe.value().assemble(inventory)).orElse(ItemStack.EMPTY); } diff --git a/src/main/java/zone/moddev/mc/basesteam/machines/CoalBoilerTileEntity.java b/src/main/java/zone/moddev/mc/basesteam/machines/CoalBoilerTileEntity.java index 52bf177..759afea 100644 --- a/src/main/java/zone/moddev/mc/basesteam/machines/CoalBoilerTileEntity.java +++ b/src/main/java/zone/moddev/mc/basesteam/machines/CoalBoilerTileEntity.java @@ -70,7 +70,7 @@ public void tickUpdate(boolean isServerWorld) { burnTime--; boilWater(); // play steam sounds occasionally - if(getLevel().random.nextInt(100) == 0){ + if(getLevel().getRandom().nextInt(100) == 0){ playSoundAtTileEntity(SoundEvents.FIRE_EXTINGUISH, SoundSource.AMBIENT, 0.5f, 1f, this); } if(timeSinceSound > 200){ @@ -118,7 +118,8 @@ private int getFuelBurnTime() { private void decrementFuel() { if(inventory[0].getCount() == 1){ - inventory[0] = inventory[0].getCraftingRemainder(); + var remainder = inventory[0].getCraftingRemainder(); + inventory[0] = remainder == null ? ItemStack.EMPTY : remainder.create(); } else { this.removeItem(0, 1); } diff --git a/src/main/java/zone/moddev/mc/basesteam/machines/ElectricBoilerTileEntity.java b/src/main/java/zone/moddev/mc/basesteam/machines/ElectricBoilerTileEntity.java index 14b4acb..f5bc439 100644 --- a/src/main/java/zone/moddev/mc/basesteam/machines/ElectricBoilerTileEntity.java +++ b/src/main/java/zone/moddev/mc/basesteam/machines/ElectricBoilerTileEntity.java @@ -70,7 +70,7 @@ public void tickUpdate(boolean isServerWorld) { if(!redstone && getEnergy(ELECTRIC_POWER) >= ELECTRICITY_PER_TICK){ boilWater(); // play steam sounds occasionally - if(getLevel().random.nextInt(100) == 0){ + if(getLevel().getRandom().nextInt(100) == 0){ playSoundAtTileEntity( SoundEvents.FIRE_EXTINGUISH, SoundSource.AMBIENT, 0.5f, 1f, this); } if(timeSinceSound > 200){ diff --git a/src/main/java/zone/moddev/mc/basesteam/machines/GeothermalBoilerTileEntity.java b/src/main/java/zone/moddev/mc/basesteam/machines/GeothermalBoilerTileEntity.java index ea9aa58..708c0a0 100644 --- a/src/main/java/zone/moddev/mc/basesteam/machines/GeothermalBoilerTileEntity.java +++ b/src/main/java/zone/moddev/mc/basesteam/machines/GeothermalBoilerTileEntity.java @@ -78,7 +78,7 @@ public void tickUpdate(boolean isServerWorld) { if(!redstone && tank.getFluidAmount() > 0 && temperature >= ACTIVATION_TEMPERATURE){ boilWater(); // play steam sounds occasionally - if(getLevel().random.nextInt(100) == 0){ + if(getLevel().getRandom().nextInt(100) == 0){ playSoundAtTileEntity( SoundEvents.FIRE_EXTINGUISH, SoundSource.AMBIENT, 0.5f, 1f, this); } if(timeSinceSound > 200){ diff --git a/src/main/java/zone/moddev/mc/basesteam/machines/OilBoilerTileEntity.java b/src/main/java/zone/moddev/mc/basesteam/machines/OilBoilerTileEntity.java index 3c3d624..8171ae2 100644 --- a/src/main/java/zone/moddev/mc/basesteam/machines/OilBoilerTileEntity.java +++ b/src/main/java/zone/moddev/mc/basesteam/machines/OilBoilerTileEntity.java @@ -68,7 +68,7 @@ public void tickUpdate(boolean isServerWorld) { burnTime--; boilWater(); // play steam sounds occasionally - if(getLevel().random.nextInt(100) == 0){ + if(getLevel().getRandom().nextInt(100) == 0){ playSoundAtTileEntity( SoundEvents.FIRE_EXTINGUISH, SoundSource.AMBIENT, 0.5f, 1f, this); } if(timeSinceSound > 200){ diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index f426bd1..c86cfca 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -22,7 +22,7 @@ side="BOTH" [[dependencies.basesteam]] 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/minecraft/atlases/blocks.json b/src/main/resources/assets/minecraft/atlases/blocks.json index d69f990..26f2ab8 100644 --- a/src/main/resources/assets/minecraft/atlases/blocks.json +++ b/src/main/resources/assets/minecraft/atlases/blocks.json @@ -1,8 +1,100 @@ { "sources": [ { - "type": "single", + "type": "minecraft:single", + "resource": "basesteam:blocks/coal_boiler_front" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/electric_boiler_back" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/electric_boiler_front" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/geothermal_boiler_side" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/geothermal_boiler_top" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/oil_tank_end" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/platform_top" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/rock_crusher_front" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/rock_crusher_top" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/steam_drill_bottom" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/steam_drill_top" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/steam_elevator_side" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/steam_furnace_front" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/steam_furnace_top" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/steam_pipe" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/steam_pump_front" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/steam_pump_side" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/steam_switch_off" + }, + { + "type": "minecraft:single", + "resource": "basesteam:blocks/steam_switch_on" + }, + { + "type": "minecraft:single", "resource": "basesteam:entity/drill_bit" + }, + { + "type": "minecraft:single", + "resource": "basesteam:materials/bolted_copper" + }, + { + "type": "minecraft:single", + "resource": "basesteam:materials/bolted_iron" + }, + { + "type": "minecraft:single", + "resource": "basesteam:materials/iron" + }, + { + "type": "minecraft:single", + "resource": "basesteam:materials/steel_dark_hole" } ] } diff --git a/src/main/resources/assets/minecraft/atlases/items.json b/src/main/resources/assets/minecraft/atlases/items.json index c2643ee..06beaed 100644 --- a/src/main/resources/assets/minecraft/atlases/items.json +++ b/src/main/resources/assets/minecraft/atlases/items.json @@ -1,5 +1,16 @@ { "sources": [ - { "type": "directory", "source": "items", "prefix": "items/" } + { + "type": "minecraft:single", + "resource": "basesteam:items/blackpowder_cartridge" + }, + { + "type": "minecraft:single", + "resource": "basesteam:items/steam_drill_bit" + }, + { + "type": "minecraft:single", + "resource": "basesteam:items/steam_governor" + } ] } diff --git a/src/main/resources/data/basesteam/villager_trade/blackpowder_cartridge.json b/src/main/resources/data/basesteam/villager_trade/blackpowder_cartridge.json new file mode 100644 index 0000000..010db81 --- /dev/null +++ b/src/main/resources/data/basesteam/villager_trade/blackpowder_cartridge.json @@ -0,0 +1,10 @@ +{ + "wants": { "id": "minecraft:emerald", "count": 1 }, + "gives": { "id": "basesteam:blackpowder_cartridge", "count": 1 }, + "max_uses": 7, + "reputation_discount": 0, + "xp": 1, + "given_item_modifiers": [ + { "function": "minecraft:set_count", "count": { "type": "minecraft:uniform", "min": 5, "max": 7 } } + ] +} diff --git a/src/main/resources/data/basesteam/villager_trade/enchanted_musket.json b/src/main/resources/data/basesteam/villager_trade/enchanted_musket.json new file mode 100644 index 0000000..d27afd3 --- /dev/null +++ b/src/main/resources/data/basesteam/villager_trade/enchanted_musket.json @@ -0,0 +1,15 @@ +{ + "wants": { "id": "minecraft:emerald", "count": { "type": "minecraft:uniform", "min": 15, "max": 25 } }, + "gives": { "id": "basesteam:musket", "count": 1 }, + "max_uses": 7, + "reputation_discount": 0, + "xp": 1, + "given_item_modifiers": [ + { + "function": "minecraft:enchant_with_levels", + "levels": { "type": "minecraft:uniform", "min": 5, "max": 19 }, + "options": "#minecraft:non_treasure", + "include_additional_cost_component": true + } + ] +} diff --git a/src/main/resources/data/basesteam/villager_trade/musket.json b/src/main/resources/data/basesteam/villager_trade/musket.json new file mode 100644 index 0000000..b38a703 --- /dev/null +++ b/src/main/resources/data/basesteam/villager_trade/musket.json @@ -0,0 +1,8 @@ +{ + "wants": { "id": "minecraft:emerald", "count": { "type": "minecraft:uniform", "min": 10, "max": 15 } }, + "gives": { "id": "basesteam:musket", "count": 1 }, + "max_uses": 7, + "reputation_discount": 0, + "xp": 1, + "given_item_modifiers": [] +} diff --git a/src/main/resources/data/basesteam/villager_trade/steam_governor.json b/src/main/resources/data/basesteam/villager_trade/steam_governor.json new file mode 100644 index 0000000..3a14a29 --- /dev/null +++ b/src/main/resources/data/basesteam/villager_trade/steam_governor.json @@ -0,0 +1,10 @@ +{ + "wants": { "id": "minecraft:emerald", "count": 1 }, + "gives": { "id": "basesteam:steam_governor", "count": 1 }, + "max_uses": 7, + "reputation_discount": 0, + "xp": 1, + "given_item_modifiers": [ + { "function": "minecraft:set_count", "count": { "type": "minecraft:uniform", "min": 1, "max": 4 } } + ] +} diff --git a/src/main/resources/data/basesteam/villager_trade/steam_pipe.json b/src/main/resources/data/basesteam/villager_trade/steam_pipe.json new file mode 100644 index 0000000..2b46e2b --- /dev/null +++ b/src/main/resources/data/basesteam/villager_trade/steam_pipe.json @@ -0,0 +1,10 @@ +{ + "wants": { "id": "minecraft:emerald", "count": 1 }, + "gives": { "id": "basesteam:steam_pipe", "count": 1 }, + "max_uses": 7, + "reputation_discount": 0, + "xp": 1, + "given_item_modifiers": [ + { "function": "minecraft:set_count", "count": { "type": "minecraft:uniform", "min": 4, "max": 8 } } + ] +} diff --git a/src/main/resources/data/minecraft/tags/villager_trade/armorer/level_1.json b/src/main/resources/data/minecraft/tags/villager_trade/armorer/level_1.json new file mode 100644 index 0000000..b37a593 --- /dev/null +++ b/src/main/resources/data/minecraft/tags/villager_trade/armorer/level_1.json @@ -0,0 +1,4 @@ +{ + "replace": false, + "values": ["basesteam:steam_governor"] +} diff --git a/src/main/resources/data/minecraft/tags/villager_trade/cleric/level_3.json b/src/main/resources/data/minecraft/tags/villager_trade/cleric/level_3.json new file mode 100644 index 0000000..8b11ccd --- /dev/null +++ b/src/main/resources/data/minecraft/tags/villager_trade/cleric/level_3.json @@ -0,0 +1,4 @@ +{ + "replace": false, + "values": ["basesteam:enchanted_musket"] +} 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..2e49d6e --- /dev/null +++ b/src/main/resources/data/minecraft/tags/villager_trade/librarian/level_1.json @@ -0,0 +1,4 @@ +{ + "replace": false, + "values": ["basesteam:steam_governor", "basesteam:steam_pipe"] +} diff --git a/src/main/resources/data/minecraft/tags/villager_trade/toolsmith/level_1.json b/src/main/resources/data/minecraft/tags/villager_trade/toolsmith/level_1.json new file mode 100644 index 0000000..0745ad7 --- /dev/null +++ b/src/main/resources/data/minecraft/tags/villager_trade/toolsmith/level_1.json @@ -0,0 +1,4 @@ +{ + "replace": false, + "values": ["basesteam:steam_governor", "basesteam:blackpowder_cartridge", "basesteam:musket"] +} diff --git a/src/main/resources/data/minecraft/tags/villager_trade/weaponsmith/level_1.json b/src/main/resources/data/minecraft/tags/villager_trade/weaponsmith/level_1.json new file mode 100644 index 0000000..0745ad7 --- /dev/null +++ b/src/main/resources/data/minecraft/tags/villager_trade/weaponsmith/level_1.json @@ -0,0 +1,4 @@ +{ + "replace": false, + "values": ["basesteam:steam_governor", "basesteam:blackpowder_cartridge", "basesteam:musket"] +} diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index ba78a5d..e14cd39 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":"baseSteam" } } diff --git a/src/test/java/zone/moddev/mc/basesteam/machines/CoalBoilerFillPersistenceTest.java b/src/test/java/zone/moddev/mc/basesteam/machines/CoalBoilerFillPersistenceTest.java index 9ec7afe..bdfee46 100644 --- a/src/test/java/zone/moddev/mc/basesteam/machines/CoalBoilerFillPersistenceTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/machines/CoalBoilerFillPersistenceTest.java @@ -217,6 +217,6 @@ private static SaveWorld world(boolean remote) { } } private static final class SaveWorld extends net.minecraft.server.level.ServerLevel { - private SaveWorld() { super(null, null, null, null, null, null, false, 0L, java.util.Collections.emptyList(), false, null); } + private SaveWorld() { super(null, null, null, null, null, null, false, 0L, java.util.Collections.emptyList(), false); } } } diff --git a/src/test/java/zone/moddev/mc/basesteam/machines/RockCrusherReloadRedstoneTest.java b/src/test/java/zone/moddev/mc/basesteam/machines/RockCrusherReloadRedstoneTest.java index 5c0e915..218de97 100644 --- a/src/test/java/zone/moddev/mc/basesteam/machines/RockCrusherReloadRedstoneTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/machines/RockCrusherReloadRedstoneTest.java @@ -84,7 +84,7 @@ private static final class ControlSample extends RuntimeException {} private static final class ControlWorld extends ServerLevel { boolean powered, stopAtFirstSample; int samples; - private ControlWorld() { super(null, null, null, null, null, null, false, 0L, java.util.Collections.emptyList(), false, null); } + private ControlWorld() { super(null, null, null, null, null, null, false, 0L, java.util.Collections.emptyList(), false); } @Override public boolean hasNeighborSignal(BlockPos pos) { samples++; if (stopAtFirstSample) throw new ControlSample(); diff --git a/src/test/java/zone/moddev/mc/basesteam/machines/WaterBoilerTankNotificationTest.java b/src/test/java/zone/moddev/mc/basesteam/machines/WaterBoilerTankNotificationTest.java index ab4bdd3..98f5b0a 100644 --- a/src/test/java/zone/moddev/mc/basesteam/machines/WaterBoilerTankNotificationTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/machines/WaterBoilerTankNotificationTest.java @@ -162,6 +162,6 @@ private static Level world(boolean client) { } catch (ReflectiveOperationException failure) { throw new AssertionError(failure); } } private static final class SaveWorld extends net.minecraft.server.level.ServerLevel { - private SaveWorld() { super(null, null, null, null, null, null, false, 0L, java.util.Collections.emptyList(), false, null); } + private SaveWorld() { super(null, null, null, null, null, null, false, 0L, java.util.Collections.emptyList(), false); } } } diff --git a/src/test/java/zone/moddev/mc/basesteam/resources/BuildWorkflowContractTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/BuildWorkflowContractTest.java index 927798a..729f8bc 100644 --- a/src/test/java/zone/moddev/mc/basesteam/resources/BuildWorkflowContractTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/resources/BuildWorkflowContractTest.java @@ -12,15 +12,16 @@ public class BuildWorkflowContractTest { try (var reader = Files.newBufferedReader(Path.of("gradle.properties"))) { pins.load(reader); } - assertEquals("1.21.11", pins.getProperty("minecraft_version")); - assertEquals("61.1.0", pins.getProperty("forge_version")); - assertEquals("21", pins.getProperty("java_version")); + assertEquals("26.1.2", pins.getProperty("minecraft_version")); + assertEquals("64.1.5", pins.getProperty("forge_version")); + assertEquals("25", pins.getProperty("java_version")); assertEquals("official", pins.getProperty("mapping_channel")); - assertEquals("1.21.11", pins.getProperty("mapping_version")); - assertEquals("2.4.15.121111", pins.getProperty("poweradvantage_version")); + assertEquals("26.1.2", pins.getProperty("mapping_version")); + assertEquals("2.4.16.2601021", pins.getProperty("poweradvantage_version")); for (String key : new String[] {"poweradvantage_source_ref", "poweradvantage_deobf_sha256", "poweradvantage_player_sha256", "poweradvantage_test_bootstrap_sha256", "poweradvantage_test_bootstrap_source_sha256", "poweradvantage_test_registry_source_sha256", + "poweradvantage_test_loader_source_sha256", "basemetals_source_commit", "basemetals_deobf_sha256", "basemetals_player_sha256", "orespawn_source_commit", "orespawn_deobf_sha256", "orespawn_player_sha256"}) { @@ -29,7 +30,7 @@ public class BuildWorkflowContractTest { } assertEquals(pins.getProperty("poweradvantage_deobf_sha256"), pins.getProperty("poweradvantage_player_sha256")); String build = Files.readString(Path.of("build.gradle")); - assertTrue(build.contains("apply from: 'gradle/forge61-mavenizer.gradle'")); + assertTrue(build.contains("apply from: 'gradle/forge64-mavenizer.gradle'")); assertFalse(build.contains("renamer.classes(")); assertFalse(build.contains("toSrgFile")); assertTrue(build.contains("basePower-${project.poweradvantage_version}.jar")); @@ -40,9 +41,11 @@ public class BuildWorkflowContractTest { String source = Files.readString(Path.of(".github/workflows", name)); int pinCheck = source.indexOf("bash .github/verify-published-pins.sh"); assertTrue(name, pinCheck > 0 && pinCheck < source.indexOf("Check out released basePower source")); - assertTrue(name, source.contains("branches: [master-1.21.11")); - assertTrue(name, source.contains("java-version: '21.0.7+6.0.LTS'")); - assertTrue(name, source.contains("-Pforge61MavenizerJar=$MAVENIZER")); + assertTrue(name, source.contains("branches: [master-26.1.2")); + assertTrue(name, source.contains("java-version: '25.0.3+9.0.LTS'")); + assertFalse(name, source.contains("JAVA_HOME_8_X64")); + assertFalse(name, source.contains("java-version: '21")); + assertTrue(name, source.contains("-Pforge64MavenizerJar=$MAVENIZER")); assertTrue(name, source.contains("-PpowerAdvantageDeobfJar=$POWER_PLAYER")); assertFalse(name, source.contains("forge47")); } diff --git a/src/test/java/zone/moddev/mc/basesteam/resources/DedicatedSideContractTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/DedicatedSideContractTest.java index a535a9c..648bade 100644 --- a/src/test/java/zone/moddev/mc/basesteam/resources/DedicatedSideContractTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/resources/DedicatedSideContractTest.java @@ -12,7 +12,7 @@ public class DedicatedSideContractTest { private static final String ROOT = "/zone/moddev/mc/basesteam/"; private static final String ONLY_IN = "Lnet/minecraftforge/api/distmarker/OnlyIn;"; - @Test public void machineMenusStayCommonAndDecorationMethodsStayClientOnly() throws Exception { + @Test public void machineMenusRetainLegacyClientCallbacksWithoutUnsupportedSideMarkers() throws Exception { for (String name : List.of("BlastFurnace", "CoalBoiler", "ElectricBoiler", "GeothermalBoiler", "OilBoiler", "RockCrusher", "SteamDrill", "SteamPump", "SteamStill", "SteamTank")) { ClassNode menu = read("gui/" + name + "GUI"); @@ -23,17 +23,43 @@ public class DedicatedSideContractTest { .toList(); assertFalse("Expected at least the decoration override: " + name, clientSignatures.isEmpty()); for (var method : clientSignatures) { - assertTrue(name + "#" + method.name + " leaks a client class on the server", + assertFalse(name + "#" + method.name + " must not rely on unsupported mod-side stripping", clientOnly(method.visibleAnnotations) || clientOnly(method.invisibleAnnotations)); } } } - @Test public void renderingHelpersAndRegistrationAreEntirelyClientOnly() throws Exception { + @Test public void renderingHelpersRemainBehindTheActualClientDistributionGuard() throws Exception { for (String name : List.of("gui/GUIHelper", "graphics/ClientRegistration", "graphics/DrillBitRenderer")) { ClassNode node = read(name); - assertTrue(name, clientOnly(node.visibleAnnotations) || clientOnly(node.invisibleAnnotations)); + assertFalse(name, clientOnly(node.visibleAnnotations) || clientOnly(node.invisibleAnnotations)); } + var constructor = read("SteamAdvantage").methods.stream() + .filter(method -> method.name.equals("")).findFirst().orElseThrow(); + var instructions = java.util.Arrays.asList(constructor.instructions.toArray()); + int call = -1; + int guard = -1; + boolean literalClient = false; + for (int index = 0; index < instructions.size(); index++) { + var instruction = instructions.get(index); + if (instruction instanceof org.objectweb.asm.tree.FieldInsnNode field) { + if (field.owner.equals("net/minecraftforge/fml/loading/FMLEnvironment") && field.name.equals("dist")) + guard = index; + if (field.owner.equals("net/minecraftforge/api/distmarker/Dist") && field.name.equals("CLIENT")) + literalClient = true; + } + if (instruction instanceof org.objectweb.asm.tree.MethodInsnNode method + && method.owner.equals("zone/moddev/mc/basesteam/graphics/ClientRegistration") + && method.name.equals("register")) call = index; + } + assertTrue("Client registration must follow the native distribution read", guard >= 0 && guard < call && literalClient); + boolean guarded = false; + for (int index = guard + 1; index < call; index++) { + if (instructions.get(index) instanceof org.objectweb.asm.tree.JumpInsnNode jump + && jump.getOpcode() == org.objectweb.asm.Opcodes.IF_ACMPNE + && instructions.indexOf(jump.label) > call) guarded = true; + } + assertTrue("The non-client branch must skip the renderer registration call", guarded); } private static boolean clientOnly(List annotations) { @@ -46,7 +72,7 @@ private static ClassNode read(String path) throws Exception { try (InputStream input = DedicatedSideContractTest.class.getResourceAsStream(ROOT + path + ".class")) { assertNotNull(path, input); ClassNode node = new ClassNode(); - new ClassReader(input).accept(node, ClassReader.SKIP_CODE); + new ClassReader(input).accept(node, 0); return node; } } diff --git a/src/test/java/zone/moddev/mc/basesteam/resources/Native2612ToolchainContractTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/Native2612ToolchainContractTest.java new file mode 100644 index 0000000..09cee6c --- /dev/null +++ b/src/test/java/zone/moddev/mc/basesteam/resources/Native2612ToolchainContractTest.java @@ -0,0 +1,65 @@ +package zone.moddev.mc.basesteam.resources; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; +import org.junit.Test; +import static org.junit.Assert.*; + +public class Native2612ToolchainContractTest { + @Test public void targetUsesExactJava25AndRecommendedForge64() throws Exception { + Properties pins = new Properties(); + try (var reader = Files.newBufferedReader(Path.of("gradle.properties"))) { + pins.load(reader); + } + assertEquals("26.1.2", pins.getProperty("minecraft_version")); + assertEquals("[26.1.2]", pins.getProperty("minecraft_version_range")); + assertEquals("64.1.5", pins.getProperty("forge_version")); + assertEquals("[64.1.5,65)", pins.getProperty("forge_version_range")); + assertEquals("[64,65)", pins.getProperty("loader_version_range")); + assertEquals("20260409.101008", pins.getProperty("mcp_version")); + assertEquals("2.2.17.2601021", pins.getProperty("mod_version")); + assertEquals("25", pins.getProperty("java_version")); + assertEquals("25", pins.getProperty("gradle_java_version")); + assertEquals("25.0.3+9", pins.getProperty("java_toolchain_version")); + assertEquals("false", pins.getProperty("reobf")); + String build = Files.readString(Path.of("build.gradle")); + assertTrue(build.contains("fgtools.configure('slimelauncher')")); + assertTrue(build.contains("JavaVersion.VERSION_25")); + assertFalse(build.contains("JavaLanguageVersion.of(21)")); + assertFalse(build.contains("JavaLanguageVersion.of(8)")); + } + + @Test public void targetUsesSealedNativeMavenizerWithoutForge61Repairs() throws Exception { + String build = Files.readString(Path.of("build.gradle")); + assertTrue(build.contains("apply from: 'gradle/forge64-mavenizer.gradle'")); + String tool = Files.readString(Path.of("gradle/forge64-mavenizer.gradle")); + assertTrue(tool.contains("BB0DACB3DEB2A711CA67DD402D5FDAB5A9847C37C7B9B596DEC7CF37378FA085")); + assertTrue(tool.contains("21D1F5984E735BCEA1B1EFB9F965655796B39538FC91AAED1C3474004710423D")); + assertTrue(tool.contains("JavaLanguageVersion.of(25)")); + assertFalse(tool.contains("B36CA046")); + assertFalse(tool.contains("88F2EE4C")); + } + + @Test public void nativeNumberProviderFallbackPreservesBoundsButRejectsMalformedMaps() { + zone.moddev.mc.basesteam.testing.NativeTestBootstrap.start(); + var codec = net.minecraft.world.level.storage.loot.providers.number.NumberProviders.CODEC; + var ops = com.mojang.serialization.JsonOps.INSTANCE; + var original = com.google.gson.JsonParser.parseString( + "{\"type\":\"minecraft:uniform\",\"min\":15,\"max\":25}").getAsJsonObject(); + var fallback = original.deepCopy(); + fallback.addProperty("type", "missing_mod:unregistered_number_provider"); + var decoded = codec.parse(ops, fallback); + assertTrue(decoded.error().isEmpty()); + assertTrue(decoded.result().isPresent()); + assertEquals(original, codec.encodeStart(ops, decoded.getOrThrow()).getOrThrow()); + for (String invalid : new String[]{ + "{\"type\":\"missing_mod:unregistered_number_provider\"}", + "{\"type\":\"missing_mod:unregistered_number_provider\",\"min\":\"invalid\",\"max\":25}", + "{\"type\":\"missing_mod:unregistered_number_provider\",\"min\":15,\"max\":[]}"}) { + var rejected = codec.parse(ops, com.google.gson.JsonParser.parseString(invalid)); + assertTrue(invalid, rejected.error().isPresent()); + assertTrue(invalid, rejected.result().isEmpty()); + } + } +} diff --git a/src/test/java/zone/moddev/mc/basesteam/resources/NativeBlockItemTranslationTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/NativeBlockItemTranslationTest.java index 8a45f4c..5841bbc 100644 --- a/src/test/java/zone/moddev/mc/basesteam/resources/NativeBlockItemTranslationTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/resources/NativeBlockItemTranslationTest.java @@ -47,22 +47,29 @@ private static void isolated(String mode, String marker) throws Exception { .map(entry -> new java.io.File(entry).getAbsolutePath()) .collect(java.util.stream.Collectors.joining(java.io.File.pathSeparator)); var command = new ArrayList<>(java.util.List.of(javaExecutable.toString(), "-XX:ActiveProcessorCount=4", "-Xmx512m", + "-Dterminal.jline=false", "-Dbasesteam.resourcesRoot=" + Path.of("src/main/resources").toAbsolutePath(), "--add-opens", "java.base/java.lang.invoke=ALL-UNNAMED")); command.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments().stream() .filter(argument -> argument.startsWith("-javaagent:")).toList()); command.addAll(java.util.List.of("-cp", classpath, NativeBlockItemTranslationTest.class.getName(), mode)); Path log = evidence.resolve("native-factory.log"); - Process process = new ProcessBuilder(command).redirectErrorStream(true).redirectOutput(log.toFile()).start(); + ProcessBuilder builder = new ProcessBuilder(command).directory(evidence.toFile()) + .redirectInput(ProcessBuilder.Redirect.PIPE).redirectErrorStream(true).redirectOutput(log.toFile()); + builder.environment().put("TERM", "dumb"); + for (String name : java.util.List.of("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS")) builder.environment().remove(name); + Process process = builder.start(); if (!process.waitFor(90, TimeUnit.SECONDS)) { process.destroyForcibly().waitFor(); throw new AssertionError("Native BlockItem proof timed out: " + log); } String output = Files.readString(log); + zone.moddev.mc.basesteam.testing.NativePackResourceProof.requireCleanOutput(output, log); assertEquals(log + "\n" + output, 0, process.exitValue()); assertTrue(log + "\n" + output, output.contains(marker)); } public static void main(String[] args) throws Exception { + zone.moddev.mc.basesteam.testing.NativePackResourceProof.requireCleanJvmEnvironment(); if (args.length != 1) throw new IllegalArgumentException("Expected factory or negative proof"); NativeTestBootstrap.start(); Blocks.init(); @@ -81,24 +88,28 @@ public static void main(String[] args) throws Exception { @SuppressWarnings("unchecked") private static void proveFactory() throws Exception { - MappedRegistry items = new MappedRegistry<>(Registries.ITEM, Lifecycle.stable()); + MappedRegistry items = (MappedRegistry)net.minecraft.core.registries.BuiltInRegistries.ITEM; var constructor = RegisterEvent.class.getDeclaredConstructor(ResourceKey.class, ForgeRegistry.class, Registry.class); constructor.setAccessible(true); var visible = ItemGroups.class.getDeclaredField("VISIBLE_ITEMS"); visible.setAccessible(true); Set visibleItems = (Set)visible.get(null); try { - Blocks.registerBlockItems(constructor.newInstance(ForgeRegistries.Keys.ITEMS, null, items)); + Blocks.registerBlockItems(constructor.newInstance(ForgeRegistries.Keys.ITEMS, ForgeRegistries.ITEMS, items)); } finally { - for (Item item : items) visibleItems.remove(item); + items.keySet().stream().filter(id -> id.getNamespace().equals("basesteam")) + .forEach(id -> visibleItems.remove(items.getValue(id))); } + NativeTestBootstrap.bindRegisteredItemComponents(); assertEquals(Set.of("steam_pipe", "steam_track", "steam_boiler_coal", "steam_boiler_electric", "steam_boiler_geothermal", "steam_tank", "steam_furnace", "steam_crusher", "steam_drill", "drillbit", "platform", "steam_elevator", "steam_switch", "pump_pipe_steam", "steam_still", "steam_pump", "steam_boiler_oil"), - items.keySet().stream().map(Identifier::getPath).collect(java.util.stream.Collectors.toSet())); + items.keySet().stream().filter(id -> id.getNamespace().equals("basesteam")) + .map(Identifier::getPath).collect(java.util.stream.Collectors.toSet())); JsonObject english = english(); for (Identifier id : items.keySet()) { + if (!id.getNamespace().equals("basesteam")) continue; String key = "block.basesteam." + id.getPath(); assertEquals("basesteam", id.getNamespace()); assertTrue(items.getValue(id) instanceof BlockItem); @@ -115,6 +126,8 @@ private static void proveFactory() throws Exception { private static void proveNegative() throws Exception { BlockItem wrong = new BlockItem(Blocks.steam_pipe, new Item.Properties().setId(ResourceKey.create( Registries.ITEM, Identifier.fromNamespaceAndPath("basesteam", "steam_pipe")))); + ForgeRegistries.ITEMS.register(Identifier.fromNamespaceAndPath("basesteam", "steam_pipe"), wrong); + NativeTestBootstrap.bindRegisteredItemComponents(); assertEquals("item.basesteam.steam_pipe", wrong.getDescriptionId()); assertEquals("item.basesteam.steam_pipe", ((TranslatableContents)wrong.getDefaultInstance().getHoverName().getContents()).getKey()); @@ -124,6 +137,6 @@ private static void proveNegative() throws Exception { private static JsonObject english() throws Exception { return JsonParser.parseString(Files.readString( - Path.of("src/main/resources/assets/basesteam/lang/en_us.json"))).getAsJsonObject(); + Path.of(System.getProperty("basesteam.resourcesRoot"), "assets/basesteam/lang/en_us.json"))).getAsJsonObject(); } } diff --git a/src/test/java/zone/moddev/mc/basesteam/resources/NativeBlockPropertiesTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/NativeBlockPropertiesTest.java index fde65cb..3609003 100644 --- a/src/test/java/zone/moddev/mc/basesteam/resources/NativeBlockPropertiesTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/resources/NativeBlockPropertiesTest.java @@ -57,7 +57,7 @@ public static Collection cases() { // Native opacity is 1, not 0, for a non-solid block with a full-cube outline. int expected = height < 0 || height == 4 ? 1 : 0; for (BlockState state : states()) { - assertEquals(state.toString(), expected, state.getLightBlock()); + assertEquals(state.toString(), expected, state.getLightDampening()); } } diff --git a/src/test/java/zone/moddev/mc/basesteam/resources/NativeDistCleanerContractTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/NativeDistCleanerContractTest.java new file mode 100644 index 0000000..8f3b020 --- /dev/null +++ b/src/test/java/zone/moddev/mc/basesteam/resources/NativeDistCleanerContractTest.java @@ -0,0 +1,97 @@ +package zone.moddev.mc.basesteam.resources; + +import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.fml.loading.RuntimeDistCleaner; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; +import static org.junit.Assert.*; + +public class NativeDistCleanerContractTest { + private java.lang.reflect.Field nativeSide; + private Object previousSide; + + @Before public void captureSide() throws ReflectiveOperationException { + nativeSide = RuntimeDistCleaner.class.getDeclaredField("DIST"); + nativeSide.setAccessible(true); + previousSide = nativeSide.get(null); + } + + @After public void restoreSide() throws IllegalAccessException { + nativeSide.set(null, previousSide); + } + + @Test public void nativeClientCleanerAcceptsTheThreeClientDispatchedHelpers() throws Exception { + for (String name : List.of("gui/GUIHelper", "graphics/ClientRegistration", "graphics/DrillBitRenderer")) + assertEquals(name, ILaunchPluginService.ComputeFlags.NO_REWRITE, clean(read(name), Dist.CLIENT)); + } + + @Test public void nativeServerCleanerAcceptsCommonMenusAndMachineAccessors() throws Exception { + for (String name : List.of("BlastFurnace", "CoalBoiler", "ElectricBoiler", "GeothermalBoiler", + "OilBoiler", "RockCrusher", "SteamDrill", "SteamPump", "SteamStill", "SteamTank")) { + ClassNode menu = read("gui/" + name + "GUI"); + assertEquals(name, ILaunchPluginService.ComputeFlags.NO_REWRITE, clean(menu, Dist.DEDICATED_SERVER)); + for (var field : menu.fields) + assertFalse(name + " field " + field.name, field.desc.contains("net/minecraft/client/") || field.desc.contains("GUIContainer")); + } + assertEquals(ILaunchPluginService.ComputeFlags.NO_REWRITE, + clean(read("machines/BlastFurnaceTileEntity"), Dist.DEDICATED_SERVER)); + } + + @Test public void matchingClientTypeAnnotationIsRejectedByTheActualNativeCleaner() { + ClassNode node = fixture(); + node.visibleAnnotations = new ArrayList<>(List.of(clientMarker())); + assertThrows(UnsupportedOperationException.class, () -> clean(node, Dist.CLIENT)); + } + + @Test public void nativeServerRejectsUnsupportedClientMethodMarkersInsteadOfStrippingThem() { + ClassNode node = fixture(); + MethodNode method = new MethodNode(Opcodes.ACC_PUBLIC, "clientOnly", "()V", null, null); + method.visibleAnnotations = new ArrayList<>(List.of(clientMarker())); + node.methods.add(method); + assertThrows(UnsupportedOperationException.class, () -> clean(node, Dist.DEDICATED_SERVER)); + } + + private static int clean(ClassNode node, Dist side) { + RuntimeDistCleaner cleaner = new RuntimeDistCleaner(); + cleaner.getExtension().accept(side); + return cleaner.processClassWithFlags(ILaunchPluginService.Phase.AFTER, node, + Type.getObjectType(node.name), "baseSteam native side contract"); + } + + private static AnnotationNode clientMarker() { + AnnotationNode marker = new AnnotationNode(Type.getDescriptor(OnlyIn.class)); + marker.values = new ArrayList<>(List.of("value", new String[] {Type.getDescriptor(Dist.class), "CLIENT"})); + return marker; + } + + private static ClassNode fixture() { + ClassNode node = new ClassNode(); + node.version = 69; + node.access = Opcodes.ACC_PUBLIC; + node.name = "zone/moddev/mc/basesteam/testing/UnsupportedMarkerFixture"; + node.superName = "java/lang/Object"; + return node; + } + + private static ClassNode read(String name) throws Exception { + try (InputStream input = NativeDistCleanerContractTest.class.getResourceAsStream( + "/zone/moddev/mc/basesteam/" + name + ".class")) { + assertNotNull(name, input); + ClassNode node = new ClassNode(); + new ClassReader(input).accept(node, ClassReader.SKIP_CODE); + return node; + } + } +} diff --git a/src/test/java/zone/moddev/mc/basesteam/resources/NativeEnchantmentContractTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/NativeEnchantmentContractTest.java index c0d4806..66a6423 100644 --- a/src/test/java/zone/moddev/mc/basesteam/resources/NativeEnchantmentContractTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/resources/NativeEnchantmentContractTest.java @@ -47,6 +47,7 @@ public class NativeEnchantmentContractTest { if (!ForgeRegistries.ITEMS.containsKey(key)) ForgeRegistries.ITEMS.register(key, item); assertSame(item, ForgeRegistries.ITEMS.getValue(key)); }); + NativeTestBootstrap.bindRegisteredItemComponents(); previousTags = new HashMap<>(); BuiltInRegistries.ITEM.freeze(); BuiltInRegistries.ITEM.getTags().forEach(tag -> diff --git a/src/test/java/zone/moddev/mc/basesteam/resources/NativeFuelContractTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/NativeFuelContractTest.java index 57001a3..42c9bfb 100644 --- a/src/test/java/zone/moddev/mc/basesteam/resources/NativeFuelContractTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/resources/NativeFuelContractTest.java @@ -1,6 +1,10 @@ package zone.moddev.mc.basesteam.resources; import zone.moddev.mc.basesteam.testing.NativeTestBootstrap; +import zone.moddev.mc.basesteam.testing.NativeMachineFixture; +import zone.moddev.mc.basesteam.machines.BlastFurnaceTileEntity; +import zone.moddev.mc.basesteam.machines.CoalBoilerTileEntity; +import cyano.poweradvantage.api.simple.TileEntitySimplePowerMachine; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; @@ -27,7 +31,9 @@ import static org.junit.Assert.*; public class NativeFuelContractTest { + @org.junit.Rule public final NativeMachineFixture machineTypes = new NativeMachineFixture(); private static Map, List>> previousTags; + private static Map, List>> nativeFuelTags; @BeforeClass public static void bootstrapNativeFuel() { NativeTestBootstrap.start(); @@ -44,10 +50,15 @@ public class NativeFuelContractTest { BuiltInRegistries.ITEM.freeze(); BuiltInRegistries.ITEM.getTags().forEach(tag -> previousTags.put(tag.key(), tag.stream().toList())); - NativeTestBootstrap.bindItemTags(loaded); + nativeFuelTags = Map.copyOf(loaded); + NativeTestBootstrap.bindItemTags(nativeFuelTags); } } + @org.junit.Before public void bindFuelTagsAfterTheActualMachineFixturesAreInstalled() { + NativeTestBootstrap.bindItemTags(nativeFuelTags); + } + @AfterClass public static void restoreNativeTags() { if (previousTags != null) { NativeTestBootstrap.bindItemTags(previousTags); @@ -73,6 +84,110 @@ public class NativeFuelContractTest { }; ForgeRegistries.ITEMS.register(Identifier.fromNamespaceAndPath("basesteam_test", "fuel_override"), item); BuiltInRegistries.ITEM.freeze(); + NativeTestBootstrap.bindRegisteredItemComponents(); assertEquals(237, FurnaceFuelHelper.getBurnTime(new ItemStack(item))); } + + @Test public void coalBoilerConsumesLastCoalWithoutARemainder() throws Exception { + assertLastCoal(CoalBoilerTileEntity.class); + } + + @Test public void blastFurnaceConsumesLastCoalWithoutARemainder() throws Exception { + assertLastCoal(BlastFurnaceTileEntity.class); + } + + @Test public void coalBoilerConsumesExactlyOneFromLargerCoalStacks() throws Exception { + assertLargerCoalStacks(CoalBoilerTileEntity.class); + } + + @Test public void blastFurnaceConsumesExactlyOneFromLargerCoalStacks() throws Exception { + assertLargerCoalStacks(BlastFurnaceTileEntity.class); + } + + @Test public void coalBoilerReturnsExactlyOneBucketForLavaFuel() throws Exception { + assertLavaRemainder(CoalBoilerTileEntity.class); + } + + @Test public void blastFurnaceReturnsExactlyOneBucketForLavaFuel() throws Exception { + assertLavaRemainder(BlastFurnaceTileEntity.class); + } + + @Test public void bothActualFuelLookupsRejectWaterBucketsWithoutChangingThem() throws Exception { + ItemStack water = new ItemStack(Items.WATER_BUCKET); + assertEquals(0, FurnaceFuelHelper.getBurnTime(water)); + CoalBoilerTileEntity boiler = fueled(CoalBoilerTileEntity.class, water); + var coalLookup = CoalBoilerTileEntity.class.getDeclaredMethod("getFuelBurnTime"); + coalLookup.setAccessible(true); + assertEquals(0, coalLookup.invoke(boiler)); + assertSame(water, boiler.getItem(0)); + assertEquals(1, boiler.getItem(0).getCount()); + BlastFurnaceTileEntity furnace = fueled(BlastFurnaceTileEntity.class, water.copy()); + var furnaceLookup = BlastFurnaceTileEntity.class.getDeclaredMethod("getFuelBurnTime", ItemStack.class); + furnaceLookup.setAccessible(true); + assertEquals(0, furnaceLookup.invoke(furnace, furnace.getItem(0))); + assertTrue(furnace.getItem(0).is(Items.WATER_BUCKET)); + assertEquals(1, furnace.getItem(0).getCount()); + } + + private static void assertLastCoal(Class type) throws Exception { + ItemStack coal = new ItemStack(Items.COAL); + assertEquals(1600, FurnaceFuelHelper.getBurnTime(coal)); + assertNull("Coal has no native crafting remainder", coal.getCraftingRemainder()); + var machine = fueled(type, coal); + consume(machine); + assertSame(ItemStack.EMPTY, machine.getItem(0)); + assertEquals(0, machine.getItem(0).getCount()); + } + + private static void assertLargerCoalStacks(Class type) throws Exception { + for (int count : new int[]{2, 7}) { + ItemStack coal = new ItemStack(Items.COAL, count); + var machine = fueled(type, coal); + for (int remaining = count - 1; remaining >= 0; remaining--) { + consume(machine); + assertEquals(type.getSimpleName(), remaining, machine.getItem(0).getCount()); + if (remaining > 0) { + assertSame(coal, machine.getItem(0)); + assertTrue(machine.getItem(0).is(Items.COAL)); + assertNull(machine.getItem(0).getCraftingRemainder()); + } else { + assertSame(ItemStack.EMPTY, machine.getItem(0)); + } + } + } + } + + private static void assertLavaRemainder(Class type) throws Exception { + ItemStack previous = null; + for (int use = 0; use < 2; use++) { + ItemStack lava = new ItemStack(Items.LAVA_BUCKET); + assertEquals(20000, FurnaceFuelHelper.getBurnTime(lava)); + var template = lava.getCraftingRemainder(); + assertNotNull(template); + ItemStack expected = template.create(); + assertTrue(expected.is(Items.BUCKET)); + assertEquals(1, expected.getCount()); + var machine = fueled(type, lava); + consume(machine); + ItemStack actual = machine.getItem(0); + assertTrue(ItemStack.isSameItemSameComponents(expected, actual)); + assertEquals(1, actual.getCount()); + assertNotSame(lava, actual); + assertNotSame(previous, actual); + assertEquals(0, FurnaceFuelHelper.getBurnTime(actual)); + previous = actual; + } + } + + private static T fueled(Class type, ItemStack fuel) { + T machine = NativeMachineFixture.create(type); + machine.setItem(0, fuel); + return machine; + } + + private static void consume(TileEntitySimplePowerMachine machine) throws Exception { + var method = machine.getClass().getDeclaredMethod("decrementFuel"); + method.setAccessible(true); + method.invoke(machine); + } } diff --git a/src/test/java/zone/moddev/mc/basesteam/resources/NativeItemAtlasContractTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/NativeItemAtlasContractTest.java index 94bc16e..a51c90b 100644 --- a/src/test/java/zone/moddev/mc/basesteam/resources/NativeItemAtlasContractTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/resources/NativeItemAtlasContractTest.java @@ -1,6 +1,7 @@ package zone.moddev.mc.basesteam.resources; import com.google.gson.JsonParser; +import com.google.gson.JsonObject; import com.mojang.serialization.Dynamic; import com.mojang.serialization.JsonOps; import java.io.IOException; @@ -19,6 +20,8 @@ import net.minecraft.client.renderer.texture.atlas.SpriteSourceList; import net.minecraft.client.renderer.texture.atlas.SpriteSources; import net.minecraft.client.renderer.texture.atlas.sources.DirectoryLister; +import net.minecraft.client.renderer.texture.atlas.sources.SingleFile; +import net.minecraft.client.resources.model.sprite.Material; import net.minecraft.data.AtlasIds; import net.minecraft.network.chat.Component; import net.minecraft.resources.Identifier; @@ -47,29 +50,16 @@ public class NativeItemAtlasContractTest { var steam = new PathPackResources.PathResourcesSupplier(Path.of("src/main/resources")); var parent = new FilePackResources.FileResourcesSupplier(power); assertEquals(Identifier.withDefaultNamespace("items"), AtlasIds.ITEMS); - for (String directory : List.of("blocks", "materials")) { - assertThrows("Block/material directories must never be accepted by the item atlas", AssertionError.class, - () -> requireItemsOnly(List.of(new DirectoryLister(directory, directory + "/")))); - } - assertThrows("Even an items source must retain its exact sprite prefix", AssertionError.class, - () -> requireItemsOnly(List.of(new DirectoryLister("items", "")))); try (var resources = new MultiPackResourceManager(PackType.CLIENT_RESOURCES, List.of(parent.openPrimary(info("basepower")), steam.openPrimary(info("basesteam"))))) { - Set blockDirectories = new HashSet<>(); - Set itemDirectories = new HashSet<>(); - Set blockSprites = sprites(resources, AtlasIds.BLOCKS, blockDirectories); - Set itemSprites = sprites(resources, AtlasIds.ITEMS, itemDirectories); + Set registered = new HashSet<>(); + Set blockSprites = sprites(resources, AtlasIds.BLOCKS, registered); + Set itemSprites = sprites(resources, AtlasIds.ITEMS, registered); Set blockTextures = textures(Path.of("src/main/resources/assets/basesteam/models/block")); assertFalse("The block-model texture oracle must not be empty", blockTextures.isEmpty()); - Set duplicatedDirectories = new HashSet<>(blockDirectories); - duplicatedDirectories.retainAll(itemDirectories); - duplicatedDirectories.retainAll(Set.of("blocks", "materials")); - assertTrue("ITEMS must not shadow block/material directories: " + duplicatedDirectories, - duplicatedDirectories.isEmpty()); Set collisions = new HashSet<>(blockSprites); collisions.retainAll(itemSprites); - collisions.retainAll(blockTextures); - assertTrue("block_or_item must not resolve world-block textures through ITEMS: " + collisions, + assertTrue("Native sprite lookup must be unambiguous across both atlases: " + collisions, collisions.isEmpty()); Set expected = textures(Path.of("src/main/resources/assets/basesteam/models")); assertFalse("Inspect the actual Steam model texture declarations", expected.isEmpty()); @@ -82,14 +72,17 @@ public class NativeItemAtlasContractTest { Set missing = new HashSet<>(expected); missing.removeAll(allSprites); assertTrue("Textures absent from both actual native atlas sources: " + missing, missing.isEmpty()); - assertTrue("BlockSimpleModelWrapper requires the block atlas", blockSprites.containsAll(blockTextures)); - assertTrue("Block model materials must not resolve through ITEMS", - java.util.Collections.disjoint(blockTextures, itemSprites)); + for (Identifier id : expected) { + assertEquals("Native Material retains the exact sprite without an atlas field", id, + Material.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString('"' + id.toString() + '"')) + .getOrThrow().sprite()); + } Set itemIcons = expected.stream().filter(id -> id.getPath().startsWith("items/")) .collect(java.util.stream.Collectors.toSet()); assertFalse("The legacy generated-item icon oracle must not be empty", itemIcons.isEmpty()); assertTrue("Native item lookup uses ITEMS", itemSprites.containsAll(itemIcons)); - assertTrue("Legacy generated-item lookup retains BLOCKS compatibility", blockSprites.containsAll(itemIcons)); + assertTrue("Native item sprites must not also be registered in BLOCKS", + java.util.Collections.disjoint(blockSprites, itemIcons)); assertTrue(blockSprites.contains(Identifier.parse("basesteam:entity/drill_bit"))); assertFalse(itemSprites.contains(Identifier.parse("basesteam:entity/drill_bit"))); assertFalse(allSprites.contains(Identifier.parse("basesteam:missing_atlas_negative_control"))); @@ -102,19 +95,70 @@ public class NativeItemAtlasContractTest { } try (var resources = new MultiPackResourceManager(PackType.CLIENT_RESOURCES, List.of(steam.openPrimary(info("basesteam"))))) { - assertEquals("Steam contributes one item-directory definition", 1, + assertEquals("Steam contributes one explicit item-atlas definition", 1, resources.getResourceStack(Identifier.withDefaultNamespace("atlases/items.json")).size()); assertTrue("Shared materials cannot exist without the required Power pack", resources.getResource( Identifier.parse("basepower:textures/materials/wood.png")).isEmpty()); assertEquals("Without Power, the native item loader offers three icons plus missing-texture fallback", 4, SpriteSourceList.load(resources, AtlasIds.ITEMS).list(resources).size()); - Set available = new HashSet<>(sprites(resources, AtlasIds.BLOCKS, new HashSet<>())); - available.addAll(sprites(resources, AtlasIds.ITEMS, new HashSet<>())); + Set registered = new HashSet<>(); + Set available = new HashSet<>(sprites(resources, AtlasIds.BLOCKS, registered)); + available.addAll(sprites(resources, AtlasIds.ITEMS, registered)); Set missing = textures(Path.of("src/main/resources/assets/basesteam/models")); missing.removeAll(available); assertTrue("Omitting Power must still expose the missing shared material", - missing.contains(Identifier.parse("basesteam:materials/steel_dark_hole"))); + missing.contains(Identifier.parse("basepower:materials/wood"))); + } + } + + @Test public void directoriesForeignNamespacesAndAliasesAreRejectedBeforeNativeLoading() { + for (String source : List.of( + "{\"type\":\"minecraft:directory\",\"source\":\"items\",\"prefix\":\"items/\"}", + "{\"type\":\"minecraft:single\",\"resource\":\"basepower:items/steam_governor\"}", + "{\"type\":\"single\",\"resource\":\"basesteam:items/steam_governor\"}", + "{\"type\":\"minecraft:single\",\"resource\":\"basesteam:items/steam_governor\",\"sprite\":\"basesteam:items/alias\"}", + "{\"type\":\"minecraft:single\",\"resource\":\"basesteam:items/steam_governor\",\"unknown\":true}")) { + assertThrows(source, AssertionError.class, () -> requireOwnedSingles( + atlas(source), "basesteam", true, new HashSet<>(), id -> true)); + } + assertTrue("The native codec accepts directories, so the ownership contract is an additional required guard", + SpriteSources.FILE_CODEC.parse(new Dynamic<>(JsonOps.INSTANCE, + atlas("{\"type\":\"minecraft:directory\",\"source\":\"items\",\"prefix\":\"items/\"}"))) + .getOrThrow().getFirst() instanceof DirectoryLister); + } + + @Test public void missingDuplicateAndCrossAtlasSpritesAreRejected() { + String source = "{\"type\":\"minecraft:single\",\"resource\":\"basesteam:items/steam_governor\"}"; + assertThrows(AssertionError.class, () -> requireOwnedSingles( + atlas(source), "basesteam", true, new HashSet<>(), id -> false)); + assertThrows(AssertionError.class, () -> requireOwnedSingles( + atlas(source + "," + source), "basesteam", true, new HashSet<>(), id -> true)); + Set registered = new HashSet<>(); + requireOwnedSingles(atlas(source), "basesteam", false, registered, id -> true); + assertThrows(AssertionError.class, () -> requireOwnedSingles( + atlas(source), "basesteam", true, registered, id -> true)); + for (String id : List.of("basesteam:blocks/steam_pipe", "basesteam:materials/iron", "basesteam:entity/drill_bit")) { + assertThrows(id, AssertionError.class, () -> requireOwnedSingles(atlas( + "{\"type\":\"minecraft:single\",\"resource\":\"" + id + "\"}"), + "basesteam", true, new HashSet<>(), value -> true)); + } + } + + @Test public void malformedNativeAtlasAndMaterialTypesAreRejected() { + for (String json : List.of("{}", "{\"sources\":[]}", "{\"sources\":{}}", + "{\"sources\":[],\"extra\":1}", "{\"sources\":[null]}", + "{\"sources\":[{\"type\":\"minecraft:single\",\"resource\":3}]}")) { + assertThrows(json, AssertionError.class, () -> requireOwnedSingles( + JsonParser.parseString(json).getAsJsonObject(), "basesteam", true, new HashSet<>(), id -> true)); } + assertTrue(Material.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString("{\"sprite\":3}")).error().isPresent()); + assertTrue(Material.CODEC.parse(JsonOps.INSTANCE, + JsonParser.parseString("{\"sprite\":\"basesteam:materials/iron\",\"force_translucent\":\"bad\"}")) + .error().isPresent()); + Material material = Material.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString( + "{\"sprite\":\"basesteam:materials/iron\",\"force_translucent\":true}" )).getOrThrow(); + assertEquals(Identifier.parse("basesteam:materials/iron"), material.sprite()); + assertTrue(material.forceTranslucent()); } @Test public void missingVerifiedPowerPathIsRejected() { @@ -144,7 +188,7 @@ private static Path pinnedPower(Properties pins, String configured) throws Excep return power; } - private static Set sprites(ResourceManager resources, Identifier atlas, Set directories) + private static Set sprites(ResourceManager resources, Identifier atlas, Set registered) throws Exception { var definitions = resources.getResourceStack(atlas.withPrefix("atlases/").withSuffix(".json")); assertFalse("Missing native atlas " + atlas, definitions.isEmpty()); @@ -165,14 +209,15 @@ private static Set sprites(ResourceManager resources, Identifier atl try { for (var definition : definitions) { try (var reader = definition.openAsReader()) { - var sources = SpriteSources.FILE_CODEC.parse( - new Dynamic<>(JsonOps.INSTANCE, JsonParser.parseReader(reader))).getOrThrow(); - if (atlas.equals(AtlasIds.ITEMS)) requireItemsOnly(sources); + String owner = definition.sourcePackId().replace("mod:", ""); + assertTrue("Only the two verified mod packs may supply these definitions", + Set.of("basepower", "basesteam").contains(owner)); + var sources = requireOwnedSingles(JsonParser.parseReader(reader).getAsJsonObject(), owner, + atlas.equals(AtlasIds.ITEMS), registered, id -> resources.getResource( + id.withPrefix("textures/").withSuffix(".png")).isPresent()); + assertEquals("Exact current native atlas inventory", owner.equals("basesteam") + ? (atlas.equals(AtlasIds.ITEMS) ? 3 : 24) : (atlas.equals(AtlasIds.ITEMS) ? 6 : 34), sources.size()); for (SpriteSource source : sources) { - if (source instanceof DirectoryLister directory) { - assertEquals(directory.sourcePath() + "/", directory.idPrefix()); - directories.add(directory.sourcePath()); - } source.run(resources, output); } } @@ -183,12 +228,35 @@ private static Set sprites(ResourceManager resources, Identifier atl } } - private static void requireItemsOnly(List sources) { - assertEquals("Only the native items directory belongs in this atlas", 1, sources.size()); - assertTrue("Single block/material sprites must not shadow the block atlas", sources.getFirst() instanceof DirectoryLister); - DirectoryLister source = (DirectoryLister)sources.getFirst(); - assertEquals("items", source.sourcePath()); - assertEquals("items/", source.idPrefix()); + private static List requireOwnedSingles(JsonObject atlas, String owner, boolean items, + Set registered, java.util.function.Predicate exists) { + assertEquals(Set.of("sources"), atlas.keySet()); + assertTrue(atlas.get("sources").isJsonArray()); + assertFalse(atlas.getAsJsonArray("sources").isEmpty()); + for (var value : atlas.getAsJsonArray("sources")) { + assertTrue(value.isJsonObject()); + var source = value.getAsJsonObject(); + assertEquals(Set.of("type", "resource"), source.keySet()); + assertTrue(source.get("type").isJsonPrimitive() && source.getAsJsonPrimitive("type").isString()); + assertEquals("minecraft:single", source.get("type").getAsString()); + assertTrue(source.get("resource").isJsonPrimitive() && source.getAsJsonPrimitive("resource").isString()); + Identifier id = Identifier.parse(source.get("resource").getAsString()); + assertEquals("Each pack must own every sprite it registers", owner, id.getNamespace()); + assertFalse(id.getPath().contains("..")); + if (items) assertTrue("Only item sprites belong in ITEMS", id.getPath().startsWith("items/")); + assertTrue("Duplicate sprite within/across atlases: " + id, registered.add(id)); + assertTrue("Missing physical texture " + id, exists.test(id)); + } + var sources = SpriteSources.FILE_CODEC.parse(new Dynamic<>(JsonOps.INSTANCE, atlas)).getOrThrow(); + for (SpriteSource source : sources) { + assertTrue(source instanceof SingleFile); + assertTrue(((SingleFile)source).spriteId().isEmpty()); + } + return sources; + } + + private static JsonObject atlas(String sources) { + return JsonParser.parseString("{\"sources\":[" + sources + "]}").getAsJsonObject(); } private static Set textures(Path root) throws IOException { diff --git a/src/test/java/zone/moddev/mc/basesteam/resources/NativeLootContractTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/NativeLootContractTest.java index f3abf36..eba012f 100644 --- a/src/test/java/zone/moddev/mc/basesteam/resources/NativeLootContractTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/resources/NativeLootContractTest.java @@ -39,10 +39,10 @@ public class NativeLootContractTest { } } - @Test public void packUsesNative12111ResourceAndDataFormats() throws Exception { + @Test public void packUsesNative2612DataFormatAndForgeClientMetadata() throws Exception { JsonObject pack = read(ROOT.resolve("pack.mcmeta")).getAsJsonObject("pack"); - assertEquals("[94,1]", pack.getAsJsonArray("min_format").toString()); - assertEquals("[94,1]", pack.getAsJsonArray("max_format").toString()); + assertEquals("[101,1]", pack.getAsJsonArray("min_format").toString()); + assertEquals("[101,1]", pack.getAsJsonArray("max_format").toString()); assertFalse(pack.has("pack_format")); assertFalse(pack.has("supported_formats")); assertFalse(pack.has("forge:server_data_pack_format")); diff --git a/src/test/java/zone/moddev/mc/basesteam/resources/NativePackFormatContractTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/NativePackFormatContractTest.java index 5d6db52..22974ca 100644 --- a/src/test/java/zone/moddev/mc/basesteam/resources/NativePackFormatContractTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/resources/NativePackFormatContractTest.java @@ -41,13 +41,13 @@ public class NativePackFormatContractTest { assertEquals(server, PackMetadataSection.forPackType(PackType.SERVER_DATA).codec() .parse(JsonOps.INSTANCE, pack).getOrThrow()); var version = SharedConstants.getCurrentVersion(); - assertEquals(PackFormat.of(75, 0), version.packVersion(PackType.CLIENT_RESOURCES)); - assertEquals(PackFormat.of(94, 1), version.packVersion(PackType.SERVER_DATA)); + assertEquals(PackFormat.of(84, 0), version.packVersion(PackType.CLIENT_RESOURCES)); + assertEquals(PackFormat.of(101, 1), version.packVersion(PackType.SERVER_DATA)); assertTrue(client.supportedFormats().isValueInRange(version.packVersion(PackType.CLIENT_RESOURCES))); assertTrue(server.supportedFormats().isValueInRange(version.packVersion(PackType.SERVER_DATA))); - assertFalse(client.supportedFormats().isValueInRange(PackFormat.of(74))); - assertFalse(server.supportedFormats().isValueInRange(PackFormat.of(95))); - assertFalse(client.supportedFormats().isValueInRange(PackFormat.of(94, 1))); - assertFalse(server.supportedFormats().isValueInRange(PackFormat.of(75, 0))); + assertFalse(client.supportedFormats().isValueInRange(PackFormat.of(83))); + assertFalse(server.supportedFormats().isValueInRange(PackFormat.of(102))); + assertFalse(client.supportedFormats().isValueInRange(PackFormat.of(101, 1))); + assertFalse(server.supportedFormats().isValueInRange(PackFormat.of(84, 0))); } } diff --git a/src/test/java/zone/moddev/mc/basesteam/resources/NativePackRangeContractTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/NativePackRangeContractTest.java index 3440153..a30fff0 100644 --- a/src/test/java/zone/moddev/mc/basesteam/resources/NativePackRangeContractTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/resources/NativePackRangeContractTest.java @@ -20,11 +20,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))); } } @@ -46,11 +46,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/basesteam/resources/NativeRegistrationLifecycleTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/NativeRegistrationLifecycleTest.java index 4679d0a..70d7991 100644 --- a/src/test/java/zone/moddev/mc/basesteam/resources/NativeRegistrationLifecycleTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/resources/NativeRegistrationLifecycleTest.java @@ -27,8 +27,11 @@ public class NativeRegistrationLifecycleTest { .anyMatch(call -> call.owner.startsWith("net/minecraftforge/registries/"))); assertFalse(calls("init/Blocks", "registerBlockItems").stream().anyMatch(call -> initializes(call, "Blocks"))); assertFalse(calls("init/Items", "init").stream().anyMatch(call -> initializes(call, "Blocks"))); - assertFalse(calls("init/Recipes", "init").stream() - .anyMatch(call -> initializes(call, "Blocks") || initializes(call, "Items"))); + assertFalse(calls("SteamAdvantage", "commonSetup").stream() + .anyMatch(call -> initializes(call, "Blocks") || initializes(call, "Items") + || call.owner.equals(INIT + "Recipes"))); + assertNull("The discarded pre-component recipe bridge must not be packaged", + NativeRegistrationLifecycleTest.class.getResource("/" + INIT + "Recipes.class")); } @Test public void commonSetupQueuesGuiDefinitions() throws Exception { diff --git a/src/test/java/zone/moddev/mc/basesteam/resources/NativeRequiredDependencyContractTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/NativeRequiredDependencyContractTest.java index d9cae1d..f7968c2 100644 --- a/src/test/java/zone/moddev/mc/basesteam/resources/NativeRequiredDependencyContractTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/resources/NativeRequiredDependencyContractTest.java @@ -46,9 +46,9 @@ private static void requireBaseMetals(ModInfo info) { assertTrue("Base Metals is REQUIRED, not an optional installation scope", dependency.isMandatory()); assertEquals(IModInfo.Ordering.AFTER, dependency.getOrdering()); assertEquals(IModInfo.DependencySide.BOTH, dependency.getSide()); - assertEquals("[3.1.2.121111,4.0.0)", dependency.getVersionRange().toString()); - assertTrue(dependency.getVersionRange().containsVersion(new DefaultArtifactVersion("3.1.2.121111"))); - assertFalse(dependency.getVersionRange().containsVersion(new DefaultArtifactVersion("3.1.1.121111"))); + assertEquals("[3.1.2.2601021,4.0.0)", dependency.getVersionRange().toString()); + assertTrue(dependency.getVersionRange().containsVersion(new DefaultArtifactVersion("3.1.2.2601021"))); + assertFalse(dependency.getVersionRange().containsVersion(new DefaultArtifactVersion("3.1.1.2601021"))); assertFalse(dependency.getVersionRange().containsVersion(new DefaultArtifactVersion("4.0.0"))); } @@ -61,7 +61,7 @@ private static IModInfo.ModVersion baseMetals(ModInfo info) { private static ModInfo parse(String metadata) throws Exception { Path root = Path.of("build/native-required-dependency-metadata"); Files.createDirectories(root); - Path jar = Files.createTempDirectory(root, "forge61-").resolve("metadata.jar"); + Path jar = Files.createTempDirectory(root, "forge64-").resolve("metadata.jar"); Manifest manifest = new Manifest(); manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); manifest.getMainAttributes().putValue("Automatic-Module-Name", "basesteam"); @@ -83,11 +83,11 @@ private static ModInfo parseJar(Path jar) { private static String fixture(String dependency) { return """ modLoader="javafml" - loaderVersion="[61,62)" + loaderVersion="[64,65)" license="GPL-3.0-only" [[mods]] modId="basesteam" - version="2.2.16.121111" + version="2.2.17.2601021" displayName="baseSteam" """ + dependency; } @@ -97,7 +97,7 @@ private static String dependency(boolean required) { [[dependencies.basesteam]] modId="basemetals" mandatory=%s - versionRange="[3.1.2.121111,4.0.0)" + versionRange="[3.1.2.2601021,4.0.0)" ordering="AFTER" side="BOTH" """.formatted(required); diff --git a/src/test/java/zone/moddev/mc/basesteam/resources/OptionalCrushingRecipeContractTest.java b/src/test/java/zone/moddev/mc/basesteam/resources/OptionalCrushingRecipeContractTest.java index 124a65a..f1306cf 100644 --- a/src/test/java/zone/moddev/mc/basesteam/resources/OptionalCrushingRecipeContractTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/resources/OptionalCrushingRecipeContractTest.java @@ -117,4 +117,21 @@ private static boolean baseGuard(JsonElement value) { assertEquals(amount, output.has("count") ? output.get("count").getAsInt() : 1); } } + + @Test public void commonSetupDoesNotConstructDiscardedPreComponentRecipeStacks() throws Exception { + var type = new org.objectweb.asm.tree.ClassNode(); + new org.objectweb.asm.ClassReader("zone.moddev.mc.basesteam.SteamAdvantage").accept(type, 0); + var setup = type.methods.stream().filter(method -> method.name.equals("commonSetup")).findFirst().orElseThrow(); + for (var instruction : setup.instructions) { + if (instruction instanceof org.objectweb.asm.tree.MethodInsnNode call) { + assertNotEquals("The obsolete recipe bridge constructs unbound mod ItemStacks during common setup", + "zone/moddev/mc/basesteam/init/Recipes", call.owner); + } + if (instruction instanceof org.objectweb.asm.tree.TypeInsnNode allocation) { + assertNotEquals("Common setup must not allocate mod stacks before native component binding", + "net/minecraft/world/item/ItemStack", allocation.desc); + } + } + assertFalse(Files.exists(Path.of("src/main/java/zone/moddev/mc/basesteam/init/Recipes.java"))); + } } diff --git a/src/test/java/zone/moddev/mc/basesteam/testing/EclipseTestInventory.java b/src/test/java/zone/moddev/mc/basesteam/testing/EclipseTestInventory.java new file mode 100644 index 0000000..afc4540 --- /dev/null +++ b/src/test/java/zone/moddev/mc/basesteam/testing/EclipseTestInventory.java @@ -0,0 +1,166 @@ +package zone.moddev.mc.basesteam.testing; + +import com.google.gson.GsonBuilder; +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.TreeMap; +import java.util.TreeSet; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import org.junit.runner.Description; +import org.junit.runner.Request; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.w3c.dom.Element; + +/** Compares all fresh compiled JUnit methods with the complete Gradle XML inventory. */ +public final class EclipseTestInventory { + private static final Set REVIEWED_PARAMETERIZED = Set.of( + "zone.moddev.mc.basesteam.machines.WaterBoilerFluidBoundaryTest", + "zone.moddev.mc.basesteam.machines.WaterBoilerTankNotificationTest", + "zone.moddev.mc.basesteam.resources.NativeBlockPropertiesTest"); + private EclipseTestInventory() {} + + public record Inventory(Map> suites, int tests, Set reviewedParameterized) {} + + public static void main(String[] arguments) throws Exception { + if (arguments.length != 3) throw new IllegalArgumentException("classes, Gradle XML directory, receipt"); + Inventory inventory = verify(Path.of(arguments[0]), Path.of(arguments[1])); + Files.writeString(Path.of(arguments[2]), new GsonBuilder().setPrettyPrinting().create().toJson(inventory) + "\n"); + System.out.println("ECJ_COMPLETE_INVENTORY suites=" + inventory.suites().size() + " tests=" + inventory.tests()); + } + + public static Inventory verify(Path classes, Path results) throws Exception { + Map> compiled = compiledTests(classes); + Map> reported = reportedTests(results); + if (!compiled.equals(reported)) { + throw new AssertionError("Incomplete or stale Gradle inventory: compiled=" + compiled + ", reported=" + reported); + } + Set reviewed = new TreeSet<>(compiled.keySet()); + reviewed.retainAll(REVIEWED_PARAMETERIZED); + return new Inventory(compiled, compiled.values().stream().mapToInt(Set::size).sum(), reviewed); + } + + private static Map> compiledTests(Path root) throws Exception { + Map> suites = new TreeMap<>(); + try (var paths = Files.walk(root)) { + for (Path path : paths.filter(file -> file.toString().endsWith(".class")).sorted().toList()) { + ClassNode type = new ClassNode(); + new ClassReader(Files.readAllBytes(path)).accept(type, + ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + String name = type.name.replace('/', '.'); + boolean parameterized = false; + if (has(type.visibleAnnotations, "Lorg/junit/runner/RunWith;")) { + AnnotationNode runner = type.visibleAnnotations.stream() + .filter(annotation -> "Lorg/junit/runner/RunWith;".equals(annotation.desc)).findFirst().orElseThrow(); + parameterized = REVIEWED_PARAMETERIZED.contains(name) && runner.values != null + && runner.values.size() == 2 && "value".equals(runner.values.get(0)) + && org.objectweb.asm.Type.getType("Lorg/junit/runners/Parameterized;").equals(runner.values.get(1)); + if (!parameterized) throw new AssertionError("Review custom JUnit discovery: " + type.name); + } + if ("junit/framework/TestCase".equals(type.superName)) { + throw new AssertionError("Review custom JUnit discovery: " + type.name); + } + Set methods = new TreeSet<>(); + for (var method : type.methods) { + if (!has(method.visibleAnnotations, "Lorg/junit/Test;")) continue; + if (has(type.visibleAnnotations, "Lorg/junit/Ignore;") + || has(method.visibleAnnotations, "Lorg/junit/Ignore;") + || (method.access & Opcodes.ACC_PUBLIC) == 0 + || (method.access & (Opcodes.ACC_STATIC | Opcodes.ACC_ABSTRACT)) != 0 + || !"()V".equals(method.desc) || !methods.add(method.name)) { + throw new AssertionError("Ignored or unsupported JUnit method: " + type.name + "." + method.name); + } + } + if (methods.isEmpty()) continue; + if (type.name.contains("$") || (type.access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_INTERFACE)) != 0 + || !"java/lang/Object".equals(type.superName)) { + throw new AssertionError("Review nested or inherited JUnit discovery: " + type.name); + } + if (parameterized) { + Set expanded = new TreeSet<>(); + collectParameterized(Request.aClass(Class.forName(name)).getRunner().getDescription(), name, expanded); + for (String method : methods) { + if (expanded.stream().noneMatch(test -> test.startsWith(method + "["))) { + throw new AssertionError("Missing parameterized method: " + name + "." + method); + } + } + for (String test : expanded) { + int bracket = test.indexOf('['); + if (bracket < 0 || !methods.contains(test.substring(0, bracket))) { + throw new AssertionError("Unexpected parameterized method: " + name + "." + test); + } + } + methods = expanded; + } + if (suites.put(name, methods) != null) { + throw new AssertionError("Duplicate compiled test class: " + type.name); + } + } + } + if (suites.isEmpty()) throw new AssertionError("Empty compiled JUnit inventory"); + return suites; + } + + private static void collectParameterized(Description description, String type, Set methods) { + if (description.isTest()) { + if (!type.equals(description.getClassName()) || description.getMethodName() == null + || !methods.add(description.getMethodName())) { + throw new AssertionError("Invalid parameterized description: " + description); + } + } else { + description.getChildren().forEach(child -> collectParameterized(child, type, methods)); + } + } + + private static Map> reportedTests(Path root) throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + Map> suites = new TreeMap<>(); + try (var paths = Files.list(root)) { + for (Path path : paths.filter(file -> file.getFileName().toString().startsWith("TEST-") + && file.toString().endsWith(".xml")).sorted().toList()) { + Element suite = factory.newDocumentBuilder().parse(path.toFile()).getDocumentElement(); + String name = suite.getAttribute("name"); + if (!"testsuite".equals(suite.getTagName()) || name.isEmpty() + || !path.getFileName().toString().equals("TEST-" + name + ".xml")) { + throw new AssertionError("Invalid Gradle test suite: " + path); + } + for (String attribute : List.of("failures", "errors", "skipped")) { + if (!"0".equals(suite.getAttribute(attribute))) { + throw new AssertionError("Non-green Gradle inventory: " + path + " " + attribute); + } + } + Set methods = new TreeSet<>(); + var cases = suite.getElementsByTagName("testcase"); + for (int index = 0; index < cases.getLength(); index++) { + Element test = (Element) cases.item(index); + String method = test.getAttribute("name"); + if (!name.equals(test.getAttribute("classname")) || method.isEmpty() + || !methods.add(method) || test.getElementsByTagName("skipped").getLength() != 0 + || test.getElementsByTagName("failure").getLength() != 0 + || test.getElementsByTagName("error").getLength() != 0) { + throw new AssertionError("Invalid Gradle test case: " + path + " " + method); + } + } + if (methods.isEmpty() || methods.size() != Integer.parseInt(suite.getAttribute("tests")) + || suites.put(name, methods) != null) { + throw new AssertionError("Duplicate or inconsistent Gradle suite: " + path); + } + } + } + if (suites.isEmpty()) throw new AssertionError("Empty Gradle JUnit inventory"); + return suites; + } + + private static boolean has(List annotations, String descriptor) { + return annotations != null && annotations.stream().anyMatch(annotation -> descriptor.equals(annotation.desc)); + } +} diff --git a/src/test/java/zone/moddev/mc/basesteam/testing/EclipseTestInventoryTest.java b/src/test/java/zone/moddev/mc/basesteam/testing/EclipseTestInventoryTest.java new file mode 100644 index 0000000..18e9933 --- /dev/null +++ b/src/test/java/zone/moddev/mc/basesteam/testing/EclipseTestInventoryTest.java @@ -0,0 +1,118 @@ +package zone.moddev.mc.basesteam.testing; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import static org.junit.Assert.*; + +public class EclipseTestInventoryTest { + @Rule public TemporaryFolder temporary = new TemporaryFolder(); + + @Test public void includesEveryAnnotatedClassWithoutPackageOrFilenameFilters() throws Exception { + Path classes = temporary.newFolder("classes").toPath(); + Path results = temporary.newFolder("results").toPath(); + type(classes, "zone/moddev/mc/basesteam/NormalTest", "java/lang/Object", null, false, "one"); + type(classes, "outside/prefix/UnusualSpec", "java/lang/Object", null, false, "first", "second"); + xml(results, "zone.moddev.mc.basesteam.NormalTest", 0, "one"); + xml(results, "outside.prefix.UnusualSpec", 0, "first", "second"); + var inventory = EclipseTestInventory.verify(classes, results); + assertEquals(2, inventory.suites().size()); + assertEquals(3, inventory.tests()); + } + + @Test public void rejectsPrefixFilteredAndPartialMethodResults() throws Exception { + Path classes = temporary.newFolder("classes").toPath(); + Path results = temporary.newFolder("results").toPath(); + type(classes, "owned/NormalTest", "java/lang/Object", null, false, "one"); + type(classes, "outside/UnusualSpec", "java/lang/Object", null, false, "first", "second"); + xml(results, "owned.NormalTest", 0, "one"); + assertThrows(AssertionError.class, () -> EclipseTestInventory.verify(classes, results)); + xml(results, "outside.UnusualSpec", 0, "first"); + assertThrows(AssertionError.class, () -> EclipseTestInventory.verify(classes, results)); + xml(results, "outside.UnusualSpec", 0, "first", "second"); + assertEquals(3, EclipseTestInventory.verify(classes, results).tests()); + } + + @Test public void rejectsStaleSuitesAndSameCountWrongMethods() throws Exception { + Path classes = temporary.newFolder("classes").toPath(); + Path results = temporary.newFolder("results").toPath(); + type(classes, "owned/Example", "java/lang/Object", null, false, "actual"); + xml(results, "owned.Stale", 0, "actual"); + assertThrows(AssertionError.class, () -> EclipseTestInventory.verify(classes, results)); + Files.delete(results.resolve("TEST-owned.Stale.xml")); + xml(results, "owned.Example", 0, "wrong"); + assertThrows(AssertionError.class, () -> EclipseTestInventory.verify(classes, results)); + } + + @Test public void rejectsSkippedFailedAndEmptyResults() throws Exception { + Path classes = temporary.newFolder("classes").toPath(); + Path results = temporary.newFolder("results").toPath(); + type(classes, "owned/Example", "java/lang/Object", null, false, "actual"); + assertThrows(AssertionError.class, () -> EclipseTestInventory.verify(classes, results)); + xml(results, "owned.Example", 1, "actual"); + assertThrows(AssertionError.class, () -> EclipseTestInventory.verify(classes, results)); + Path suite = results.resolve("TEST-owned.Example.xml"); + Files.writeString(suite, Files.readString(suite).replace("skipped=\"1\"", "skipped=\"0\"") + .replace("failures=\"0\"", "failures=\"1\"")); + assertThrows(AssertionError.class, () -> EclipseTestInventory.verify(classes, results)); + } + + @Test public void rejectsCustomNestedAndInheritedDiscovery() throws Exception { + int index = 0; + for (String[] fixture : List.of( + new String[]{"owned/Custom", "java/lang/Object", "Lorg/junit/runner/RunWith;"}, + new String[]{"owned/Outer$Nested", "java/lang/Object", ""}, + new String[]{"owned/Inherited", "owned/Parent", ""}, + new String[]{"owned/OldStyle", "junit/framework/TestCase", ""})) { + Path classes = temporary.newFolder("classes" + index).toPath(); + Path results = temporary.newFolder("results" + index++).toPath(); + type(classes, fixture[0], fixture[1], fixture[2].isEmpty() ? null : fixture[2], false, "one"); + xml(results, fixture[0].replace('/', '.'), 0, "one"); + assertThrows(AssertionError.class, () -> EclipseTestInventory.verify(classes, results)); + } + } + + @Test public void rejectsIgnoredMethodsAndIgnoredClasses() throws Exception { + for (boolean classIgnore : List.of(false, true)) { + Path classes = temporary.newFolder("classes" + classIgnore).toPath(); + Path results = temporary.newFolder("results" + classIgnore).toPath(); + type(classes, "owned/Ignored", "java/lang/Object", + classIgnore ? "Lorg/junit/Ignore;" : null, !classIgnore, "one"); + xml(results, "owned.Ignored", 0, "one"); + assertThrows(AssertionError.class, () -> EclipseTestInventory.verify(classes, results)); + } + } + + private static void type(Path root, String name, String parent, String annotation, boolean ignoreMethod, + String... methods) throws Exception { + ClassWriter writer = new ClassWriter(0); + writer.visit(Opcodes.V25, Opcodes.ACC_PUBLIC, name, null, parent, null); + if (annotation != null) writer.visitAnnotation(annotation, true).visitEnd(); + for (String nameOfMethod : methods) { + var method = writer.visitMethod(Opcodes.ACC_PUBLIC, nameOfMethod, "()V", null, null); + method.visitAnnotation("Lorg/junit/Test;", true).visitEnd(); + if (ignoreMethod) method.visitAnnotation("Lorg/junit/Ignore;", true).visitEnd(); + method.visitCode(); + method.visitInsn(Opcodes.RETURN); + method.visitMaxs(0, 1); + method.visitEnd(); + } + writer.visitEnd(); + Path file = root.resolve(name + ".class"); + Files.createDirectories(file.getParent()); + Files.write(file, writer.toByteArray()); + } + + private static void xml(Path root, String name, int skipped, String... methods) throws Exception { + StringBuilder xml = new StringBuilder(""); + for (String method : methods) xml.append(""); + Files.writeString(root.resolve("TEST-" + name + ".xml"), xml.append("").toString()); + } +} diff --git a/src/test/java/zone/moddev/mc/basesteam/testing/NativePackResourceProof.java b/src/test/java/zone/moddev/mc/basesteam/testing/NativePackResourceProof.java index 98d5909..ac3d450 100644 --- a/src/test/java/zone/moddev/mc/basesteam/testing/NativePackResourceProof.java +++ b/src/test/java/zone/moddev/mc/basesteam/testing/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-"); String classpath = Arrays.stream(System.getProperty("basesteam.nativePackClasspath", System.getProperty("java.class.path")).split(java.util.regex.Pattern.quote(java.io.File.pathSeparator))) .map(entry -> new java.io.File(entry).getAbsolutePath()) @@ -62,6 +62,7 @@ public static synchronized JsonObject read() throws Exception { evidence.toString()).directory(evidence.toFile()) .redirectInput(ProcessBuilder.Redirect.PIPE).redirectErrorStream(true).redirectOutput(log.toFile()); builder.environment().put("TERM", "dumb"); + for (String name : List.of("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS")) builder.environment().remove(name); Process process = builder.start(); if (!process.waitFor(90, TimeUnit.SECONDS)) { process.destroyForcibly().waitFor(); @@ -74,7 +75,7 @@ public static synchronized JsonObject read() throws Exception { return receipt.deepCopy(); } - static void requireCleanOutput(String output, Path log) { + public static void requireCleanOutput(String output, Path log) { if (java.util.regex.Pattern.compile("(?m)(?:\\[[^\\r\\n]*\\b(?:WARN|ERROR|FATAL)\\b[^\\r\\n]*\\]|\\bmain (?:WARN|ERROR|FATAL)\\b)") .matcher(output).find()) { throw new AssertionError("Native Forge resource diagnostic in " + log + "\n" + output); @@ -82,8 +83,8 @@ static void requireCleanOutput(String output, Path log) { } public static void main(String[] args) throws Exception { + requireCleanJvmEnvironment(); SharedConstants.tryDetectVersion(); - Bootstrap.bootStrap(); Path resources = Path.of(args[0]); Path evidence = Path.of(args[1]); Path jar = evidence.resolve("basesteam-resource-contract.jar"); @@ -100,7 +101,8 @@ public static void main(String[] args) throws Exception { } ModFile mod = new ModFile(SecureJar.from(jar), new ForgeDevLocator(), ModFileParser::modsTomlParser); if (mod.getModFileInfo().showAsResourcePack()) throw new AssertionError("Steam must use Forge's hidden client pack"); - ModList.of(List.of(mod), mod.getModInfos().stream().map(ModInfo.class::cast).toList()); + installResourceFixture(mod); + Bootstrap.bootStrap(); List diagnostics = new ArrayList<>(); AbstractAppender appender = new AbstractAppender("SteamPackContract", null, PatternLayout.createDefaultLayout(), false, Property.EMPTY_ARRAY) { @@ -113,6 +115,11 @@ public static void main(String[] args) throws Exception { logger.addAppender(appender); JsonObject result = new JsonObject(); JsonObject child = new JsonObject(); + JsonObject environment = new JsonObject(); + for (String name : List.of("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS")) { + environment.addProperty(name, System.getenv(name)); + } + child.add("jvmOptionEnvironment", environment); child.addProperty("terminalJline", System.getProperty("terminal.jline")); child.addProperty("term", System.getenv("TERM")); child.addProperty("consoleAttached", System.console() != null); @@ -147,8 +154,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) { @@ -195,4 +202,28 @@ public static void main(String[] args) throws Exception { if (!diagnostics.isEmpty()) throw new AssertionError("Native Forge resource diagnostics: " + diagnostics); System.out.println("BASESTEAM_NATIVE_PACK_RESOURCES_PASS " + result); } + + public static void requireCleanJvmEnvironment() { + for (String name : List.of("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS")) { + if (System.getenv(name) != null) throw new AssertionError("Native proof inherited " + name); + } + } + + private static void installResourceFixture(ModFile mod) throws ReflectiveOperationException { + // Isolated reader fixture, not a dependency/startup test. Forge 64 removed ModList.of. + Class state = Class.forName("net.minecraftforge.fml.loading.ModSorter$State"); + var constructor = state.getDeclaredConstructor(List.class, List.class); + constructor.setAccessible(true); + Object discovery = constructor.newInstance(List.of(mod), + mod.getModInfos().stream().map(ModInfo.class::cast).toList()); + Class loading = Class.forName("net.minecraftforge.fml.loading.LoadingModListImpl"); + var init = loading.getDeclaredMethod("init", state, net.minecraftforge.fml.loading.EarlyLoadingException.class); + init.setAccessible(true); + init.invoke(null, discovery, null); + ModList.init(); + if (ModList.getModFiles().size() != 1 || ModList.getModFiles().getFirst() != mod.getModFileInfo() + || !net.minecraftforge.fml.loading.LoadingModList.getErrors().isEmpty()) { + throw new AssertionError("The native Forge 64 resource fixture changed"); + } + } } diff --git a/src/test/java/zone/moddev/mc/basesteam/testing/NativePackResourceProofTest.java b/src/test/java/zone/moddev/mc/basesteam/testing/NativePackResourceProofTest.java index 429eb24..57e79e8 100644 --- a/src/test/java/zone/moddev/mc/basesteam/testing/NativePackResourceProofTest.java +++ b/src/test/java/zone/moddev/mc/basesteam/testing/NativePackResourceProofTest.java @@ -15,6 +15,9 @@ public class NativePackResourceProofTest { assertEquals("dumb", child.get("term").getAsString()); assertFalse("The child must not attach a console", child.get("consoleAttached").getAsBoolean()); assertFalse("JLine must not construct a terminal", child.get("terminalCreated").getAsBoolean()); + for (String name : java.util.List.of("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS")) { + assertTrue(name, child.getAsJsonObject("jvmOptionEnvironment").get(name).isJsonNull()); + } assertEquals(0, proof.getAsJsonArray("warningsAndErrors").size()); for (PackType type : PackType.values()) { assertTrue(type.toString(), proof.getAsJsonObject(type.name()).get("resourcesVerified").getAsInt() > 0); diff --git a/src/test/java/zone/moddev/mc/basesteam/testing/NativeTestBootstrap.java b/src/test/java/zone/moddev/mc/basesteam/testing/NativeTestBootstrap.java index e614176..3b62e84 100644 --- a/src/test/java/zone/moddev/mc/basesteam/testing/NativeTestBootstrap.java +++ b/src/test/java/zone/moddev/mc/basesteam/testing/NativeTestBootstrap.java @@ -28,6 +28,11 @@ public static RegistryAccess.Frozen registries() { return RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); } + public static void bindRegisteredItemComponents() { + BuiltInRegistries.DATA_COMPONENT_INITIALIZERS.build(net.minecraft.data.registries.VanillaRegistries.createLookup()) + .forEach(net.minecraft.core.component.DataComponentInitializers.PendingComponents::apply); + } + public static synchronized void registerVanillaFluidTypes() throws ReflectiveOperationException { start(); if (ForgeMod.LAVA_TYPE.isPresent()) return; diff --git a/tools/run-packaged-server-smoke.ps1 b/tools/run-packaged-server-smoke.ps1 index a9d6720..127a37d 100644 --- a/tools/run-packaged-server-smoke.ps1 +++ b/tools/run-packaged-server-smoke.ps1 @@ -2,7 +2,7 @@ param( [Parameter(Mandatory)][string]$ForgeInstall, [Parameter(Mandatory)][ValidatePattern('^[a-zA-Z0-9_-]+$')][string]$RunId, - [string]$JavaExe = "$env:USERPROFILE/.jdks/temurin-21.0.7+6/bin/java.exe", + [string]$JavaExe = "$env:USERPROFILE/.jdks/jdk-25.0.3+9/bin/java.exe", [string]$PowerJar, [string]$BaseMetalsJar, [string]$OreSpawnJar, @@ -12,19 +12,24 @@ param( ) $ErrorActionPreference = 'Stop' +$jvmEnvironment = [ordered]@{} +foreach ($name in @('JAVA_TOOL_OPTIONS', 'JDK_JAVA_OPTIONS', '_JAVA_OPTIONS')) { + $jvmEnvironment[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') + if ($null -ne $jvmEnvironment[$name]) { throw "Packaged qualification must not inherit $name" } +} if ($WithoutBaseMetals) { throw 'Base Metals is required by baseSteam; a no-Base-Metals runtime is not supported.' } $project = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path $forge = (Resolve-Path -LiteralPath $ForgeInstall).Path $pins = Get-Content -Raw -LiteralPath (Join-Path $project 'gradle.properties') | ConvertFrom-StringData -if ($pins.minecraft_version -ne '1.21.11' -or $pins.forge_version -ne '61.1.0' -or +if ($pins.minecraft_version -ne '26.1.2' -or $pins.forge_version -ne '64.1.5' -or $pins.poweradvantage_source_ref -cnotmatch '^[0-9a-f]{40}$') { - throw 'An exact Forge 61.1.0 candidate and committed Power source pin are required.' + throw 'An exact Forge 64.1.5 candidate and committed Power source pin are required.' } $java = (Resolve-Path -LiteralPath $JavaExe).Path $javaVersion = (& $java -version 2>&1 | Out-String).Trim() -if ($LASTEXITCODE -ne 0 -or $javaVersion -notmatch 'Temurin-21\.0\.7\+6') { +if ($LASTEXITCODE -ne 0 -or $javaVersion -notmatch 'Temurin-25\.0\.3\+9') { throw "Unexpected test JVM: $javaVersion" } @@ -39,10 +44,10 @@ if (!$PowerJar) { $PowerJar = Join-Path $project "../../PowerAdvantage/build/libs/basePower-$($pins.poweradvantage_version).jar" } if (!$BaseMetalsJar) { - $BaseMetalsJar = Join-Path $project "../../../../BaseMetals/.verification-basemetals12111/Advantage shared hook/build/libs/BaseMetals-$($pins.basemetals_version).jar" + $BaseMetalsJar = Join-Path $project "../../../../BaseMetals/BaseMetals 2612/BaseMetals/build/libs/BaseMetals-$($pins.basemetals_version).jar" } if (!$OreSpawnJar) { - $OreSpawnJar = Join-Path $project "../../../../MinecraftMineralogy/OS12111/OreSpawn/build/libs/OreSpawn-$($pins.orespawn_version).jar" + $OreSpawnJar = Join-Path $project "../../../../MinecraftMineralogy/OS2612/OreSpawn/build/libs/OreSpawn-$($pins.orespawn_version).jar" } $mods = [ordered]@{ $steamName = @{ Path = (Join-Path $project "build/libs/$steamName"); Sha256 = $steamSha } @@ -60,7 +65,7 @@ $forgeJars = [ordered]@{ } foreach ($file in $forgeJars.Keys) { if ($forgeJars[$file] -cnotmatch '^[A-F0-9]{64}$') { - throw "Supply the coordinated Forge 61.1.0 runtime hash in gradle.properties: $file" + throw "Supply the coordinated Forge 64.1.5 runtime hash in gradle.properties: $file" } if ((Get-FileHash -LiteralPath (Join-Path $forge $file)).Hash -cne $forgeJars[$file]) { throw "Installed Forge runtime hash mismatch: $file" @@ -85,6 +90,8 @@ $receipt = [ordered]@{ forge = $forgeVersion forgeJars = $forgeJars java = $javaVersion + jvmOptionEnvironment = $jvmEnvironment + jvmArguments = @('-Xms512m', '-Xmx3g', '-XX:ActiveProcessorCount=4', '-Djava.net.preferIPv6Addresses=system') mods = $mods world = (Join-Path $run 'steam-smoke') transport = $(if ($UseNioTransport) { 'NIO' } else { 'native-availability-selection' }) diff --git a/tools/verify-eclipse-ecj.ps1 b/tools/verify-eclipse-ecj.ps1 index 9744ef1..35ce578 100644 --- a/tools/verify-eclipse-ecj.ps1 +++ b/tools/verify-eclipse-ecj.ps1 @@ -1,11 +1,16 @@ [CmdletBinding()] param( [Parameter(Mandatory)][ValidatePattern('^[a-zA-Z0-9_-]+$')][string]$RunId, - [string]$JavaExe = "$env:USERPROFILE/.jdks/temurin-21.0.7+6/bin/java.exe", + [string]$JavaExe = "$env:USERPROFILE/.jdks/jdk-25.0.3+9/bin/java.exe", [string]$CompilerJar = "$env:USERPROFILE/.p2/pool/plugins/org.eclipse.jdt.core.compiler.batch_3.46.0.v20260528-0407.jar" ) $ErrorActionPreference = 'Stop' +$jvmEnvironment = [ordered]@{} +foreach ($name in @('JAVA_TOOL_OPTIONS', 'JDK_JAVA_OPTIONS', '_JAVA_OPTIONS')) { + $jvmEnvironment[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') + if ($null -ne $jvmEnvironment[$name]) { throw "ECJ qualification must not inherit $name" } +} $project = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path $java = (Resolve-Path -LiteralPath $JavaExe).Path $compiler = (Resolve-Path -LiteralPath $CompilerJar).Path @@ -14,7 +19,7 @@ if ($compilerHash -cne 'D0D43F8E2D7003E5EFED612E2CBB5F01870043397D8F1BBE536FD912 throw 'The installed Eclipse ECJ 3.46.0 compiler does not match the qualified tool.' } $version = (& $java -version 2>&1 | Out-String).Trim() -if ($LASTEXITCODE -ne 0 -or $version -notmatch 'Temurin-21\.0\.7\+6') { throw "Unexpected JVM: $version" } +if ($LASTEXITCODE -ne 0 -or $version -notmatch 'Temurin-25\.0\.3\+9') { throw "Unexpected JVM: $version" } $pins = Get-Content -Raw -LiteralPath (Join-Path $project 'gradle.properties') | ConvertFrom-StringData [xml]$classpath = Get-Content -Raw -LiteralPath (Join-Path $project '.classpath') $entries = @($classpath.classpath.classpathentry) @@ -25,6 +30,9 @@ $receipt = [ordered]@{ status = 'FAILED' commit = (& git -C $project rev-parse HEAD) java = $version + jvmOptionEnvironment = $jvmEnvironment + compilerJvmArguments = @('-XX:ActiveProcessorCount=4', '-Xmx1G') + unitJvmArguments = @('-XX:ActiveProcessorCount=4', '-Xmx1536m') compiler = @{ path = $compiler; sha256 = $compilerHash } classpathSha256 = (Get-FileHash -LiteralPath (Join-Path $project '.classpath') -Algorithm SHA256).Hash powerSource = $pins.poweradvantage_source_ref @@ -65,7 +73,7 @@ try { $output = Join-Path $run $scope New-Item -ItemType Directory -Path $output | Out-Null if ($scope -ne 'main') { $libraries = @((Join-Path $run 'main')) + $libraries } - $arguments = @('-21', '-proc:none', '-encoding', 'UTF-8', '-classpath', ('"' + ($libraries -join ';') + '"'), + $arguments = @('-25', '-proc:none', '-encoding', 'UTF-8', '-classpath', ('"' + ($libraries -join ';') + '"'), '-d', ('"' + $output + '"')) + @($sources | ForEach-Object { '"' + $_ + '"' }) $argumentFile = Join-Path $run "$scope.args" $arguments | Set-Content -LiteralPath $argumentFile -Encoding utf8 @@ -85,18 +93,60 @@ try { } $runtime = @((Join-Path $run 'test'), (Join-Path $run 'main'), (Join-Path $project 'build/resources/main')) $runtime += @($receipt.scopes.test.libraries | Where-Object { $_ -ne (Join-Path $run 'main') }) - $tests = @($receipt.scopes.test.sources | Where-Object { $_.path -like '*Test.java' } | ForEach-Object { - $source = Get-Content -Raw -LiteralPath $_.path - if ($source -notmatch '(?m)^\s*package\s+([\w.]+)\s*;') { throw "Missing package in $($_.path)" } - $Matches[1] + '.' + [IO.Path]::GetFileNameWithoutExtension($_.path) + $results = Join-Path $project 'build/test-results/test' + $xml = @(Get-ChildItem -LiteralPath $results -Filter TEST-*.xml | Sort-Object Name) + if ($xml.Count -lt 2) { throw 'A complete Gradle XML inventory is required before independent ECJ qualification' } + $receipt.gradleResults = @($xml | ForEach-Object { + @{ path = $_.FullName; sha256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash } }) + $inventory = Join-Path $run 'complete-inventory.json' + $inventoryMain = 'zone.moddev.mc.basesteam.testing.EclipseTestInventory' + & $java -XX:ActiveProcessorCount=4 -cp ($runtime -join ';') $inventoryMain (Join-Path $run 'test') $results $inventory ` + *> (Join-Path $run 'complete-inventory.log') + if ($LASTEXITCODE -ne 0) { throw "Compiled JUnit inventory differs from complete Gradle results; see $run/complete-inventory.log" } + $receipt.inventory = Get-Content -Raw -LiteralPath $inventory | ConvertFrom-Json + $tests = @($receipt.inventory.suites.PSObject.Properties.Name | Sort-Object) + $receipt.inventoryNegativeControls = @() + foreach ($negative in @('omitted-suite', 'omitted-method', 'stale-method')) { + $negativeResults = Join-Path $run $negative + New-Item -ItemType Directory -Path $negativeResults | Out-Null + $xml | Copy-Item -Destination $negativeResults + $first = Join-Path $negativeResults $xml[0].Name + if ($negative -eq 'omitted-suite') { + Remove-Item -LiteralPath $first + } else { + [xml]$document = Get-Content -Raw -LiteralPath $first + $method = @($document.testsuite.testcase)[0] + if ($negative -eq 'omitted-method') { + $null = $document.testsuite.RemoveChild($method) + $document.testsuite.SetAttribute('tests', ([int]$document.testsuite.tests - 1).ToString()) + } else { + $method.SetAttribute('name', 'staleInventoryNegativeControl') + } + $document.Save($first) + } + & $java -XX:ActiveProcessorCount=4 -cp ($runtime -join ';') $inventoryMain (Join-Path $run 'test') $negativeResults ` + (Join-Path $run "$negative.json") *> (Join-Path $run "$negative.log") + if ($LASTEXITCODE -eq 0 -or (Get-Content -Raw -LiteralPath (Join-Path $run "$negative.log")) -notmatch + 'Incomplete or stale Gradle inventory|Duplicate or inconsistent Gradle suite') { + throw "ECJ inventory accepted or failed for the wrong reason: $negative" + } + $receipt.inventoryNegativeControls += $negative + } $unitReceipt = Join-Path $run 'junit.json' - & $java -XX:ActiveProcessorCount=4 -Xmx1536m "-javaagent:$bootstrap" --add-opens java.base/java.lang.invoke=ALL-UNNAMED ` + $powerJar = @($receipt.scopes.test.libraries | Where-Object { [IO.Path]::GetFileName($_) -like 'basePower-*.jar' }) + if ($powerJar.Count -ne 1) { throw 'ECJ unit runtime must retain exactly one audited native Power jar' } + & $java -XX:ActiveProcessorCount=4 -Xmx1536m "-javaagent:$bootstrap" "-Dbasesteam.nativePowerJar=$($powerJar[0])" --add-opens java.base/java.lang.invoke=ALL-UNNAMED ` -cp ($runtime -join ';') zone.moddev.mc.basesteam.testing.EclipseUnitLauncher $unitReceipt @tests ` *> (Join-Path $run 'junit.log') $receipt.unitExitCode = $LASTEXITCODE if ($LASTEXITCODE -ne 0) { throw "ECJ-compiled JUnit failed; see $run/junit.log" } $receipt.unit = Get-Content -Raw -LiteralPath $unitReceipt | ConvertFrom-Json + if ($receipt.unit.tests -ne $receipt.inventory.tests -or $receipt.unit.ignored -ne 0 -or + $receipt.unit.assumptions -ne 0 -or $receipt.unit.failures -ne 0 -or + (@($receipt.unit.classes | Sort-Object) -join ',') -cne ($tests -join ',')) { + throw 'ECJ JUnit execution did not match the complete compiled test inventory' + } $receipt.status = 'ECJ_ALL_SCOPES_AND_UNIT_PASS' Write-Output "$($receipt.status): $run" } finally { diff --git a/tools/verify-native-port.js b/tools/verify-native-port.js index 8d53439..1ec04ef 100644 --- a/tools/verify-native-port.js +++ b/tools/verify-native-port.js @@ -25,7 +25,7 @@ for (const name of files(resources).filter(name => /\.(json|mcmeta)$/.test(name) json.set(name, JSON.parse(text)); } assert.deepEqual(json.get(`${resources}/pack.mcmeta`).pack, { - min_format: [94, 1], max_format: [94, 1], description: 'baseSteam' + min_format: [101, 1], max_format: [101, 1], description: 'baseSteam' }); const metadata = read(`${resources}/META-INF/mods.toml`); assert.match(metadata, /^showAsResourcePack=false$/m); @@ -113,6 +113,7 @@ for (const id of [...pickaxe.values.map(value => value.split(':')[1]), for (const name of source) { assert(!/ResourceLocation|net\.minecraftforge\.eventbus\.api\.(?:IEventBus|SubscribeEvent)/.test(read(name)), name); } -console.log(`BASESTEAM_NATIVE12111_SOURCE_PASS json=${json.size} java=${source.length} factories=${ids.length} harvest=${pickaxe.values.length} testMethods=${testCount}`); +console.log(`BASESTEAM_NATIVE2612_SOURCE_PASS json=${json.size} java=${source.length} factories=${ids.length} harvest=${pickaxe.values.length} testMethods=${testCount}`); console.log('Source checks only. Compilation, executed tests, packaged runtime and manual acceptance are separate.'); require('./verify-enchantment-data'); +require('./verify-villager-trades'); diff --git a/tools/verify-villager-trades.js b/tools/verify-villager-trades.js new file mode 100644 index 0000000..57ea3cf --- /dev/null +++ b/tools/verify-villager-trades.js @@ -0,0 +1,65 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const root = path.resolve(__dirname, '..', 'src/main/resources/data'); +const uniform = (min, max) => ({ type: 'minecraft:uniform', min, max }); +const sale = (item, cost, modifiers = []) => ({ + wants: { id: 'minecraft:emerald', count: cost }, + gives: { id: `basesteam:${item}`, count: 1 }, + max_uses: 7, reputation_discount: 0, xp: 1, + given_item_modifiers: modifiers +}); +const count = (min, max) => [{ function: 'minecraft:set_count', count: uniform(min, max) }]; +const expectedTrades = { + steam_governor: sale('steam_governor', 1, count(1, 4)), + steam_pipe: sale('steam_pipe', 1, count(4, 8)), + blackpowder_cartridge: sale('blackpowder_cartridge', 1, count(5, 7)), + musket: sale('musket', uniform(10, 15)), + enchanted_musket: sale('musket', uniform(15, 25), [{ + function: 'minecraft:enchant_with_levels', levels: uniform(5, 19), + options: '#minecraft:non_treasure', include_additional_cost_component: true + }]) +}; +const expectedTags = { + 'armorer/level_1': ['steam_governor'], + 'toolsmith/level_1': ['steam_governor', 'blackpowder_cartridge', 'musket'], + 'weaponsmith/level_1': ['steam_governor', 'blackpowder_cartridge', 'musket'], + 'librarian/level_1': ['steam_governor', 'steam_pipe'], + 'cleric/level_3': ['enchanted_musket'] +}; +function validate(trades, tags) { + assert.deepEqual(trades, expectedTrades, 'Keep every legacy quantity, inclusive price, uses, XP and enchantment surcharge'); + assert.deepEqual(tags, Object.fromEntries(Object.entries(expectedTags).map(([key, values]) => + [key, { replace: false, values: values.map(value => `basesteam:${value}`) }]))); +} +function jsonFiles(directory, prefix = '') { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { + const name = prefix + entry.name; + return entry.isDirectory() ? jsonFiles(path.join(directory, entry.name), name + '/') : + entry.name.endsWith('.json') ? [[name.slice(0, -5), JSON.parse(fs.readFileSync(path.join(directory, entry.name), 'utf8'))]] : []; + }); +} +const trades = Object.fromEntries(jsonFiles(path.join(root, 'basesteam/villager_trade'))); +const tags = Object.fromEntries(jsonFiles(path.join(root, 'minecraft/tags/villager_trade'))); +validate(trades, tags); +const controls = [ + data => { data.trades.steam_governor.given_item_modifiers[0].count.max = 5; }, + data => { data.trades.musket.wants.count.min = 9; }, + data => { data.trades.steam_pipe.max_uses = 8; }, + data => { data.trades.steam_governor.xp = 2; }, + data => { data.trades.musket.reputation_discount = 0.05; }, + data => { data.trades.enchanted_musket.given_item_modifiers[0].include_additional_cost_component = false; }, + data => { data.trades.enchanted_musket.given_item_modifiers[0].levels.max = 20; }, + data => { data.tags['cleric/level_1'] = data.tags['cleric/level_3']; delete data.tags['cleric/level_3']; }, + data => { data.tags['librarian/level_1'].replace = true; }, + data => { data.trades.steam_governor.unrecognized = true; } +]; +for (const mutate of controls) { + const data = structuredClone({ trades, tags }); + mutate(data); + assert.throws(() => validate(data.trades, data.tags), assert.AssertionError); +} +console.log(`BASESTEAM_NATIVE2612_TRADE_DATA_PASS definitions=5 placements=10 negativeControls=${controls.length}`); +console.log('Exact data contract only; native codec and generated-offer runtime checks remain separate.');