diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fa868247..39ec71a84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,31 +1,51 @@ -name: Base Metals 26.2 CI +name: Base Metals 26.3 CI on: push: branches: - - master-26.2 + - master-26.3 - 'feature/**' pull_request: branches: - - master-26.2 + - master-26.3 permissions: contents: read concurrency: - group: basemetals-26.2-${{ github.workflow }}-${{ github.ref }} + group: basemetals-26.3-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: name: Build, test, and audit - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 timeout-minutes: 90 + env: + LIBGL_ALWAYS_SOFTWARE: '1' + GALLIUM_DRIVER: llvmpipe + SDL_VIDEO_DRIVER: x11 + SDL_VIDEO_FORCE_EGL: '1' steps: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Prepare Mesa software rendering + run: | + sudo apt-get update + sudo apt-get install --yes --no-install-recommends libgl1-mesa-dri libglx-mesa0 mesa-utils + xvfb-run -a -s '-screen 0 1280x1024x24 +extension GLX +render -noreset' glxinfo -B + + - name: Check the virtual display supports Minecraft's OpenGL context + run: | + native="$RUNNER_TEMP/basemetals-sdl-linux.jar" + curl --fail --location --retry 3 --output "$native" \ + https://libraries.minecraft.net/org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-linux.jar + echo "E1880683A4AF33299E9A1B8D7EC1799BED8FB709064EA08DCC2A15CD1101C0B3 $native" | sha256sum --check --strict + timeout 30s xvfb-run -a -s '-screen 0 1280x1024x24 +extension GLX +render -noreset' \ + python3 tools/validation/check_sdl_graphics.py "$native" + - name: Install pinned Java 25 runtime and toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: @@ -49,7 +69,7 @@ jobs: "$RUNNER_TEMP/nbt-tests/bin/python" -m unittest discover -s tools/validation -p 'test_*.py' - name: Test upgrade log boundaries - run: node --test tools/validation/test_painting_registry.js tools/validation/test_runtime_log.js + run: node --test tools/validation/test_painting_registry.js tools/validation/test_runtime_log.js tools/validation/test_vanilla_registry.js - name: Build, test, and audit first candidate run: | @@ -59,9 +79,9 @@ jobs: --no-daemon --stacktrace first="$RUNNER_TEMP/basemetals-first" mkdir -p "$first" - cp build/libs/BaseMetals-3.1.1.2602001.jar "$first/" - cp build/libs/BaseMetals-3.1.1.2602001-sources.jar "$first/" - cp build/libs/BaseMetals-3.1.1.2602001-javadoc.jar "$first/" + cp build/libs/BaseMetals-3.1.1.2603001.jar "$first/" + cp build/libs/BaseMetals-3.1.1.2603001-sources.jar "$first/" + cp build/libs/BaseMetals-3.1.1.2603001-javadoc.jar "$first/" cp build/release/SHA256SUMS "$first/" - name: Rebuild and prove byte-identical archives @@ -69,9 +89,9 @@ jobs: ./gradlew clean assemble verifyReleaseArtifacts writeReleaseChecksums \ -PorespawnVerificationRepository="${{ steps.orespawn.outputs.repository }}" \ --no-daemon --stacktrace - cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.1.1.2602001.jar" build/libs/BaseMetals-3.1.1.2602001.jar - cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.1.1.2602001-sources.jar" build/libs/BaseMetals-3.1.1.2602001-sources.jar - cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.1.1.2602001-javadoc.jar" build/libs/BaseMetals-3.1.1.2602001-javadoc.jar + cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.1.1.2603001.jar" build/libs/BaseMetals-3.1.1.2603001.jar + cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.1.1.2603001-sources.jar" build/libs/BaseMetals-3.1.1.2603001-sources.jar + cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.1.1.2603001-javadoc.jar" build/libs/BaseMetals-3.1.1.2603001-javadoc.jar cmp "$RUNNER_TEMP/basemetals-first/SHA256SUMS" build/release/SHA256SUMS - name: Run all required GameTests @@ -83,14 +103,14 @@ jobs: - name: Install official Forge packaged runtimes id: forge-runtimes run: | - server_runtime="$RUNNER_TEMP/forge-26.2-65.1.0-server" - client_runtime="$RUNNER_TEMP/forge-26.2-65.1.0-client" - installer="$RUNNER_TEMP/forge-26.2-65.1.0-installer.jar" + server_runtime="$RUNNER_TEMP/forge-26.3-66.0.0-server" + client_runtime="$RUNNER_TEMP/forge-26.3-66.0.0-client" + installer="$RUNNER_TEMP/forge-26.3-66.0.0-installer.jar" mkdir -p "$server_runtime" "$client_runtime" curl --fail --location --retry 3 \ --output "$installer" \ - https://maven.minecraftforge.net/net/minecraftforge/forge/26.2-65.1.0/forge-26.2-65.1.0-installer.jar - echo "813FBBFECC3086A492783B9F5023AE15007737B42E17AAB75F48BA77403314DF $installer" | sha256sum --check --strict + https://maven.minecraftforge.net/net/minecraftforge/forge/26.3-66.0.0/forge-26.3-66.0.0-installer.jar + echo "C6173EEA56AED0B947722F314542196E28A35285844961D517C7FB571492BBE3 $installer" | sha256sum --check --strict ( cd "$server_runtime" java -jar "$installer" --installServer @@ -111,7 +131,9 @@ jobs: - name: Run exact packaged integrated-world client smoke run: >- - timeout --kill-after=30s 15m xvfb-run -a ./gradlew packagedClientStartupSmoke + timeout --kill-after=30s 15m xvfb-run -a + -s '-screen 0 1280x1024x24 +extension GLX +render -noreset' + ./gradlew packagedClientStartupSmoke -PvirtualDisplaySmoke=true -PpackagedForgeClientRuntime=${{ steps.forge-runtimes.outputs.client_runtime }} -PorespawnVerificationRepository=${{ steps.orespawn.outputs.repository }} @@ -119,7 +141,9 @@ jobs: - name: Run bounded development integrated-world client smoke run: >- - timeout --kill-after=30s 15m xvfb-run -a ./gradlew runClient + timeout --kill-after=30s 15m xvfb-run -a + -s '-screen 0 1280x1024x24 +extension GLX +render -noreset' + ./gradlew runClient -PvirtualDisplaySmoke=true -PintegratedWorldSmoke=true -PintegratedWorldSmokeName=basemetals-ci-${{ github.run_id }} @@ -135,13 +159,13 @@ jobs: - name: Upload audited release candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: BaseMetals-26.2-${{ github.sha }} + name: BaseMetals-26.3-${{ github.sha }} if-no-files-found: error retention-days: 30 path: | - build/libs/BaseMetals-3.1.1.2602001.jar - build/libs/BaseMetals-3.1.1.2602001-sources.jar - build/libs/BaseMetals-3.1.1.2602001-javadoc.jar + build/libs/BaseMetals-3.1.1.2603001.jar + build/libs/BaseMetals-3.1.1.2603001-sources.jar + build/libs/BaseMetals-3.1.1.2603001-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt @@ -149,7 +173,7 @@ jobs: if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: BaseMetals-26.2-diagnostics-${{ github.sha }} + name: BaseMetals-26.3-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 7bb158a99..50322ad2d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -3,11 +3,11 @@ name: CodeQL on: push: branches: - - master-26.2 + - master-26.3 - 'feature/**' pull_request: branches: - - master-26.2 + - master-26.3 permissions: actions: read diff --git a/.github/workflows/validate-gradle-build.yml b/.github/workflows/validate-gradle-build.yml index d8bacce9a..d77ba0452 100644 --- a/.github/workflows/validate-gradle-build.yml +++ b/.github/workflows/validate-gradle-build.yml @@ -3,11 +3,11 @@ name: Validate Gradle Wrapper on: push: branches: - - master-26.2 + - master-26.3 - 'feature/**' pull_request: branches: - - master-26.2 + - master-26.3 permissions: contents: read diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d7289ae53..f2ecdba8d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,13 @@ +Base Metals 3.1.1.2603001 + +- Port the released 26.2 version to Minecraft 26.3 and Forge 66, keeping Java 25. +- Keep raw metals, ore overlays, filled buckets, High/Low Fantasy and the existing gameplay fixes. +- Update recipes, loot, screens and save handling for the newer Minecraft formats. +- Keep coal and charcoal fuels, tool interactions and enchanted smith trades working with the new item components. +- Use published OreSpawn 4.1.0.2603001 with the same eleven rules and copper defaults. +- Read the new block-state save format while keeping older worlds, current player data and upgrade backups intact. +- Skip dungeon placement in protected imported terrain, while leaving new chunks and new underground areas available for generation. + Base Metals 3.1.1.2602001 - Port the released 26.1.2 version to Minecraft 26.2 and Forge 65, keeping Java 25. diff --git a/README.md b/README.md index 18a85fcab..d614dd6e4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Discord](https://img.shields.io/badge/Discord-MMD-green.svg?style=flat&logo=Discord)](https://discord.moddev.zone) [![CurseForge downloads](https://cf.way2muchnoise.eu/full_base-metals_downloads.svg)](https://www.curseforge.com/minecraft/mc-mods/base-metals) [![Supported Minecraft versions](https://cf.way2muchnoise.eu/versions/Minecraft_base-metals_all.svg)](https://www.curseforge.com/minecraft/mc-mods/base-metals) -[![Build, test, and audit](https://github.com/MinecraftModDevelopmentMods/BaseMetals/actions/workflows/ci.yml/badge.svg?branch=master-26.2)](https://github.com/MinecraftModDevelopmentMods/BaseMetals/actions/workflows/ci.yml?query=branch%3Amaster-26.2) +[![Build, test, and audit](https://github.com/MinecraftModDevelopmentMods/BaseMetals/actions/workflows/ci.yml/badge.svg?branch=master-26.3)](https://github.com/MinecraftModDevelopmentMods/BaseMetals/actions/workflows/ci.yml?query=branch%3Amaster-26.3) # Base Metals @@ -12,23 +12,23 @@ shields, bows, crossbows, arrows, and bolts. The mod also includes Crack Hammers, scythes, metal anvils, the human detector, villager trades, advancements, and structure loot. -This is the Minecraft 26.2 Forge version, `3.1.1.2602001`. It keeps the -fixes and features from the released 26.1.2 version, including raw metals, ore +This is the Minecraft 26.3 Forge version, `3.1.1.2603001`. It keeps the +fixes and features from the released 26.2 version, including raw metals, ore texture overlays and High/Low Fantasy modes. Existing Base Metals items and blocks keep their registry IDs. OreSpawn handles ore generation; MMDLib and Additional Loot Tables are no longer needed. ## Requirements -- Minecraft `26.2` -- Forge `65.1.0` or newer in the Minecraft 26.2 Forge 65 line +- Minecraft `26.3` +- Forge `66.0.0` or newer in the Minecraft 26.3 Forge 66 line - A current Java 25 runtime -- OreSpawn `[4.1.0.2602001,5.0.0)` on both client and server +- OreSpawn `[4.1.0.2603001,5.0.0)` on both client and server -The build uses Forge `65.1.0` and is also tested on `65.1.5`. +The build uses Forge `66.0.0` and is also tested on `66.1.0`. -This version uses OreSpawn `4.1.0.2602001`. Put -`BaseMetals-3.1.1.2602001.jar` and a compatible OreSpawn JAR in the `mods` +This version uses OreSpawn `4.1.0.2603001`. Put +`BaseMetals-3.1.1.2603001.jar` and a compatible OreSpawn JAR in the `mods` directory. MMDLib and Additional Loot Tables must not be carried forward from an older installation. Mineralogy is optional. @@ -144,13 +144,16 @@ Adding Base Metals to a modern world does not trigger the old numeric-ID conversion just because an older backup is present. Existing player data stays in place; genuine legacy upgrades still make backups before changing saved data. +The 26.3 update also protects imported terrain from dungeon placement during +conversion. New chunks and the new underground areas can still generate normally. + ## Mod and data-pack compatibility Forge item, block, and fluid tags are Base Metals' public compatibility API. Compatible tags and the existing recipe data for Mekanism, Thermal, Tinkers' Construct and Ender IO are retained, but their Java integrations are not enabled -in this Forge 26.2 port. Do not add jars made for older Minecraft versions or +in this Forge 26.3 port. Do not add jars made for older Minecraft versions or NeoForge to this instance. No optional integration API is required to compile or launch Base Metals. @@ -198,10 +201,12 @@ are isolated from the published JAR. Virtual-display client checks use `-PvirtualDisplaySmoke=true` to skip Forge's early splash window. Minecraft still opens its game window and runs the full -world check. Ordinary client launches keep the normal splash screen. +world check. Linux CI uses Ubuntu 26.04 with Mesa/EGL software rendering and checks +the SDL/OpenGL context before starting Minecraft. Ordinary client launches keep +the normal splash screen and graphics settings. Release artifacts use Maven coordinate -`zone.moddev.mc.basemetals:BaseMetals:3.1.1.2602001`. +`zone.moddev.mc.basemetals:BaseMetals:3.1.1.2603001`. Report defects and compatibility problems through the [Base Metals issue tracker](https://github.com/MinecraftModDevelopmentMods/BaseMetals/issues). diff --git a/build.gradle b/build.gradle index bf2e3ceb7..1da84c143 100644 --- a/build.gradle +++ b/build.gradle @@ -9,14 +9,14 @@ plugins { ext.mavenizerCompatibilityJar = layout.projectDirectory.file( 'ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar') ext.mavenizerCompatibilitySha256 = - '5ABAB52A3F1BE115381C3CE80D5FD38DBE6C9F4C0B9ECF70A957700EC90FB88B' + '4150926F33A696B2088AB2675C1C3B45745CE4F9F2DEB31CCD88C9ADC834062F' ext.mavenizerFixtureChecksums = [ 'minecraft-mavenizer-0.5.21-orespawn-compat.jar': mavenizerCompatibilitySha256, - 'minecraft-source-compatibility.json': '9437E03FC5E1CB5D605D72F0538C4DF4417B64DB0A57D0AABDC5184781517AC1', - 'minecraft-mavenizer-0.5.21-orespawn-compat.patch': '2DA8DD0FD5F112F5B7591113AED06D9A58C584A25F8705E26445A6D2E9D5C78E', + 'minecraft-source-compatibility.json': '829400D74ECB1BF0CC6107D9980B8328365E3F56D9A5342002D8DF340571BDB7', + 'minecraft-mavenizer-0.5.21-orespawn-compat.patch': 'F1402D730A356589AE46C2483AC10E41062E28A748FEBB72F6CB663A28027A41', 'LICENSE-MAVENIZER.txt': '20C17D8B8C48A600800DFD14F95D5CB9FF47066A9641DDEAB48DC54AEC96E331', - 'README.md': 'EAD1477F393897C491A10FB86178213A99C360571A8063D551DA0579DF65BA6F' + 'README.md': '45AA8B3238D2E8A873FF49862A8EA1ADDBC186E39655436723C0C85773447D36' ] // Mavenizer runs during configuration, so verify its inputs before configuring Forge. mavenizerFixtureChecksums.each { name, expected -> @@ -27,7 +27,7 @@ mavenizerFixtureChecksums.each { name, expected -> } } -// Forge 65 needs no source repairs. The sealed fixture records that result +// Forge 66 needs no source repairs. The sealed fixture records that result // and stays separate from the mod and its runtime dependencies. fgtools.configure('mavenizer') { classpath.from(mavenizerCompatibilityJar) diff --git a/ci-fixtures/tools/README.md b/ci-fixtures/tools/README.md index 1365b4203..673af87f1 100644 --- a/ci-fixtures/tools/README.md +++ b/ci-fixtures/tools/README.md @@ -2,7 +2,7 @@ This directory contains a build-only derivative of MinecraftForge's MinecraftMavenizer `0.5.21`. It is used only while ForgeGradle prepares the -exact Forge `26.2-65.1.0` development dependency and is excluded from every +exact Forge `26.3-66.0.0` development dependency and is excluded from every OreSpawn publication artifact. ## Provenance and licence @@ -21,13 +21,13 @@ declaration and one accessor in that record. Missing, partial, duplicate or ambiguous states fail preparation. Targets without an explicit rule set are left unchanged. -Forge `26.2-65.1.0` compiles without source compatibility edits. Its explicit +Forge `26.3-66.0.0` compiles without source compatibility edits. Its explicit zero-rule entry proves that result is intentional and target-qualified rather than an accidental fall-through. A marker beside Mavenizer's output records the target and manifest SHA-256. Reprocessing the same source is idempotent. The derivative also propagates Gradle offline mode when the build sets -`ORESPAWN_MAVENIZER_OFFLINE=true`. Mavenizer, OreSpawn and Minecraft 26.2 all +`ORESPAWN_MAVENIZER_OFFLINE=true`. Mavenizer, OreSpawn and Minecraft 26.3 all run and compile with Java 25. ## Rebuild diff --git a/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar b/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar index b5f7fb50b..86aeb5ef2 100644 Binary files a/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar and b/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.jar differ diff --git a/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.patch b/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.patch index b0c92c519..962376f98 100644 --- a/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.patch +++ b/ci-fixtures/tools/minecraft-mavenizer-0.5.21-orespawn-compat.patch @@ -371,7 +371,7 @@ index 0000000..702d759 +{ + "schema": 1, + "targets": { -+ "net.minecraftforge:forge:26.2-65.1.0": { ++ "net.minecraftforge:forge:26.3-66.0.0": { + "expectedApplications": 0, + "rules": [] + } diff --git a/ci-fixtures/tools/minecraft-source-compatibility.json b/ci-fixtures/tools/minecraft-source-compatibility.json index f6c049d6b..3451901cd 100644 --- a/ci-fixtures/tools/minecraft-source-compatibility.json +++ b/ci-fixtures/tools/minecraft-source-compatibility.json @@ -1,7 +1,7 @@ { "schema": 1, "targets": { - "net.minecraftforge:forge:26.2-65.1.0": { + "net.minecraftforge:forge:26.3-66.0.0": { "expectedApplications": 0, "rules": [] } diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index cfcdf4f58..3fd0a60c8 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -23,7 +23,7 @@ recycling recipes also accept the appropriate vanilla equivalent. The tags and existing integration data for Mekanism, Thermal, Tinkers' Construct and Ender IO remain available for future compatibility work. Their Java API hooks -and runtime jars are not included in this Forge 26.2 port. Do not substitute +and runtime jars are not included in this Forge 26.3 port. Do not substitute jars made for older Minecraft versions or NeoForge. Other processors can use the shared Forge tags without a direct Java integration. diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 9b3dcdfbf..26a28fd39 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -2,7 +2,7 @@ Always upgrade a copy and retain the original legacy world. Install Base Metals 3.1.1 and OreSpawn 4.1.0, but do not carry MMDLib, Additional Loot Tables, the -OreSpawn 3 plugin, or the old native fallback generator into the 26.2 instance. +OreSpawn 3 plugin, or the old native fallback generator into the 26.3 instance. Minecraft's data fixer cannot identify third-party numeric block IDs from before the 1.13 flattening. Base Metals snapshots those positions, runs @@ -126,6 +126,9 @@ Modern worlds do not need the old numeric-ID conversion. Base Metals only runs it after confirming an older source save. A leftover `level.dat_old` does not replace the current player or turn on legacy terrain protection. +Dungeon generation is skipped when its centre lies in protected imported +terrain. It still runs normally in new chunks and new underground areas. + For a genuine upgrade, legacy inventories are normalized before Minecraft moves the player files and converts their item data. The world-local migration sidecar keeps the source version and saved registry map for reloads and chunks opened later; it does not store @@ -148,7 +151,7 @@ when the current world metadata confirms an upgrade from the previous storage format. The source files are kept for recovery. A modern world with leftover old files does not activate this step. -Minecraft 26.2 also moves world-generation settings, game rules and world clocks +Minecraft 26.2 moved world-generation settings, game rules and world clocks out of `level.dat` into separate saved-data files. Minecraft handles that move during the upgrade. Keep the whole world folder together rather than copying an older `level.dat` over the upgraded save. diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 54b930f1b..9bd08152a 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -1,22 +1,22 @@ # Supported versions -Base Metals `3.1.1.2602001` targets Minecraft 26.2, Forge 65.1.0 and Java 25. -Forge 65.1.5 is the additional compatibility target. -OreSpawn `4.1.0.2602001` is the exact build and test dependency. Clients and -servers need OreSpawn `[4.1.0.2602001,5.0.0)`. +Base Metals `3.1.1.2603001` targets Minecraft 26.3, Forge 66.0.0 and Java 25. +Forge 66.1.0 is the additional compatibility target. +OreSpawn `4.1.0.2603001` is the exact build and test dependency. Clients and +servers need OreSpawn `[4.1.0.2603001,5.0.0)`. -The pinned CurseForge artifact is project 245586, file 9332183: -`curse.maven:mmd-orespawn-245586:9332183`. +The pinned CurseForge artifact is project 245586, file 9333781: +`curse.maven:mmd-orespawn-245586:9333781`. Its SHA-256 is -`F74785025898DF059E0F00F6ADA53364E07ECBBC306807ED7CD55350609E2547`. +`A7FCDD626B9044091BFDB7277DA3169A5307EB5F97FCF88741A878401DCF1AEA`. Release artifacts use Maven coordinate -`zone.moddev.mc.basemetals:BaseMetals:3.1.1.2602001`. +`zone.moddev.mc.basemetals:BaseMetals:3.1.1.2603001`. The provider remains schema 3, revision 1. OreSpawn 4.1 is needed for the configuration-screen API; the provider's eleven rule IDs and 1.18 distributions have not changed. -The data pack uses format `107.1`. Forge wraps client mod resources with the -native resource-pack format `88.0`. The resource metadata retains the minor -version rather than rounding the data format down to `107`. +The data pack uses format `121.0`. Forge wraps client mod resources with the +native resource-pack format `97.1`. Pack metadata keeps the full major and +minor version numbers. diff --git a/docs/WORLDGEN.md b/docs/WORLDGEN.md index bd2909986..0b9339478 100644 --- a/docs/WORLDGEN.md +++ b/docs/WORLDGEN.md @@ -1,8 +1,8 @@ # OreSpawn world generation -Base Metals requires OreSpawn `[4.1.0.2602001,5.0.0)` and packages provider schema 3, +Base Metals requires OreSpawn `[4.1.0.2603001,5.0.0)` and packages provider schema 3, revision 1 at `data/basemetals/orespawn/provider.json`. Builds and tests use the -published OreSpawn `4.1.0.2602001` release. The provider is checked against the +published OreSpawn `4.1.0.2603001` release. The provider is checked against the included 4.1.0 schema. The provider declares ores only: it has no rocks, geomes, biome rules, terrain diff --git a/docs/schemas/orespawn-4.1.0/README.md b/docs/schemas/orespawn-4.1.0/README.md index c7e20bdf1..ed1547ef3 100644 --- a/docs/schemas/orespawn-4.1.0/README.md +++ b/docs/schemas/orespawn-4.1.0/README.md @@ -1,9 +1,9 @@ # OreSpawn 4.1.0 schemas -These public schemas match OreSpawn 4.1.0.2602001. The Base Metals provider +These public schemas match OreSpawn 4.1.0.2603001. The Base Metals provider still uses schema 3 and revision 1; its ore rules have not changed. The build validates the provider against this snapshot. OreSpawn 4.1 is also needed for the Base Metals configuration button in its world-creation screen. -Source: [OreSpawn 4.1.0.2602001](https://github.com/MinecraftModDevelopmentMods/OreSpawn/releases/tag/4.1.0.2602001). +Source: [OreSpawn 4.1.0.2603001](https://github.com/MinecraftModDevelopmentMods/OreSpawn/releases/tag/4.1.0.2603001). diff --git a/gradle.properties b/gradle.properties index 06a4ea1b0..dc4824ea3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,14 +11,14 @@ net.minecraftforge.gradle.merge-source-sets=true ## Environment Properties # The Minecraft version must agree with the Forge version to get a valid artifact -minecraft_version=26.2 +minecraft_version=26.3 # The Minecraft version range can use any release version of Minecraft as bounds. # Snapshots, pre-releases, and release candidates are not guaranteed to sort properly # as they do not follow standard versioning conventions. -minecraft_version_range=[26.2,26.3) +minecraft_version_range=[26.3,26.4) # The Forge version must agree with the Minecraft version to get a valid artifact -forge_version=65.1.0 -forge_installer_sha256=813FBBFECC3086A492783B9F5023AE15007737B42E17AAB75F48BA77403314DF +forge_version=66.0.0 +forge_installer_sha256=C6173EEA56AED0B947722F314542196E28A35285844961D517C7FB571492BBE3 # Release metadata consumed by the generic dispatcher. loader_name=forge loader_code=1 @@ -29,14 +29,14 @@ curseforge_project_id=240967 # Exact released OreSpawn used for development and qualification. Runtime # metadata intentionally remains compatible with supported OreSpawn 4 builds. -orespawn_version=4.1.0.2602001 +orespawn_version=4.1.0.2603001 orespawn_curse_project_id=245586 -orespawn_curse_file_id=9332183 -orespawn_sha256=F74785025898DF059E0F00F6ADA53364E07ECBBC306807ED7CD55350609E2547 +orespawn_curse_file_id=9333781 +orespawn_sha256=A7FCDD626B9044091BFDB7277DA3169A5307EB5F97FCF88741A878401DCF1AEA # The Forge version range can use any version of Forge as bounds or match the loader version range -forge_version_range=[65.1.0,66) +forge_version_range=[66.0.0,67) # The loader version range can only use the major version of Forge/FML as bounds -loader_version_range=[65,66) +loader_version_range=[66,67) # The mapping channel to use for mappings. # The default set of supported mapping channels are ["official", "snapshot", "snapshot_nodoc", "stable", "stable_nodoc"]. # Additional mapping channels can be registered through the "channelProviders" extension in a Gradle plugin. @@ -54,7 +54,7 @@ loader_version_range=[65,66) mapping_channel=official # The mapping version to query from the mapping channel. # This must match the format required by the mapping channel. -mapping_version=26.2 +mapping_version=26.3 ## Mod Properties @@ -67,7 +67,7 @@ mod_name=Base Metals # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=LGPL-2.1 # The mod version. See https://semver.org/ -mod_version=3.1.1.2602001 +mod_version=3.1.1.2603001 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html @@ -77,7 +77,7 @@ mod_authors=Cyanobacterium, jriwanek, dshadowwolf, GetDizzy611, SkyBlade1978, th # The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list. mod_description=Adds historically common metals, fantasy metals, alloys, tools, armour, and OreSpawn-powered deposits. -# Forge 65 uses official runtime names. +# Forge 66 uses official runtime names. reobf=false -mcp_version=20260616.103818 +mcp_version=20260918.230105 java_setup_version=25.0.3+9.0.LTS diff --git a/gradle/README.md b/gradle/README.md index c4ada76fd..22f112193 100644 --- a/gradle/README.md +++ b/gradle/README.md @@ -32,7 +32,7 @@ flower, sapling and leaf tags include suitable new plants. Normal client and server launches load only Base Metals and OreSpawn. The optional-mod profile has its own run folder, but no optional integration mods -are pinned for Forge 26.2. The Mekanism, Tinkers, Thermal and Ender IO Java +are pinned for Forge 26.3. The Mekanism, Tinkers, Thermal and Ender IO Java integrations are not enabled here; their dormant data remains available for future integration work. diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 1c8af9ad8..dfea4e9d2 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -54,12 +54,12 @@ dependencies { implementation minecraft.dependency("net.minecraftforge:forge:${minecraft_version}-${forge_version}") // OreSpawn remains an ordinary hard mod dependency in metadata and is - // never shaded into Base Metals. Use the published 26.2 release in + // never shaded into Base Metals. Use the published 26.3 release in // development so clean checkouts exercise the same artifact as players. implementation "curse.maven:mmd-orespawn-${orespawn_curse_project_id}:${orespawn_curse_file_id}" orespawnRelease "curse.maven:mmd-orespawn-${orespawn_curse_project_id}:${orespawn_curse_file_id}" - // Optional Java integrations are not enabled in this Forge 26.2 port. + // Optional Java integrations are not enabled in this Forge 26.3 port. testImplementation platform('org.junit:junit-bom:5.10.2') testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' diff --git a/gradle/ide/eclipse.gradle b/gradle/ide/eclipse.gradle index bb25ef855..5d9e658f2 100644 --- a/gradle/ide/eclipse.gradle +++ b/gradle/ide/eclipse.gradle @@ -91,7 +91,7 @@ def verifyDevelopmentRunProfiles = tasks.register('verifyDevelopmentRunProfiles' || optionalDevelopmentArtifacts.any { expected -> !optionalClient.classpath.files.any { it.name.contains("${expected.module}-${expected.file}") } }) { - throw new GradleException('The optional client must load the pinned 26.2 dependencies') + throw new GradleException('The optional client must load the pinned 26.3 dependencies') } } } @@ -334,7 +334,7 @@ def verifyEclipseProductionClasspath = tasks.register('verifyEclipseProductionCl } } String metadata = new File(projectDir, 'bin/main/META-INF/mods.toml').getText('UTF-8') - if (metadata.contains('${') || !metadata.contains('version="3.1.1.2602001"')) { + if (metadata.contains('${') || !metadata.contains('version="3.1.1.2603001"')) { throw new GradleException('Eclipse output contains unresolved or invalid Base Metals version metadata') } } diff --git a/gradle/release/artifacts.gradle b/gradle/release/artifacts.gradle index 11de2a215..441ca7daf 100644 --- a/gradle/release/artifacts.gradle +++ b/gradle/release/artifacts.gradle @@ -184,7 +184,7 @@ tasks.register('verifyMavenCoordinates') { tasks.register('verifyMavenizerCompatibilityFixture') { group = 'verification' - description = 'Verifies the checksum-sealed Forge 26.2 Mavenizer compatibility fixture.' + description = 'Verifies the checksum-sealed Forge 26.3 Mavenizer compatibility fixture.' inputs.file project.ext.mavenizerCompatibilityJar doLast { File fixture = project.ext.mavenizerCompatibilityJar.asFile @@ -198,23 +198,23 @@ tasks.register('verifyMavenizerCompatibilityFixture') { tasks.register('verifyReleaseConfiguration') { group = 'verification' - description = 'Verifies the exact Base Metals 26.2 release identity.' + description = 'Verifies the exact Base Metals 26.3 release identity.' dependsOn tasks.named('verifyMavenizerCompatibilityFixture') dependsOn tasks.named('verifyJava25Toolchain') doLast { - if (configuredReleaseIdentity.modVersion != '3.1.1.2602001' + if (configuredReleaseIdentity.modVersion != '3.1.1.2603001' || configuredReleaseIdentity.modGroup != expectedMavenGroup - || configuredReleaseIdentity.minecraftVersion != '26.2' - || configuredReleaseIdentity.forgeVersion != '65.1.0' - || configuredReleaseIdentity.forgeInstallerSha256 != '813FBBFECC3086A492783B9F5023AE15007737B42E17AAB75F48BA77403314DF' + || configuredReleaseIdentity.minecraftVersion != '26.3' + || configuredReleaseIdentity.forgeVersion != '66.0.0' + || configuredReleaseIdentity.forgeInstallerSha256 != 'C6173EEA56AED0B947722F314542196E28A35285844961D517C7FB571492BBE3' || configuredReleaseIdentity.mappingChannel != 'official' - || configuredReleaseIdentity.mappingVersion != '26.2' + || configuredReleaseIdentity.mappingVersion != '26.3' || configuredReleaseIdentity.loaderName != 'forge' || configuredReleaseIdentity.loaderCode != '1' || configuredReleaseIdentity.javaVersion != '25' || configuredReleaseIdentity.gradleJavaVersion != '25' || configuredReleaseIdentity.curseForgeProjectId != '240967') { - throw new GradleException('Unexpected Base Metals 26.2 release identity') + throw new GradleException('Unexpected Base Metals 26.3 release identity') } } } @@ -223,10 +223,10 @@ tasks.register('verifyReleaseDependencies') { group = 'verification' description = 'Resolves and verifies the exact released OreSpawn qualification dependency.' doLast { - if (configuredOreSpawnIdentity.version != '4.1.0.2602001' + if (configuredOreSpawnIdentity.version != '4.1.0.2603001' || configuredOreSpawnIdentity.curseProjectId != '245586' - || configuredOreSpawnIdentity.curseFileId != '9332183' - || configuredOreSpawnIdentity.sha256 != 'F74785025898DF059E0F00F6ADA53364E07ECBBC306807ED7CD55350609E2547') { + || configuredOreSpawnIdentity.curseFileId != '9333781' + || configuredOreSpawnIdentity.sha256 != 'A7FCDD626B9044091BFDB7277DA3169A5307EB5F97FCF88741A878401DCF1AEA') { throw new GradleException('Unexpected OreSpawn release coordinates') } File artifact = configurations.orespawnRelease.resolvedConfiguration.resolvedArtifacts @@ -326,14 +326,14 @@ tasks.register('verifyReleaseArtifacts') { if (manifest.mainAttributes.getValue('Maven-Artifact') != expectedMavenCoordinate) { throw new GradleException('Release manifest contains the wrong Maven coordinate') } - String expectedBuildTarget = 'minecraft-26.2-forge-65.1.0-java-25' + String expectedBuildTarget = 'minecraft-26.3-forge-66.0.0-java-25' if (manifest.mainAttributes.getValue('Build-Target') != expectedBuildTarget) { throw new GradleException('Release manifest contains the wrong stable build target') } String modsToml = zip.getInputStream(zip.getEntry('META-INF/mods.toml')).getText('UTF-8') if (modsToml.contains('${') - || !modsToml.contains('version="3.1.1.2602001"') - || !modsToml.contains('versionRange="[4.1.0.2602001,5.0.0)"')) { + || !modsToml.contains('version="3.1.1.2603001"') + || !modsToml.contains('versionRange="[4.1.0.2603001,5.0.0)"')) { throw new GradleException('Release mods.toml contains unresolved or incompatible metadata') } def provider = new JsonSlurper().parse(zip.getInputStream( diff --git a/gradle/verification/packaged-runtime.gradle b/gradle/verification/packaged-runtime.gradle index bce75d64b..2f85183b6 100644 --- a/gradle/verification/packaged-runtime.gradle +++ b/gradle/verification/packaged-runtime.gradle @@ -134,8 +134,8 @@ def packagedServerStartupSmoke = tasks.register('packagedServerStartupSmoke', Ex doFirst { File runtime = requireRuntimeDirectory(packagedForgeServerRuntime, 'packagedForgeServerRuntime') File libraries = new File(runtime, 'libraries') - File forgeDirectory = new File(libraries, 'net/minecraftforge/forge/26.2-65.1.0') - File launcher = new File(forgeDirectory, 'forge-26.2-65.1.0-server.jar') + File forgeDirectory = new File(libraries, 'net/minecraftforge/forge/26.3-66.0.0') + File launcher = new File(forgeDirectory, 'forge-26.3-66.0.0-server.jar') File sourceArguments = new File(forgeDirectory, System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows') ? 'win_args.txt' : 'unix_args.txt') @@ -174,9 +174,9 @@ def packagedServerStartupSmoke = tasks.register('packagedServerStartupSmoke', Ex if (!marker.isFile()) throw new GradleException('Packaged dedicated-server completion marker is missing') Properties values = new Properties() marker.withInputStream { values.load(it) } - if (values.getProperty('basemetals') != '3.1.1.2602001' - || values.getProperty('orespawn') != '4.1.0.2602001' - || values.getProperty('forge') != '65.1.0') { + if (values.getProperty('basemetals') != '3.1.1.2603001' + || values.getProperty('orespawn') != '4.1.0.2603001' + || values.getProperty('forge') != '66.0.0') { throw new GradleException("Packaged dedicated-server versions are wrong: ${values}") } verifyPackagedLog(runDirectory, 'BASEMETALS_PACKAGED_SERVER_SMOKE PASS', @@ -219,11 +219,11 @@ def preparePackagedClientSmoke = tasks.register('preparePackagedClientSmoke') { File hostPack = new File(runDirectory, 'resourcepacks/basemetals-host-test') File texture = new File(hostPack, 'assets/minecraft/textures/block/stone.png') texture.parentFile.mkdirs() - new java.util.zip.ZipFile(new File(runtime, 'versions/26.2/26.2.jar')).withCloseable { zip -> + new java.util.zip.ZipFile(new File(runtime, 'versions/26.3/26.3.jar')).withCloseable { zip -> texture.bytes = zip.getInputStream(zip.getEntry('assets/minecraft/textures/block/dirt.png')).bytes } new File(hostPack, 'pack.mcmeta').setText( - '{"pack":{"min_format":[88,0],"max_format":[88,0],"description":"Ore host replacement test"}}\n', 'UTF-8') + '{"pack":{"min_format":[97,1],"max_format":[97,1],"description":"Ore host replacement test"}}\n', 'UTF-8') new File(runDirectory, 'options.txt').append( 'resourcePacks:["vanilla","mod_resources","file/basemetals-host-test"]\n', 'UTF-8') } @@ -243,13 +243,13 @@ def packagedClientStartupSmoke = tasks.register('packagedClientStartupSmoke', Ex dependsOn preparePackagedClientSmoke doFirst { File runtime = requireRuntimeDirectory(packagedForgeClientRuntime, 'packagedForgeClientRuntime') - String forgeVersionId = ['26.2-forge-65.1.0', 'forge-65.1.0'].find { candidate -> + String forgeVersionId = ['26.3-forge-66.0.0', 'forge-66.0.0'].find { candidate -> new File(runtime, "versions/${candidate}/${candidate}.json").isFile() } - if (forgeVersionId == null) throw new GradleException('Official client runtime has no Forge 65.1.0 profile') + if (forgeVersionId == null) throw new GradleException('Official client runtime has no Forge 66.0.0 profile') File forgeJsonFile = new File(runtime, "versions/${forgeVersionId}/${forgeVersionId}.json") - File baseJsonFile = new File(runtime, 'versions/26.2/26.2.json') - File baseJar = new File(runtime, 'versions/26.2/26.2.jar') + File baseJsonFile = new File(runtime, 'versions/26.3/26.3.json') + File baseJar = new File(runtime, 'versions/26.3/26.3.jar') File nativesDirectory = new File(runtime, "natives/${forgeVersionId}") [forgeJsonFile, baseJsonFile, baseJar, new File(runtime, 'libraries'), new File(runtime, 'assets'), nativesDirectory].each { diff --git a/src/dataGenerator/java/zone/moddev/mc/basemetals/data/GeneratedData.java b/src/dataGenerator/java/zone/moddev/mc/basemetals/data/GeneratedData.java index c2a34ed48..f16b0a124 100644 --- a/src/dataGenerator/java/zone/moddev/mc/basemetals/data/GeneratedData.java +++ b/src/dataGenerator/java/zone/moddev/mc/basemetals/data/GeneratedData.java @@ -27,12 +27,9 @@ import net.minecraft.data.DataProvider; import net.minecraft.data.CachedOutput; import net.minecraft.resources.Identifier; -import net.minecraft.world.item.AxeItem; import net.minecraft.world.item.BowItem; import net.minecraft.world.item.FishingRodItem; -import net.minecraft.world.item.HoeItem; import net.minecraft.world.item.ShearsItem; -import net.minecraft.world.item.ShovelItem; /** Small catalogue-driven generator for server data and compatibility manifests. */ final class GeneratedData implements DataProvider { @@ -278,12 +275,12 @@ private void writeEnchantmentTags(CachedOutput cache) throws IOException { String name = "basemetals:" + id; if (item instanceof zone.moddev.mc.basemetals.content.MaterialItems.Sword) addAggregate(families, "swords", name); - if (item instanceof AxeItem) addAggregate(families, "axes", name); + if (item instanceof zone.moddev.mc.basemetals.content.MaterialItems.Axe) addAggregate(families, "axes", name); if (item instanceof zone.moddev.mc.basemetals.content.MaterialItems.Pickaxe) addAggregate(families, "pickaxes", name); - if (item instanceof ShovelItem) addAggregate(families, "shovels", name); - if (item instanceof HoeItem) addAggregate(families, "hoes", name); + if (item instanceof zone.moddev.mc.basemetals.content.MaterialItems.Shovel) addAggregate(families, "shovels", name); + if (item instanceof zone.moddev.mc.basemetals.content.MaterialItems.Hoe) addAggregate(families, "hoes", name); - if (item instanceof zone.moddev.mc.basemetals.content.CrackhammerItem || item instanceof zone.moddev.mc.basemetals.content.ScytheItem || item instanceof zone.moddev.mc.basemetals.content.MaterialItems.Pickaxe || item instanceof AxeItem || item instanceof ShovelItem || item instanceof HoeItem) { + if (item instanceof zone.moddev.mc.basemetals.content.CrackhammerItem || item instanceof zone.moddev.mc.basemetals.content.ScytheItem || item instanceof zone.moddev.mc.basemetals.content.MaterialItems.Pickaxe || item instanceof zone.moddev.mc.basemetals.content.MaterialItems.Axe || item instanceof zone.moddev.mc.basemetals.content.MaterialItems.Shovel || item instanceof zone.moddev.mc.basemetals.content.MaterialItems.Hoe) { addAggregate(families, "enchantable/mining", name); addAggregate(families, "enchantable/mining_loot", name); } else if (item instanceof ShearsItem) { @@ -1583,6 +1580,43 @@ private static void modernizeLootJson(JsonElement element) { } if (!element.isJsonObject()) return; JsonObject object = element.getAsJsonObject(); + + for (String dispatch : List.of("function", "condition")) { + if (object.has(dispatch) && object.get(dispatch).isJsonPrimitive()) { + object.add("type", object.remove(dispatch)); + } + } + + if (object.has("type") && "minecraft:block_state_property".equals(object.get("type").getAsString())) { + object.addProperty("type", "minecraft:match_block"); + object.add("blocks", object.remove("block")); + object.add("state", object.remove("properties")); + } + + if (object.has("functions")) { + object.add("modifier", object.remove("functions")); + } + + if (object.has("conditions")) { + JsonArray conditions = object.remove("conditions").getAsJsonArray(); + + if (conditions.size() == 1) { + object.add("condition", conditions.get(0)); + } else if (!conditions.isEmpty()) { + object.add("condition", object("type", "minecraft:all_of", "terms", conditions)); + } + } + + for (String field : List.of("count", "rolls", "bonus_rolls")) { + if (object.has(field) && object.get(field).isJsonObject()) { + JsonObject number = object.getAsJsonObject(field); + + if (!number.has("type") && number.has("min") && number.has("max")) { + number.addProperty("type", "minecraft:uniform"); + } + } + } + new ArrayList<>(object.keySet()).stream() .filter(key -> key.startsWith("__comment")) .forEach(object::remove); @@ -1593,7 +1627,7 @@ private static void modernizeLootJson(JsonElement element) { && entry.getValue().getAsJsonPrimitive().isString() && !entry.getValue().getAsString().contains(":")) { entry.setValue(new com.google.gson.JsonPrimitive("minecraft:" + entry.getValue().getAsString())); - } else { + } else if (!entry.getKey().equals("state") && !entry.getKey().equals("properties")) { modernizeLootJson(entry.getValue()); } } @@ -1621,8 +1655,21 @@ private void writeManifest(CachedOutput cache) throws IOException { JsonObject root = new JsonObject(); root.addProperty("format", 1); - root.addProperty("source", "Base Metals 26.2 catalogue"); + root.addProperty("source", "Base Metals 26.3 catalogue"); root.add("blocks", array(blocks)); + JsonObject blockDefaults = new JsonObject(); + + ModContent.blocksById().forEach((id, block) -> { + var state = block.get().defaultBlockState(); + JsonObject properties = new JsonObject(); + + state.getProperties().stream().sorted(java.util.Comparator.comparing( + net.minecraft.world.level.block.state.properties.Property::getName)) + .forEach(property -> properties.addProperty(property.getName(), propertyValue(state, property))); + blockDefaults.add("basemetals:" + id, properties); + }); + + root.add("block_defaults", blockDefaults); root.add("items", array(items)); root.add("fluids", array(ModContent.fluids().keySet().stream().flatMap(id -> java.util.stream.Stream.of("basemetals:" + id, "basemetals:flowing_" + id)).toList())); @@ -1642,6 +1689,12 @@ private void writeManifest(CachedOutput cache) throws IOException { save(cache, root, "data/basemetals/registry_manifest.json"); } + private static > String propertyValue( + net.minecraft.world.level.block.state.BlockState state, + net.minecraft.world.level.block.state.properties.Property property) { + return property.getName(state.getValue(property)); + } + private static List difference(List current, List baseline, List exclusions) { LinkedHashSet remaining = new LinkedHashSet<>(current); remaining.removeAll(baseline); @@ -1845,6 +1898,10 @@ private void save(CachedOutput cache, JsonObject value, String relative) throws value = object("type", "basemetals:content_crafting", "recipe", value); } if (relative.contains("/recipe/")) normalizeRecipeResults(value); + if (relative.contains("/loot_table/")) modernizeLootJson(value); + if (relative.contains("/loot_modifiers/") && value.has("conditions")) { + value.getAsJsonArray("conditions").forEach(GeneratedData::modernizeLootJson); + } if (relative.contains("/advancement/") || relative.contains("/loot_table/")) normalizePredicates(value); JsonObject previous = scheduledResources.putIfAbsent(relative, value.deepCopy()); if (previous != null) { @@ -2009,7 +2066,7 @@ private void writeRawProcessing(CachedOutput cache) throws IOException { "minecraft:" + method); JsonObject criteria = object("has_raw", inventoryCriterion(ingredient("#forge:raw_materials/" + material)), "has_recipe", object("trigger", "minecraft:recipe_unlocked", - "conditions", object("recipe", "basemetals:" + id))); + "conditions", object("recipes", array("basemetals:" + id)))); writeUnlock(cache, id, criteria); } } @@ -2029,7 +2086,8 @@ private void writeCraftingUnlock(CachedOutput cache, String id, JsonObject recip default -> "#forge:ingots/" + material; }; JsonObject criteria = object("has_material", inventoryCriterion(ingredient(discovery)), - "has_recipe", object("trigger", "minecraft:recipe_unlocked", "conditions", object("recipe", "basemetals:" + id))); + "has_recipe", object("trigger", "minecraft:recipe_unlocked", "conditions", + object("recipes", array("basemetals:" + id)))); List inputs = new ArrayList<>(); if (recipe.has("key")) recipe.getAsJsonObject("key").entrySet().forEach(entry -> inputs.add(entry.getValue())); else recipe.getAsJsonArray("ingredients").forEach(inputs::add); diff --git a/src/dataGenerator/java/zone/moddev/mc/basemetals/data/SmithTradeProvider.java b/src/dataGenerator/java/zone/moddev/mc/basemetals/data/SmithTradeProvider.java index 312c900e8..6f900c61d 100644 --- a/src/dataGenerator/java/zone/moddev/mc/basemetals/data/SmithTradeProvider.java +++ b/src/dataGenerator/java/zone/moddev/mc/basemetals/data/SmithTradeProvider.java @@ -35,14 +35,14 @@ public CompletableFuture run(CachedOutput cache) { "max_uses", offer.enchanted() ? 3 : 6, "xp", offer.enchanted() ? 15 : 5, "reputation_discount", 0.2, - "merchant_predicate", object("condition", "basemetals:trade_enabled", "item", offer.item())); + "merchant_predicate", object("type", "basemetals:trade_enabled", "item", offer.item())); if (offer.enchanted()) { JsonArray modifiers = new JsonArray(); - modifiers.add(object("function", "minecraft:enchant_with_levels", + modifiers.add(object("type", "minecraft:enchant_with_levels", "levels", object("type", "minecraft:uniform", "min", 5, "max", 19), "options", "#minecraft:on_random_loot", "include_additional_cost_component", true)); - trade.add("given_item_modifiers", modifiers); + trade.add("given_item_modifier", modifiers); } writes.add(DataProvider.saveStable(cache, trade, diff --git a/src/gametest/java/zone/moddev/mc/basemetals/content/BaseMetalsGameTests.java b/src/gametest/java/zone/moddev/mc/basemetals/content/BaseMetalsGameTests.java index 85c6df11f..e59896eba 100644 --- a/src/gametest/java/zone/moddev/mc/basemetals/content/BaseMetalsGameTests.java +++ b/src/gametest/java/zone/moddev/mc/basemetals/content/BaseMetalsGameTests.java @@ -159,6 +159,27 @@ public static void compatibilitySlabsStayDoubleAndHidden(GameTestHelper helper) require(helper, !ForgeRegistries.ITEMS.containsKey(Identifier.fromNamespaceAndPath(BaseMetals.MOD_ID, id)), id + " unexpectedly has a block item"); } + + var player = survivalPlayer(helper); + BlockPos position = helper.absolutePos(new BlockPos(1, 1, 1)); + ItemStack pickaxe = Items.NETHERITE_PICKAXE.getDefaultInstance(); + + ModContent.blocksById().forEach((id, block) -> { + if (block.get() instanceof net.minecraft.world.level.block.SlabBlock + && !ModContent.hiddenBlocks().contains(id)) { + for (SlabType type : SlabType.values()) { + var state = block.get().defaultBlockState() + .setValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.SLAB_TYPE, type); + var drops = Block.getDrops(state, helper.getLevel(), position, null, player, pickaxe); + int count = drops.stream().filter(stack -> stack.is(block.get().asItem())) + .mapToInt(ItemStack::getCount).sum(); + + require(helper, count == (type == SlabType.DOUBLE ? 2 : 1), + id + " has the wrong loot count for " + type); + } + } + }); + helper.succeed(); } @@ -431,6 +452,10 @@ public static void legacyToolAttributesAndFuelValuesSurviveThePort(GameTestHelpe require(helper, attribute(hammer, Attributes.ATTACK_SPEED) == -3.5D, "Crackhammer attack speed changed"); + verifyToolTransformation(helper, "bronze_axe", Blocks.OAK_LOG, Blocks.STRIPPED_OAK_LOG, 1); + verifyToolTransformation(helper, "bronze_shovel", Blocks.GRASS_BLOCK, Blocks.DIRT_PATH, 2); + verifyToolTransformation(helper, "bronze_hoe", Blocks.DIRT, Blocks.FARMLAND, 3); + require(helper, burnTime("coal_powder") == 1600, "Coal powder no longer burns for one item"); require(helper, burnTime("coal_smallpowder") == 200, "Tiny coal dust has the wrong burn time"); require(helper, burnTime("wood_gear") == 300, "Wood gear has the wrong burn time"); @@ -1157,8 +1182,38 @@ private static net.minecraft.core.Holder(); for (int index = 0; index < countField.getInt(state); index++) { - result.addAll(layers[index].prepareQuadList()); + var quads = (net.minecraft.client.resources.model.geometry.ItemQuads) quadsField.get(layers[index]); + result.addAll(quads.all()); } return result; diff --git a/src/main/java/zone/moddev/mc/basemetals/client/MaterialProjectileRenderer.java b/src/main/java/zone/moddev/mc/basemetals/client/MaterialProjectileRenderer.java index 799cfbea9..f3ebf624a 100644 --- a/src/main/java/zone/moddev/mc/basemetals/client/MaterialProjectileRenderer.java +++ b/src/main/java/zone/moddev/mc/basemetals/client/MaterialProjectileRenderer.java @@ -45,11 +45,11 @@ public void extractRenderState(MaterialProjectile projectile, State state, float @Override public void submit(State state, PoseStack poses, SubmitNodeCollector collector, CameraRenderState camera) { poses.pushPose(); - poses.mulPose(Axis.YP.rotationDegrees(state.yRot - 90.0F)); - poses.mulPose(Axis.ZP.rotationDegrees(state.xRot)); + poses.rotateDegrees(Axis.YP, state.yRot - 90.0F); + poses.rotateDegrees(Axis.ZP, state.xRot); collector.submitModel(model, state, poses, RenderTypes.entityCutout(TEXTURE), state.lightCoords, OverlayTexture.NO_OVERLAY, state.colour, null, - state.outlineColor, null); + state.outlineColor); poses.popPose(); super.submit(state, poses, collector, camera); diff --git a/src/main/java/zone/moddev/mc/basemetals/content/CrackhammerItem.java b/src/main/java/zone/moddev/mc/basemetals/content/CrackhammerItem.java index 58d4045c2..63bdddef6 100644 --- a/src/main/java/zone/moddev/mc/basemetals/content/CrackhammerItem.java +++ b/src/main/java/zone/moddev/mc/basemetals/content/CrackhammerItem.java @@ -21,8 +21,6 @@ import net.minecraft.world.phys.AABB; import net.minecraft.tags.BlockTags; import net.minecraft.world.entity.EquipmentSlot; -import net.minecraftforge.common.ToolAction; -import net.minecraftforge.common.ToolActions; public final class CrackhammerItem extends Item implements MaterialBacked { private final MaterialDefinition material; @@ -44,11 +42,6 @@ && canHarvest(stack, state) : 1.0F; } - @Override - public boolean canPerformAction(ItemStack stack, ToolAction action) { - return ToolActions.DEFAULT_PICKAXE_ACTIONS.contains(action); - } - @Override public void appendHoverText(ItemStack stack, net.minecraft.world.item.Item.TooltipContext context, TooltipDisplay display, Consumer tooltip, diff --git a/src/main/java/zone/moddev/mc/basemetals/content/MaterialItems.java b/src/main/java/zone/moddev/mc/basemetals/content/MaterialItems.java index 163bac708..562874565 100644 --- a/src/main/java/zone/moddev/mc/basemetals/content/MaterialItems.java +++ b/src/main/java/zone/moddev/mc/basemetals/content/MaterialItems.java @@ -7,6 +7,7 @@ import com.google.common.collect.Multimap; import net.minecraft.resources.Identifier; +import net.minecraft.core.component.DataComponents; import net.minecraft.core.registries.Registries; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; @@ -18,23 +19,23 @@ import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.projectile.arrow.AbstractArrow; -import net.minecraft.world.item.AxeItem; import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.BowItem; import net.minecraft.world.item.FishingRodItem; -import net.minecraft.world.item.HoeItem; import net.minecraft.world.item.component.ItemAttributeModifiers; +import net.minecraft.world.item.component.CookingFuel; import net.minecraft.world.entity.EquipmentSlotGroup; import net.minecraft.world.item.Item; import net.minecraft.world.item.ShearsItem; import net.minecraft.world.item.ShieldItem; -import net.minecraft.world.item.ShovelItem; import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.item.enchantment.EnchantmentHelper; import net.minecraft.world.item.enchantment.Enchantments; import net.minecraft.network.chat.Component; import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.loot.providers.number.ints.ResolvableInt; +import net.minecraft.world.level.storage.loot.providers.number.floats.ResolvableFloat; import javax.annotation.Nullable; import java.util.List; import java.util.function.Consumer; @@ -49,18 +50,34 @@ private MaterialItems() {} public static final class Basic extends Item implements MaterialBacked { private final MaterialDefinition material; private final int burnTime; + public Basic(MaterialDefinition material, int burnTime, Properties properties) { - super(properties); + super(withFuel(properties, burnTime)); this.material = material; this.burnTime = burnTime; } - @Override public MaterialDefinition baseMetalsMaterial() { return material; } - @Override public int getBurnTime(ItemStack stack, + + @Override + public MaterialDefinition baseMetalsMaterial() { + return material; + } + + @Override + public int getBurnTime(ItemStack stack, @Nullable net.minecraft.world.item.crafting.RecipeType recipeType) { return burnTime; } } + private static Item.Properties withFuel(Item.Properties properties, int burnTime) { + if (burnTime > 0) { + return properties.component(DataComponents.COOKING_FUEL, new CookingFuel( + new ResolvableInt.Constant(burnTime), new ResolvableFloat.Constant(1.0F))); + } + + return properties; + } + public static final class Pickaxe extends Item implements MaterialBacked { private final MaterialDefinition material; public Pickaxe(MaterialDefinition material, Properties properties) { @@ -72,40 +89,66 @@ public Pickaxe(MaterialDefinition material, Properties properties) { TooltipDisplay display, Consumer tooltip, TooltipFlag flag) { addToolTooltip(material, tooltip); } } - public static final class Axe extends AxeItem implements MaterialBacked { + public static final class Axe extends Item implements MaterialBacked { private final MaterialDefinition material; + public Axe(MaterialDefinition material, Properties properties) { - super(new MaterialTier(material).toolMaterial(), 4.0F + material.baseAttackDamage(), - material.axeAttackSpeed(), properties); + super(properties.axe(new MaterialTier(material).toolMaterial(), + 4.0F + material.baseAttackDamage(), material.axeAttackSpeed())); this.material = material; } - @Override public MaterialDefinition baseMetalsMaterial() { return material; } - @Override public void appendHoverText(ItemStack stack, net.minecraft.world.item.Item.TooltipContext context, - TooltipDisplay display, Consumer tooltip, TooltipFlag flag) { addToolTooltip(material, tooltip); } + + @Override + public MaterialDefinition baseMetalsMaterial() { + return material; + } + + @Override + public void appendHoverText(ItemStack stack, Item.TooltipContext context, + TooltipDisplay display, Consumer tooltip, TooltipFlag flag) { + addToolTooltip(material, tooltip); + } } - public static final class Shovel extends ShovelItem implements MaterialBacked { + public static final class Shovel extends Item implements MaterialBacked { private final MaterialDefinition material; + public Shovel(MaterialDefinition material, Properties properties) { - super(new MaterialTier(material).toolMaterial(), 1.5F, -3.0F, properties); + super(properties.shovel(new MaterialTier(material).toolMaterial(), 1.5F, -3.0F)); this.material = material; } - @Override public MaterialDefinition baseMetalsMaterial() { return material; } - @Override public void appendHoverText(ItemStack stack, net.minecraft.world.item.Item.TooltipContext context, - TooltipDisplay display, Consumer tooltip, TooltipFlag flag) { addToolTooltip(material, tooltip); } + + @Override + public MaterialDefinition baseMetalsMaterial() { + return material; + } + + @Override + public void appendHoverText(ItemStack stack, Item.TooltipContext context, + TooltipDisplay display, Consumer tooltip, TooltipFlag flag) { + addToolTooltip(material, tooltip); + } } - public static final class Hoe extends HoeItem implements MaterialBacked { + public static final class Hoe extends Item implements MaterialBacked { private final MaterialDefinition material; + public Hoe(MaterialDefinition material, Properties properties) { - super(new MaterialTier(material).toolMaterial(), -material.baseAttackDamage(), - material.baseAttackDamage() - 3.0F, properties); + super(properties.hoe(new MaterialTier(material).toolMaterial(), + -material.baseAttackDamage(), material.baseAttackDamage() - 3.0F)); this.material = material; } - @Override public MaterialDefinition baseMetalsMaterial() { return material; } - @Override public void appendHoverText(ItemStack stack, net.minecraft.world.item.Item.TooltipContext context, - TooltipDisplay display, Consumer tooltip, TooltipFlag flag) { addToolTooltip(material, tooltip); } + @Override + public MaterialDefinition baseMetalsMaterial() { + return material; + } + + @Override + public void appendHoverText(ItemStack stack, Item.TooltipContext context, + TooltipDisplay display, Consumer tooltip, TooltipFlag flag) { + addToolTooltip(material, tooltip); + } } public static class Sword extends Item implements MaterialBacked { @@ -154,11 +197,14 @@ private static ArmorType armorType(EquipmentSlot slot) { public static final class BurnableBlock extends BlockItem { private final int burnTime; + public BurnableBlock(net.minecraft.world.level.block.Block block, int burnTime, Properties properties) { - super(block, properties); + super(block, withFuel(properties, burnTime)); this.burnTime = burnTime; } - @Override public int getBurnTime(ItemStack stack, + + @Override + public int getBurnTime(ItemStack stack, @Nullable net.minecraft.world.item.crafting.RecipeType recipeType) { return burnTime; } diff --git a/src/main/java/zone/moddev/mc/basemetals/content/ModContent.java b/src/main/java/zone/moddev/mc/basemetals/content/ModContent.java index 07d7ea582..fb08e21fe 100644 --- a/src/main/java/zone/moddev/mc/basemetals/content/ModContent.java +++ b/src/main/java/zone/moddev/mc/basemetals/content/ModContent.java @@ -445,7 +445,7 @@ private static void registerFluid(String name, int colour, boolean mercury) { flowing[0] = FLUIDS.register("flowing_" + name, () -> new ForgeFlowingFluid.Flowing(properties)); block[0] = BLOCKS.register(name, () -> new MoltenMetalBlock(() -> source[0].get(), BlockBehaviour.Properties.of().setId(BLOCKS.key(name)).mapColor(MapColor.FIRE).liquid().replaceable() - .pushReaction(PushReaction.DESTROY).noCollision().strength(100.0F).noLootTable(), mercury)); + .pushReaction(PushReaction.POPPED).noCollision().strength(100.0F).noLootTable(), mercury)); BLOCKS_BY_ID.put(name, block[0]); // Forge 1.12 registered an ItemBlock under every molten block ID. Keep // those hidden inventory IDs so old stacks can survive a world upgrade. diff --git a/src/main/java/zone/moddev/mc/basemetals/migration/Legacy112WorldMigrator.java b/src/main/java/zone/moddev/mc/basemetals/migration/Legacy112WorldMigrator.java index b8b55a2d6..a0cfe0f3a 100644 --- a/src/main/java/zone/moddev/mc/basemetals/migration/Legacy112WorldMigrator.java +++ b/src/main/java/zone/moddev/mc/basemetals/migration/Legacy112WorldMigrator.java @@ -40,7 +40,6 @@ import net.minecraft.nbt.ListTag; import net.minecraft.nbt.LongArrayTag; import net.minecraft.nbt.NbtIo; -import net.minecraft.nbt.NbtUtils; import net.minecraft.nbt.Tag; import net.minecraft.resources.Identifier; import net.minecraft.util.datafix.DataFixTypes; @@ -668,7 +667,7 @@ private static CompoundTag restoredState(LegacyBlock legacy, Map property : state.getProperties()) { + writeProperty(properties, state, property); + } + tag.put("Properties", properties); + } + + return tag; + } + + private static > void writeProperty(CompoundTag target, + BlockState state, Property property) { + target.putString(property.getName(), property.getName(state.getValue(property))); + } + + static String blockName(Tag state) { + if (state instanceof CompoundTag compound) { + return compound.getStringOr("id", compound.getStringOr("Name", "")); + } + if (state instanceof StringTag text) { + return text.value(); + } + + return ""; + } + + static CompoundTag properties(CompoundTag state) { + return state.getCompoundOrEmpty(state.contains("properties") ? "properties" : "Properties"); + } +} diff --git a/src/main/java/zone/moddev/mc/basemetals/migration/LegacyWorldDataHook.java b/src/main/java/zone/moddev/mc/basemetals/migration/LegacyWorldDataHook.java index 4b1d74f68..71b5b4d66 100644 --- a/src/main/java/zone/moddev/mc/basemetals/migration/LegacyWorldDataHook.java +++ b/src/main/java/zone/moddev/mc/basemetals/migration/LegacyWorldDataHook.java @@ -56,7 +56,6 @@ import net.minecraft.nbt.Tag; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.NbtUtils; import net.minecraft.world.level.block.state.properties.AttachFace; import net.minecraft.world.level.block.state.properties.DoorHingeSide; import net.minecraft.world.level.block.state.properties.DoubleBlockHalf; @@ -370,9 +369,9 @@ static int migrateWallStates(Tag value) { int changed = 0; if (value instanceof CompoundTag) { CompoundTag compound = (CompoundTag) value; - String name = compound.getStringOr("Name", ""); + String name = LegacyBlockStateCodec.blockName(compound); if (name.startsWith("basemetals:") && name.endsWith("_wall")) { - CompoundTag properties = compound.getCompoundOrEmpty("Properties"); + CompoundTag properties = LegacyBlockStateCodec.properties(compound); for (String direction : new String[] {"north", "east", "south", "west"}) { String connection = properties.getStringOr(direction, ""); if ("true".equals(connection) || "false".equals(connection)) { @@ -441,7 +440,7 @@ private static String paletteBlockAt(CompoundTag level, int x, int y, int z) { if (word >= states.length) return ""; state = (int) ((states[word] >>> ((position % perWord) * bits)) & ((1L << bits) - 1)); } - return state < palette.size() ? palette.getCompoundOrEmpty(state).getStringOr("Name", "") : ""; + return state < palette.size() ? LegacyBlockStateCodec.blockName(palette.get(state)) : ""; } return ""; } @@ -511,7 +510,7 @@ private static int installLegacyBlockStates(CompoundTag snapshot) { for (int meta = 0; meta < 16; meta++) { BlockState state = legacyState(block, entry.getKey().getPath(), meta); table[(entry.getValue().intValue() << 4) | meta] = - new Dynamic<>(net.minecraft.nbt.NbtOps.INSTANCE, NbtUtils.writeBlockState(state)); + new Dynamic<>(net.minecraft.nbt.NbtOps.INSTANCE, LegacyBlockStateCodec.writeFlattenedState(state)); mapped++; } } diff --git a/src/main/java/zone/moddev/mc/basemetals/mixin/MonsterRoomFeatureMixin.java b/src/main/java/zone/moddev/mc/basemetals/mixin/MonsterRoomFeatureMixin.java new file mode 100644 index 000000000..22c6028b5 --- /dev/null +++ b/src/main/java/zone/moddev/mc/basemetals/mixin/MonsterRoomFeatureMixin.java @@ -0,0 +1,27 @@ +package zone.moddev.mc.basemetals.mixin; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.feature.MonsterRoomFeature; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import zone.moddev.mc.basemetals.migration.LegacyWorldDataHook; + +/** Keeps dungeon generation out of imported terrain before it tries to place a spawner. */ +@Mixin(MonsterRoomFeature.class) +abstract class MonsterRoomFeatureMixin { + @Inject(method = "place(Lnet/minecraft/world/level/WorldGenLevel;Lnet/minecraft/world/level/chunk/ChunkGenerator;Lnet/minecraft/util/RandomSource;Lnet/minecraft/core/BlockPos;)Z", + at = @At("HEAD"), cancellable = true) + private void basemetals$leaveImportedRoomsAlone(WorldGenLevel level, ChunkGenerator generator, + RandomSource random, BlockPos position, CallbackInfoReturnable callback) { + if (level instanceof WorldGenRegion region + && LegacyWorldDataHook.shouldBlockWorldgenWrite(region, position)) { + callback.setReturnValue(false); + } + } +} diff --git a/src/main/java/zone/moddev/mc/basemetals/recipe/ContentCraftingRecipe.java b/src/main/java/zone/moddev/mc/basemetals/recipe/ContentCraftingRecipe.java index 79159a3ce..6cb8208d1 100644 --- a/src/main/java/zone/moddev/mc/basemetals/recipe/ContentCraftingRecipe.java +++ b/src/main/java/zone/moddev/mc/basemetals/recipe/ContentCraftingRecipe.java @@ -14,6 +14,7 @@ import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.RecipeSerializer; import net.minecraft.world.item.crafting.display.RecipeDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplay; import net.minecraft.world.level.Level; import net.minecraftforge.registries.ForgeRegistries; import zone.moddev.mc.basemetals.config.ContentPolicy; @@ -22,14 +23,23 @@ public final class ContentCraftingRecipe implements CraftingRecipe { public static final RecipeSerializer SERIALIZER = Serializer.create(); private final CraftingRecipe plain; + private final String resultItem; private ContentCraftingRecipe(CraftingRecipe plain) { this.plain = plain; + List displays = plain.display(); + + if (displays.size() != 1 + || !(displays.getFirst().result() instanceof SlotDisplay.ItemStackSlotDisplay result)) { + throw new IllegalArgumentException("Content-mode recipes need a fixed crafting output"); + } + + // Recipe loading happens before item components are ready to make stacks. + this.resultItem = ForgeRegistries.ITEMS.getKey(result.stack().item().value()).toString(); } private boolean allowed() { - ItemStack result = plain.assemble(CraftingInput.EMPTY); - return ContentPolicy.active().allows(ForgeRegistries.ITEMS.getKey(result.getItem()).toString()); + return ContentPolicy.active().allows(resultItem); } private static ContentCraftingRecipe wrap(Recipe recipe) { @@ -86,11 +96,11 @@ public RecipeSerializer getSerializer() { } private static final class Serializer { - private static final MapCodec CODEC = Recipe.CODEC.fieldOf("recipe") + private static final MapCodec CODEC = Recipe.DIRECT_CODEC.fieldOf("recipe") .xmap(ContentCraftingRecipe::wrap, recipe -> recipe.plain); // Preserve empty pattern slots and tag names instead of flattening the ingredients. private static final StreamCodec STREAM_CODEC = - ByteBufCodecs.fromCodecWithRegistries(Recipe.CODEC) + ByteBufCodecs.fromCodecWithRegistries(Recipe.DIRECT_CODEC) .map(ContentCraftingRecipe::wrap, recipe -> recipe.plain); private static RecipeSerializer create() { diff --git a/src/main/java/zone/moddev/mc/basemetals/recipe/LegacySmeltingRecipe.java b/src/main/java/zone/moddev/mc/basemetals/recipe/LegacySmeltingRecipe.java index cfe299618..96808dd50 100644 --- a/src/main/java/zone/moddev/mc/basemetals/recipe/LegacySmeltingRecipe.java +++ b/src/main/java/zone/moddev/mc/basemetals/recipe/LegacySmeltingRecipe.java @@ -44,7 +44,7 @@ public static final class Serializer { private Serializer() {} public static RecipeSerializer create() { - return new RecipeSerializer<>(cookingMapCodec(LegacySmeltingRecipe::new, 200), + return new RecipeSerializer<>(cookingMapCodec(LegacySmeltingRecipe::new), cookingStreamCodec(LegacySmeltingRecipe::new)); } } diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index 1b932ddd2..ac2e67484 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -14,7 +14,7 @@ description='''${mod_description}''' [[dependencies.${mod_id}]] modId="orespawn" mandatory=true -versionRange="[4.1.0.2602001,5.0.0)" +versionRange="[4.1.0.2603001,5.0.0)" ordering="AFTER" side="BOTH" diff --git a/src/main/resources/basemetals.mixins.json b/src/main/resources/basemetals.mixins.json index a6b98faa4..0a223a165 100644 --- a/src/main/resources/basemetals.mixins.json +++ b/src/main/resources/basemetals.mixins.json @@ -9,6 +9,7 @@ "BlockStateDataMixin", "LevelCapabilityDataMixin", "LevelStorageSourceMixin", + "MonsterRoomFeatureMixin", "OreSpawnEarlyTagCompatibilityMixin", "SimpleRegionStorageMixin", "WorldGenRegionMixin" diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index 9ea2b4f5f..328c020bc 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,7 +1,7 @@ { "pack": { "description": "${mod_id} resources", - "min_format": [107, 1], - "max_format": [107, 1] + "min_format": [121, 0], + "max_format": [121, 0] } } diff --git a/src/packagedClientSmoke/packaged-resources/META-INF/mods.toml b/src/packagedClientSmoke/packaged-resources/META-INF/mods.toml index 6ad3ab50d..8ff095bcb 100644 --- a/src/packagedClientSmoke/packaged-resources/META-INF/mods.toml +++ b/src/packagedClientSmoke/packaged-resources/META-INF/mods.toml @@ -10,13 +10,13 @@ displayName="Base Metals packaged client smoke" [[dependencies.basemetals_client_smoke]] modId="basemetals" mandatory=true -versionRange="[3.1.1.2602001]" +versionRange="[3.1.1.2603001]" ordering="AFTER" side="CLIENT" [[dependencies.basemetals_client_smoke]] modId="orespawn" mandatory=true -versionRange="[4.1.0.2602001]" +versionRange="[4.1.0.2603001]" ordering="AFTER" side="CLIENT" diff --git a/src/packagedServerSmoke/java/zone/moddev/mc/basemetals/smoke/PackagedServerSmoke.java b/src/packagedServerSmoke/java/zone/moddev/mc/basemetals/smoke/PackagedServerSmoke.java index cb5cdfff3..7520225d2 100644 --- a/src/packagedServerSmoke/java/zone/moddev/mc/basemetals/smoke/PackagedServerSmoke.java +++ b/src/packagedServerSmoke/java/zone/moddev/mc/basemetals/smoke/PackagedServerSmoke.java @@ -40,7 +40,7 @@ public PackagedServerSmoke() { public void onServerStarted(ServerStartedEvent event) { String baseMetals = version("basemetals"); String oreSpawn = version("orespawn"); - if (!"3.1.1.2602001".equals(baseMetals) || !"4.1.0.2602001".equals(oreSpawn)) { + if (!"3.1.1.2603001".equals(baseMetals) || !"4.1.0.2603001".equals(oreSpawn)) { throw new IllegalStateException("PACKAGED_SERVER_SMOKE FAIL versions basemetals=" + baseMetals + " orespawn=" + oreSpawn); } @@ -80,8 +80,7 @@ private static void verifyContent(net.minecraft.server.MinecraftServer server) { for (var entry : items.entrySet()) { String id = entry.getKey(); if (id.endsWith("_powder") || id.endsWith("_smallpowder") || id.endsWith("_nugget")) { - int burn = server.fuelValues().burnDuration(entry.getValue().get().getDefaultInstance(), - net.minecraft.world.item.crafting.RecipeType.SMELTING); + int burn = new TestFurnace().burnDuration(level, entry.getValue().get().getDefaultInstance()); int expected = id.startsWith("coal_") || id.startsWith("charcoal_") ? (id.endsWith("_powder") ? 1600 : 200) : 0; if (burn != expected) throw new IllegalStateException(id + " has burn time " + burn + ", expected " + expected); @@ -103,6 +102,16 @@ private static void verifyContent(net.minecraft.server.MinecraftServer server) { zone.moddev.mc.basemetals.config.BaseMetalsConfig.activeMode().serializedName(), items.size()); } + private static final class TestFurnace extends net.minecraft.world.level.block.entity.FurnaceBlockEntity { + private TestFurnace() { + super(net.minecraft.core.BlockPos.ZERO, net.minecraft.world.level.block.Blocks.FURNACE.defaultBlockState()); + } + + private int burnDuration(net.minecraft.server.level.ServerLevel level, net.minecraft.world.item.ItemStack stack) { + return getBurnDuration(level, stack); + } + } + private static void verifyIntegrationRecipes(net.minecraft.server.MinecraftServer server) throws IOException { var policy = zone.moddev.mc.basemetals.config.ContentPolicy.active(); int checked = 0; @@ -176,7 +185,7 @@ private static void verifyOreSample(net.minecraft.server.MinecraftServer server) } private static void verifyTinkers() { - throw new IllegalStateException("Tinkers integration is not enabled in this Forge 26.2 build"); + throw new IllegalStateException("Tinkers integration is not enabled in this Forge 26.3 build"); } private static void verifyModernFlatTerrain(net.minecraft.server.MinecraftServer server) { diff --git a/src/packagedServerSmoke/packaged-resources/META-INF/mods.toml b/src/packagedServerSmoke/packaged-resources/META-INF/mods.toml index 898db2c93..42d533b43 100644 --- a/src/packagedServerSmoke/packaged-resources/META-INF/mods.toml +++ b/src/packagedServerSmoke/packaged-resources/META-INF/mods.toml @@ -10,13 +10,13 @@ displayName="Base Metals packaged server smoke" [[dependencies.basemetals_server_smoke]] modId="basemetals" mandatory=true -versionRange="[3.1.1.2602001]" +versionRange="[3.1.1.2603001]" ordering="AFTER" side="SERVER" [[dependencies.basemetals_server_smoke]] modId="orespawn" mandatory=true -versionRange="[4.1.0.2602001]" +versionRange="[4.1.0.2603001]" ordering="AFTER" side="SERVER" diff --git a/src/smoke/datapack/pack.mcmeta b/src/smoke/datapack/pack.mcmeta index 0b61bc891..694a36be6 100644 --- a/src/smoke/datapack/pack.mcmeta +++ b/src/smoke/datapack/pack.mcmeta @@ -1,7 +1,7 @@ { "pack": { - "min_format": [107, 1], - "max_format": [107, 1], + "min_format": [121, 0], + "max_format": [121, 0], "description": "Base Metals custom ordinary dimension smoke fixture" } } diff --git a/src/test/java/zone/moddev/mc/basemetals/data/Minecraft1211ResourceTest.java b/src/test/java/zone/moddev/mc/basemetals/data/Minecraft1211ResourceTest.java index 95c17fc82..492aeb149 100644 --- a/src/test/java/zone/moddev/mc/basemetals/data/Minecraft1211ResourceTest.java +++ b/src/test/java/zone/moddev/mc/basemetals/data/Minecraft1211ResourceTest.java @@ -14,15 +14,15 @@ class Minecraft1211ResourceTest { @Test - void resourceAndDataPacksUseThe262Formats() throws Exception { + void resourceAndDataPacksUseThe263Formats() throws Exception { var pack = read("src/main/resources/pack.mcmeta").getAsJsonObject("pack"); - assertEquals("[107,1]", pack.get("min_format").toString()); - assertEquals("[107,1]", pack.get("max_format").toString()); + assertEquals("[121,0]", pack.get("min_format").toString()); + assertEquals("[121,0]", pack.get("max_format").toString()); net.minecraft.SharedConstants.tryDetectVersion(); var resources = net.minecraft.SharedConstants.getCurrentVersion() .packVersion(net.minecraft.server.packs.PackType.CLIENT_RESOURCES); - assertEquals(88, resources.major()); - assertEquals(0, resources.minor()); + assertEquals(97, resources.major()); + assertEquals(1, resources.minor()); } @Test @@ -46,7 +46,7 @@ void silkTouchPredicatesUseTheEnchantmentSetField() throws Exception { var loot = read("src/generated/resources/data/basemetals/loot_table/blocks/adamantine_ore.json"); var silk = loot.getAsJsonArray("pools").get(0).getAsJsonObject().getAsJsonArray("entries").get(0) .getAsJsonObject().getAsJsonArray("children").get(0).getAsJsonObject() - .getAsJsonArray("conditions").get(0).getAsJsonObject().getAsJsonObject("predicate") + .getAsJsonObject("condition").getAsJsonObject("predicate") .getAsJsonObject("predicates").getAsJsonArray("minecraft:enchantments").get(0).getAsJsonObject(); assertEquals("minecraft:silk_touch", silk.get("enchantments").getAsString()); @@ -54,6 +54,45 @@ void silkTouchPredicatesUseTheEnchantmentSetField() throws Exception { assertFalse(silk.has("predicates")); } + @Test + void lootAndTradeDispatchKeysUseTheNativeTypeField() throws Exception { + for (String directory : new String[] {"loot_table", "loot_modifiers", "villager_trade"}) { + Path root = Path.of("src/generated/resources/data/basemetals", directory); + + try (var paths = Files.walk(root)) { + for (Path path : paths.filter(file -> file.toString().endsWith(".json")).toList()) { + String json = Files.readString(path); + assertFalse(json.contains("\"condition\": \""), path.toString()); + assertFalse(json.contains("\"function\":"), path.toString()); + assertFalse(json.contains("\"functions\":"), path.toString()); + if (directory.equals("loot_table")) { + assertFalse(json.contains("\"conditions\":"), path.toString()); + } + } + } + } + + var predicate = read("src/generated/resources/data/basemetals/villager_trade/weaponsmith/steel_sword.json") + .getAsJsonObject("merchant_predicate"); + assertEquals("basemetals:trade_enabled", predicate.get("type").getAsString()); + var enchanted = read("src/generated/resources/data/basemetals/villager_trade/armorer/brass_helmet_enchanted.json"); + assertTrue(enchanted.has("given_item_modifier")); + assertFalse(enchanted.has("given_item_modifiers")); + + var door = read("src/generated/resources/data/basemetals/loot_table/blocks/steel_door.json") + .getAsJsonArray("pools").get(0).getAsJsonObject().getAsJsonObject("condition") + .getAsJsonArray("terms").get(1).getAsJsonObject(); + assertEquals("minecraft:match_block", door.get("type").getAsString()); + assertEquals("basemetals:steel_door", door.get("blocks").getAsString()); + assertEquals("lower", door.getAsJsonObject("state").get("half").getAsString()); + + var slab = read("src/generated/resources/data/basemetals/loot_table/blocks/steel_slab.json") + .getAsJsonArray("pools").get(0).getAsJsonObject().getAsJsonArray("entries").get(0) + .getAsJsonObject().getAsJsonArray("modifier").get(0).getAsJsonObject() + .getAsJsonObject("condition"); + assertEquals("double", slab.getAsJsonObject("state").get("type").getAsString()); + } + @Test void scythesKeepTheirPlantAndCobwebHarvestingTag() throws Exception { var values = read("src/main/resources/data/basemetals/tags/block/scythe_harvestable.json") diff --git a/src/test/java/zone/moddev/mc/basemetals/data/RecipeRegressionTest.java b/src/test/java/zone/moddev/mc/basemetals/data/RecipeRegressionTest.java index 2d7b7f4a6..b3a9133af 100644 --- a/src/test/java/zone/moddev/mc/basemetals/data/RecipeRegressionTest.java +++ b/src/test/java/zone/moddev/mc/basemetals/data/RecipeRegressionTest.java @@ -106,6 +106,8 @@ void existing113RecipeAdvancementCriteriaRemainAvailable() throws Exception { JsonObject criteria = read(DATA.resolve("advancement/recipes/adamantine_bow.json")) .getAsJsonObject("criteria"); assertEquals("minecraft:recipe_unlocked", criteria.getAsJsonObject("has_recipe").get("trigger").getAsString()); + assertEquals("basemetals:adamantine_bow", criteria.getAsJsonObject("has_recipe") + .getAsJsonObject("conditions").getAsJsonArray("recipes").get(0).getAsString()); assertEquals("#forge:rods/adamantine", criteria.getAsJsonObject("has_ingredient_1") .getAsJsonObject("conditions").getAsJsonArray("items").get(0).getAsJsonObject().get("items").getAsString()); } diff --git a/src/test/java/zone/moddev/mc/basemetals/data/ResourceIntegrityTest.java b/src/test/java/zone/moddev/mc/basemetals/data/ResourceIntegrityTest.java index 8fe3c56a9..fc18e2599 100644 --- a/src/test/java/zone/moddev/mc/basemetals/data/ResourceIntegrityTest.java +++ b/src/test/java/zone/moddev/mc/basemetals/data/ResourceIntegrityTest.java @@ -518,7 +518,7 @@ void customAnvilLootRetainsDamageWithoutUsingTheUnsafeVanillaTag() throws Except JsonObject loot = read(resource("data/basemetals/loot_table/blocks/stone_anvil.json")) .getAsJsonObject(); JsonArray functions = loot.getAsJsonArray("pools").get(0).getAsJsonObject() - .getAsJsonArray("entries").get(0).getAsJsonObject().getAsJsonArray("functions"); + .getAsJsonArray("entries").get(0).getAsJsonObject().getAsJsonArray("modifier"); assertTrue(functions.toString().contains("minecraft:copy_state")); assertTrue(functions.toString().contains("\"damage\"")); } diff --git a/src/test/java/zone/moddev/mc/basemetals/migration/LegacyBlockStateCodecTest.java b/src/test/java/zone/moddev/mc/basemetals/migration/LegacyBlockStateCodecTest.java new file mode 100644 index 000000000..ea4219175 --- /dev/null +++ b/src/test/java/zone/moddev/mc/basemetals/migration/LegacyBlockStateCodecTest.java @@ -0,0 +1,52 @@ +package zone.moddev.mc.basemetals.migration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import com.mojang.serialization.Dynamic; +import net.minecraft.SharedConstants; +import net.minecraft.core.Direction; +import net.minecraft.nbt.NbtOps; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.fixes.References; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.DoorBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class LegacyBlockStateCodecTest { + @BeforeAll + static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + void intermediatePalettesUseHistoricalFieldNames() { + var state = Blocks.OAK_DOOR.defaultBlockState() + .setValue(DoorBlock.FACING, Direction.WEST) + .setValue(DoorBlock.OPEN, true); + var tag = LegacyBlockStateCodec.writeFlattenedState(state); + + assertEquals("minecraft:oak_door", tag.getStringOr("Name", "")); + assertEquals("west", tag.getCompoundOrEmpty("Properties").getStringOr("facing", "")); + assertEquals("true", tag.getCompoundOrEmpty("Properties").getStringOr("open", "")); + assertFalse(tag.contains("id")); + assertFalse(tag.contains("properties")); + } + + @Test + void vanillaCanUpgradeTheIntermediateStateWithoutLosingItsProperties() { + BlockState state = Blocks.OAK_DOOR.defaultBlockState() + .setValue(DoorBlock.FACING, Direction.WEST) + .setValue(DoorBlock.OPEN, true); + var source = new Dynamic<>(NbtOps.INSTANCE, LegacyBlockStateCodec.writeFlattenedState(state)); + var updated = DataFixers.getDataFixer().update(References.BLOCK_STATE, source, + 1631, SharedConstants.getCurrentVersion().dataVersion().version()); + var restored = BlockState.CODEC.parse(NbtOps.INSTANCE, updated.getValue()).getOrThrow(); + + assertEquals(state, restored); + } +} diff --git a/src/test/java/zone/moddev/mc/basemetals/migration/LegacyMigrationSafetyTest.java b/src/test/java/zone/moddev/mc/basemetals/migration/LegacyMigrationSafetyTest.java index 402af8953..c3051b407 100644 --- a/src/test/java/zone/moddev/mc/basemetals/migration/LegacyMigrationSafetyTest.java +++ b/src/test/java/zone/moddev/mc/basemetals/migration/LegacyMigrationSafetyTest.java @@ -100,7 +100,7 @@ void modernExternalChunk() throws Exception { @Test void modernLz4Chunk() throws Exception { - assumeTrue(VERSION.equals("1.20.6") || VERSION.equals("1.21.11") || VERSION.equals("26.2"), "Native LZ4 is not available on older targets"); + assumeTrue(VERSION.equals("1.20.6") || VERSION.equals("1.21.11") || VERSION.equals("26.3"), "Native LZ4 is not available on older targets"); Path world = world("lz4", false); Path file = region(world, false, true); assertTrue(nativeAccepts(file, false)); @@ -177,7 +177,7 @@ void savedRegistryCanBeScannedWithAnEmptyRegion() throws Exception { @Test void savedRegistryCanBeScannedWithLz4Chunks() throws Exception { - assumeTrue(VERSION.equals("1.20.6") || VERSION.equals("1.21.11") || VERSION.equals("26.2")); + assumeTrue(VERSION.equals("1.20.6") || VERSION.equals("1.21.11") || VERSION.equals("26.3")); Path world = world("saved-registry-lz4", false); writeRegistry(world.resolve("basemetals-legacy-registry.dat")); Path file = region(world, false, true); diff --git a/src/test/java/zone/moddev/mc/basemetals/migration/LegacyWorldDataHookTest.java b/src/test/java/zone/moddev/mc/basemetals/migration/LegacyWorldDataHookTest.java index 95c9c5ceb..c1d1aa356 100644 --- a/src/test/java/zone/moddev/mc/basemetals/migration/LegacyWorldDataHookTest.java +++ b/src/test/java/zone/moddev/mc/basemetals/migration/LegacyWorldDataHookTest.java @@ -9,6 +9,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; import net.minecraft.resources.Identifier; class LegacyWorldDataHookTest { @@ -26,6 +27,25 @@ void levelHookPassesTheFileBeforeComponentConversion() throws IOException { assertFalse(hook.contains("@Mixin(LevelStorageSource.class)")); } + @Test + void dungeonHookChecksTheExistingTerrainBoundaryBeforePlacement() throws Exception { + var method = net.minecraft.world.level.levelgen.feature.MonsterRoomFeature.class.getDeclaredMethod( + "place", net.minecraft.world.level.WorldGenLevel.class, + net.minecraft.world.level.chunk.ChunkGenerator.class, net.minecraft.util.RandomSource.class, + BlockPos.class); + assertEquals(boolean.class, method.getReturnType()); + + String hook = Files.readString(Path.of( + "src/main/java/zone/moddev/mc/basemetals/mixin/MonsterRoomFeatureMixin.java")); + assertTrue(hook.contains("@Mixin(MonsterRoomFeature.class)")); + assertTrue(hook.contains("at = @At(\"HEAD\")")); + assertTrue(hook.contains("level instanceof WorldGenRegion region")); + assertTrue(hook.contains("shouldBlockWorldgenWrite(region, position)")); + assertFalse(hook.contains("getBlockEntity")); + assertTrue(Files.readString(Path.of("src/main/resources/basemetals.mixins.json")) + .contains("\"MonsterRoomFeatureMixin\"")); + } + @Test void obsoleteVanillaBiomeSnapshotIsRemovedWithoutChangingSavedBiomesOrOtherRegistries() { CompoundTag root = biomeSnapshot("minecraft:plains", "minecraft:mountains"); @@ -169,6 +189,68 @@ void modernChunkCleanupRemovesOnlyBedsAtBaseMetalsBlocks() { assertEquals(17, entities.getCompoundOrEmpty(0).getIntOr("y", 0)); } + @Test + void nativeStateObjectsKeepPhantomBedCleanupScopedToBaseMetals() { + assertNativeBedCleanup(false); + } + + @Test + void compactNativeStatesKeepPhantomBedCleanupScopedToBaseMetals() { + assertNativeBedCleanup(true); + } + + private static void assertNativeBedCleanup(boolean compact) { + CompoundTag root = new CompoundTag(); + ListTag sections = new ListTag(); + sections.add(nativeSection(0, "basemetals:steel_block", compact)); + sections.add(nativeSection(1, "minecraft:red_bed", compact)); + root.put("sections", sections); + + ListTag entities = new ListTag(); + entities.add(bed(1)); + entities.add(bed(17)); + root.put("block_entities", entities); + + assertEquals(1, LegacyWorldDataHook.removePhantomBedEntities(root)); + assertEquals(1, entities.size()); + assertEquals(17, entities.getCompoundOrEmpty(0).getIntOr("y", 0)); + } + + private static CompoundTag nativeSection(int y, String block, boolean compact) { + CompoundTag section = section(y, block); + ListTag palette = new ListTag(); + + if (compact) { + palette.add(StringTag.valueOf(block)); + } else { + CompoundTag state = new CompoundTag(); + state.putString("id", block); + palette.add(state); + } + + section.getCompoundOrEmpty("block_states").put("palette", palette); + return section; + } + + @Test + void nativeWallFieldsKeepConnectionValuesDuringAnUpgrade() { + CompoundTag wall = new CompoundTag(); + wall.putString("id", "basemetals:steel_wall"); + CompoundTag properties = new CompoundTag(); + properties.putString("north", "true"); + properties.putString("east", "false"); + properties.putString("south", "tall"); + wall.put("properties", properties); + + assertEquals(2, LegacyWorldDataHook.migrateWallStates(wall)); + assertEquals("low", properties.getStringOr("north", "")); + assertEquals("none", properties.getStringOr("east", "")); + assertEquals("tall", properties.getStringOr("south", "")); + assertEquals(0, LegacyWorldDataHook.migrateWallStates(wall)); + assertFalse(wall.contains("Name")); + assertFalse(wall.contains("Properties")); + } + @Test void convertedChunksKeepBelowZeroRetrogen() { CompoundTag root = new CompoundTag(); diff --git a/tools/validation/README.md b/tools/validation/README.md index 749f15b7e..a46bbbda1 100644 --- a/tools/validation/README.md +++ b/tools/validation/README.md @@ -1,11 +1,16 @@ # World upgrade checks -`test_legacy_worlds.js` starts the packaged Forge 26.2 server against a fresh +The Linux client checks use Ubuntu 26.04, Xvfb and Mesa software rendering through EGL. +`check_sdl_graphics.py` first checks the same OpenGL 3.3 core and sRGB window +requirements as Minecraft. An unusable display fails immediately instead of +leaving the client waiting at a graphics error dialog. Normal launches are unchanged. + +`test_legacy_worlds.js` starts the packaged Forge 26.3 server against a fresh copy of each supplied world, then starts it again to check the saved result. It refuses to overwrite an existing test directory and fingerprints every source world before and after the test. -An optional `forgeVersion` selects another installed Forge 26.2 build for -compatibility checks; the default is the compilation target, `65.1.0`. +An optional `forgeVersion` selects another installed Forge 26.3 build for +compatibility checks; the default is the compilation target, `66.0.0`. Pass a local JSON specification containing `java` (the Java 25 executable), `serverRuntime`, `output`, `modJar`, `probeJar`, `oreSpawn` (a `path` and `sha256`), and a `profiles` list. @@ -24,6 +29,9 @@ OreSpawn profile and verifies that those values survive both starts. The log check accepts known vanilla registry changes, including paintings moved to data files. Unknown or modded entries still fail the test. Check that boundary with `node --test tools/validation/test_painting_registry.js`. +The retired vanilla `minecraft:is_tempted` memory is accepted only in its +original memory registry. Unknown and modded entries still fail; +`test_vanilla_registry.js` checks that boundary. On Windows, the exact missing-Perflib-counter diagnostic from Minecraft's system probe is reported separately. It is not accepted on Linux, from another logger @@ -59,3 +67,5 @@ Its local specification needs `java` (the Java 25 executable), `serverRuntime`, `output`, `modJar`, `serverProbe`, `clientProbe`, and the same checksum-pinned `oreSpawn` object used by the world tests. The test uses localhost only and chooses a free port; no public Minecraft server is involved. +The disposable login servers explicitly disable the whitelist, which Minecraft +26.3 enables by default. Normal server settings are not changed. diff --git a/tools/validation/check_sdl_graphics.py b/tools/validation/check_sdl_graphics.py new file mode 100644 index 000000000..e9e964e0b --- /dev/null +++ b/tools/validation/check_sdl_graphics.py @@ -0,0 +1,87 @@ +"""Check Minecraft's SDL/OpenGL requirements before starting a headless client.""" + +import argparse +import ctypes +import os +from pathlib import Path +import tempfile +from zipfile import ZipFile + + +def check_context(library: Path) -> None: + sdl = ctypes.CDLL(str(library)) + sdl.SDL_Init.argtypes = [ctypes.c_uint32] + sdl.SDL_Init.restype = ctypes.c_bool + sdl.SDL_GL_LoadLibrary.argtypes = [ctypes.c_char_p] + sdl.SDL_GL_LoadLibrary.restype = ctypes.c_bool + sdl.SDL_GL_GetProcAddress.argtypes = [ctypes.c_char_p] + sdl.SDL_GL_GetProcAddress.restype = ctypes.c_void_p + sdl.SDL_GL_SetAttribute.argtypes = [ctypes.c_int, ctypes.c_int] + sdl.SDL_GL_SetAttribute.restype = ctypes.c_bool + sdl.SDL_CreateWindow.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_int, ctypes.c_uint64] + sdl.SDL_CreateWindow.restype = ctypes.c_void_p + sdl.SDL_GL_CreateContext.argtypes = [ctypes.c_void_p] + sdl.SDL_GL_CreateContext.restype = ctypes.c_void_p + sdl.SDL_GL_DestroyContext.argtypes = [ctypes.c_void_p] + sdl.SDL_DestroyWindow.argtypes = [ctypes.c_void_p] + sdl.SDL_GetError.restype = ctypes.c_char_p + + def require(success: object, operation: str) -> None: + if not success: + error = sdl.SDL_GetError().decode('utf-8', errors='replace') + raise RuntimeError(f'{operation}: {error}') + + window = None + context = None + + try: + require(sdl.SDL_Init(0x20), 'SDL video initialization failed') + require(sdl.SDL_GL_LoadLibrary(b'libGL.so.1'), 'OpenGL library could not load') + + # Minecraft also checks that SDL and LWJGL resolve the same OpenGL functions. + opengl = ctypes.CDLL('libGL.so.1') + opengl.glXGetProcAddress.argtypes = [ctypes.c_char_p] + opengl.glXGetProcAddress.restype = ctypes.c_void_p + if opengl.glXGetProcAddress(b'glGetError') != sdl.SDL_GL_GetProcAddress(b'glGetError'): + raise RuntimeError('SDL and the OpenGL loader resolve different glGetError functions') + + # Match GlBackend: OpenGL 3.3 core, forward-compatible, with an sRGB framebuffer. + for attribute, value in ((17, 3), (18, 3), (20, 1), (19, 2), (22, 1)): + require(sdl.SDL_GL_SetAttribute(attribute, value), 'OpenGL attribute was rejected') + + window = sdl.SDL_CreateWindow(b'Base Metals graphics check', 320, 480, 2147614730) + require(window, 'OpenGL window could not open') + context = sdl.SDL_GL_CreateContext(window) + require(context, 'OpenGL context could not open') + print('SDL_OPENGL_CONTEXT_PASS version=3.3 profile=core srgb=requested') + finally: + if context: + sdl.SDL_GL_DestroyContext(context) + if window: + sdl.SDL_DestroyWindow(window) + sdl.SDL_Quit() + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('native_jar', type=Path, help='The checksum-verified LWJGL SDL Linux native JAR') + args = parser.parse_args() + + if not os.environ.get('DISPLAY'): + raise RuntimeError('No X11 display is available. Run this check under xvfb-run.') + if os.environ.get('SDL_OPENGL_FORCE_SRGB_FRAMEBUFFER'): + raise RuntimeError('Remove the sRGB override; this check must use Minecraft\'s graphics requirements.') + + with tempfile.TemporaryDirectory(prefix='basemetals-sdl-check-') as directory: + library = Path(directory) / 'libSDL3.so' + with ZipFile(args.native_jar) as archive: + entries = [entry for entry in archive.namelist() if entry.endswith('/libSDL3.so')] + if len(entries) != 1: + raise RuntimeError('Expected exactly one SDL3 library in the verified native JAR') + library.write_bytes(archive.read(entries[0])) + + check_context(library) + + +if __name__ == '__main__': + main() diff --git a/tools/validation/compare_current_world.py b/tools/validation/compare_current_world.py index 03c4722d5..bcf8bf66f 100644 --- a/tools/validation/compare_current_world.py +++ b/tools/validation/compare_current_world.py @@ -13,7 +13,7 @@ import nbtlib -from compare_world_terrain import block_names, chunks +from compare_world_terrain import block_names, chunks, state_name, unwrap_state from world_paths import dimensions, player_files @@ -126,9 +126,30 @@ def canonical_player(player): return result +def canonical_block_state(state, defaults): + state = unwrap_state(state) + identifier = state_name(state) + properties = dict(defaults.get(identifier, {})) + + if isinstance(state, (dict, nbtlib.Compound)): + properties.update({str(key): str(value) for key, value in + state.get('properties', state.get('Properties', {})).items()}) + elif identifier.startswith('basemetals:') and identifier not in defaults: + raise AssertionError(f'No registered default state for compact palette entry: {identifier}') + + if identifier.startswith('basemetals:') and identifier.endswith('_wall'): + for direction in ('north', 'south', 'east', 'west'): + if properties.get(direction) in ('false', 'true'): + properties[direction] = 'low' if properties[direction] == 'true' else 'none' + + return json.dumps({'id': identifier, 'properties': properties}, sort_keys=True) + + def snapshot(world): blocks = {} stacks = Counter() + manifest = Path(__file__).resolve().parents[2] / 'src/generated/resources/data/basemetals/registry_manifest.json' + defaults = json.loads(manifest.read_text(encoding='utf8')).get('block_defaults', {}) def items(value): if isinstance(value, (dict, nbtlib.Compound)): @@ -149,19 +170,18 @@ def items(value): for key, chunk in chunks(world).items(): for section in chunk.get("sections", chunk.get("Sections", [])): original_palette = section.get('block_states', {}).get('palette', section.get('Palette', [])) - if not any(str(entry.get('Name', '')).startswith('basemetals:') for entry in original_palette): + if not any(state_name(entry).startswith('basemetals:') for entry in original_palette): continue source = copy.deepcopy(section) palette = source.get("block_states", {}).get("palette", source.get("Palette", [])) - for entry in palette: - # Older wall connections used booleans; Minecraft now stores none, low or tall. - if str(entry.get("Name", "")).startswith("basemetals:") and str(entry["Name"]).endswith("_wall"): - properties = entry.get("Properties", {}) - for direction in ("north", "south", "east", "west"): - if str(properties.get(direction, "")) in ("false", "true"): - properties[direction] = nbtlib.String("low" if str(properties[direction]) == "true" else "none") - state = ordered(entry).snbt() - entry["Name"] = nbtlib.String(state) + canonical = nbtlib.List[nbtlib.Compound]([ + nbtlib.Compound({'Name': nbtlib.String(canonical_block_state(entry, defaults))}) + for entry in palette + ]) + if 'block_states' in source: + source['block_states']['palette'] = canonical + else: + source['Palette'] = canonical for index, state in enumerate(block_names(source)): if state and "basemetals:" in state: blocks[(*key, int(section["Y"]), index)] = state diff --git a/tools/validation/compare_world_terrain.py b/tools/validation/compare_world_terrain.py index f3042660c..aad47d6e5 100644 --- a/tools/validation/compare_world_terrain.py +++ b/tools/validation/compare_world_terrain.py @@ -53,6 +53,20 @@ def chunks(world): return result +def unwrap_state(state): + # Native mixed lists wrap entries that are not already compounds. + while isinstance(state, (dict, nbtlib.Compound)) and len(state) == 1 and "" in state: + state = state[""] + return state + + +def state_name(state): + state = unwrap_state(state) + if isinstance(state, (str, nbtlib.String)): + return str(state) + return str(state.get("id", state.get("Name", ""))) + + def block_names(section): if "Blocks" in section: blocks = section["Blocks"] @@ -68,7 +82,7 @@ def block_names(section): return block_states = section.get("block_states", {}) - palette = [str(state["Name"]) for state in section.get("Palette", block_states.get("palette", []))] + palette = [state_name(state) for state in section.get("Palette", block_states.get("palette", []))] if not palette: yield from [None] * 4096 return @@ -110,13 +124,13 @@ def collect(dimension, entities): name = str(item.get("id", "")) if name in FALLING_BLOCKS: dropped_items.append({"dimension": dimension, "name": name, - "count": int(item.get("Count", 0)), + "count": int(item.get("count", item.get("Count", 0))), "position": tuple(float(value) for value in entity["Pos"]), "uuid": tuple(entity.get("UUID", []))}) continue if str(entity.get("id", "")) != "minecraft:falling_block": continue - name = str(entity.get("BlockState", {}).get("Name", "")) + name = state_name(entity.get("BlockState", entity.get("block_state", {}))) if name not in FALLING_BLOCKS: continue x, y, z = [math.floor(float(value)) for value in entity["Pos"]] diff --git a/tools/validation/test_content_modes.js b/tools/validation/test_content_modes.js index b9b4de559..dc2e84676 100644 --- a/tools/validation/test_content_modes.js +++ b/tools/validation/test_content_modes.js @@ -94,7 +94,7 @@ function verifyConfig(directory, initial) { (async () => { const results = []; - const client = clientLaunch(spec.clientRuntime, '26.2', '65.1.0'); + const client = clientLaunch(spec.clientRuntime, '26.3', '66.0.0'); const selected = spec.cases ? cases.filter(([name]) => spec.cases.includes(name)) : cases; if (!selected.length || (spec.cases && selected.length !== spec.cases.length)) { throw new Error('The requested mode-test case is unknown or duplicated.'); @@ -109,19 +109,19 @@ function verifyConfig(directory, initial) { const port = await freePort(); fs.writeFileSync(path.join(serverDir, 'eula.txt'), 'eula=true\n'); fs.writeFileSync(path.join(serverDir, 'server.properties'), - `online-mode=false\nserver-ip=127.0.0.1\nserver-port=${port}\nview-distance=3\n` + `online-mode=false\nwhite-list=false\nserver-ip=127.0.0.1\nserver-port=${port}\nview-distance=3\n` + 'simulation-distance=3\nlevel-type=minecraft:flat\n' + 'generator-settings={"layers":[{"block":"minecraft:bedrock","height":1},{"block":"minecraft:dirt","height":2},{"block":"minecraft:grass_block","height":1}],"biome":"minecraft:plains"}\n' + 'generate-structures=false\nmax-tick-time=-1\n'); const server = launch(serverDir, ['-Xms256m', '-Xmx2g', '-Dbasemetals.awaitLoginSmoke=true', - ...serverArguments(spec.serverRuntime, serverDir, '26.2', '65.1.0')], 'server.log'); + ...serverArguments(spec.serverRuntime, serverDir, '26.3', '66.0.0')], 'server.log'); let player; try { await waitFor(server, 'BASEMETALS_PACKAGED_SERVER_SMOKE PASS', 240000); player = launch(clientDir, ['-Xms512m', '-Xmx2g', '-Dbasemetals.remoteLoginSmoke=true', `-Dbasemetals.remoteAddress=127.0.0.1:${port}`, `-Dbasemetals.loginSmokeConnections=${reject ? 1 : 3}`, `-Dbasemetals.expectModeReject=${!!reject}`, ...client.args, ...client.metadata.arguments.game, - '--username', 'BMValidation', '--version', '26.2', '--gameDir', clientDir, + '--username', 'BMValidation', '--version', '26.3', '--gameDir', clientDir, '--assetsDir', path.join(spec.clientRuntime, 'assets'), '--assetIndex', client.vanilla.assetIndex.id, '--uuid', '00000000000000000000000000000118', '--accessToken', '0', '--userType', 'legacy'], 'client.log'); await waitFor(player, 'BASEMETALS_MODE_LOGIN_CLIENT PASS result=' + (reject ? 'rejected' : 'connected'), 240000); diff --git a/tools/validation/test_current_world.py b/tools/validation/test_current_world.py index f0982dcb6..f6b1fa1ee 100644 --- a/tools/validation/test_current_world.py +++ b/tools/validation/test_current_world.py @@ -13,6 +13,39 @@ import compare_current_world as content +class NativeBlockStateTest(unittest.TestCase): + def setUp(self): + self.defaults = {'basemetals:steel_wall': { + 'north': 'none', 'south': 'none', 'east': 'none', 'west': 'none', + 'up': 'true', 'waterlogged': 'false', + }} + + def canonical(self, value): + return content.canonical_block_state(nbtlib.parse_nbt(value), self.defaults) + + def test_old_and_native_default_states_match(self): + old = '{Name:"basemetals:steel_wall",Properties:{north:"false",south:"false",east:"false",west:"false",up:"true",waterlogged:"false"}}' + native = '{id:"basemetals:steel_wall",properties:{north:"none",south:"none",east:"none",west:"none",up:"true",waterlogged:"false"}}' + self.assertEqual(self.canonical(old), self.canonical(native)) + self.assertEqual(self.canonical(old), self.canonical('"basemetals:steel_wall"')) + + def test_non_default_connections_are_not_lost(self): + old = '{Name:"basemetals:steel_wall",Properties:{north:"true",east:"tall"}}' + native = '{id:"basemetals:steel_wall",properties:{north:"low",east:"tall"}}' + self.assertEqual(self.canonical(old), self.canonical(native)) + self.assertNotEqual(self.canonical(old), self.canonical('"basemetals:steel_wall"')) + + def test_unknown_properties_are_not_discarded(self): + old = '{Name:"basemetals:steel_wall",Properties:{future_property:"keep_me"}}' + self.assertNotEqual(self.canonical(old), self.canonical('"basemetals:steel_wall"')) + + def test_native_mixed_palette_wrappers(self): + self.assertEqual(self.canonical('"basemetals:steel_wall"'), + self.canonical('{"":"basemetals:steel_wall"}')) + self.assertEqual(self.canonical('{id:"basemetals:steel_wall",properties:{east:"tall"}}'), + self.canonical('{"":{id:"basemetals:steel_wall",properties:{east:"tall"}}}')) + + class SavedEntitiesTest(unittest.TestCase): def test_entity_regions_include_ammunition_and_equipment(self): for compression in (1, 2): diff --git a/tools/validation/test_legacy_worlds.js b/tools/validation/test_legacy_worlds.js index fea11afc0..d41221e4b 100644 --- a/tools/validation/test_legacy_worlds.js +++ b/tools/validation/test_legacy_worlds.js @@ -5,6 +5,7 @@ const crypto = require('crypto'); const { spawn } = require('child_process'); const { serverArguments } = require('./forge_launcher'); const { isVanillaPaintingSnapshot } = require('./painting_registry'); +const { isRetiredVanillaMemorySnapshot } = require('./vanilla_registry'); const { isWindowsCounterDiagnostic } = require('./runtime_log'); const spec = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); @@ -132,7 +133,7 @@ function checkUpgradeLog(log) { 'minecraft:block.trial_spawner.charge_activate' ].includes(entry)); - if (knownPaintingSnapshot || knownVanillaNames) { + if (knownPaintingSnapshot || knownVanillaNames || isRetiredVanillaMemorySnapshot(registry, entries)) { entries.forEach(entry => acceptedEntries.add(entry)); continue; } @@ -154,7 +155,7 @@ async function run(directory, launcher, label, marker, properties = []) { const output = fs.createWriteStream(path.join(directory, label + '.log')); const executable = phase === 'capture' ? spec.java8 : (spec.java || spec.java17); const launchArguments = phase === 'capture' ? ['-jar', launcher, 'nogui'] - : serverArguments(spec.serverRuntime, directory, '26.2', spec.forgeVersion || '65.1.0'); + : serverArguments(spec.serverRuntime, directory, '26.3', spec.forgeVersion || '66.0.0'); const child = spawn(executable, ['-Xms256m', '-Xmx2g', ...properties, ...launchArguments], { cwd: directory, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }); child.stdout.pipe(output, { end: false }); diff --git a/tools/validation/test_vanilla_registry.js b/tools/validation/test_vanilla_registry.js new file mode 100644 index 000000000..e797f8791 --- /dev/null +++ b/tools/validation/test_vanilla_registry.js @@ -0,0 +1,19 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); +const { isRetiredVanillaMemorySnapshot } = require('./vanilla_registry'); + +test('accepts the vanilla AI flag removed in 26.3', () => { + assert.equal(isRetiredVanillaMemorySnapshot('minecraft:memory_module_type', ['minecraft:is_tempted']), true); +}); + +test('rejects unknown or modded memories', () => { + for (const name of ['minecraft:unknown', 'basemetals:is_tempted']) { + assert.equal(isRetiredVanillaMemorySnapshot('minecraft:memory_module_type', + ['minecraft:is_tempted', name]), false); + } +}); + +test('does not excuse another registry or an empty report', () => { + assert.equal(isRetiredVanillaMemorySnapshot('minecraft:block', ['minecraft:is_tempted']), false); + assert.equal(isRetiredVanillaMemorySnapshot('minecraft:memory_module_type', []), false); +}); diff --git a/tools/validation/test_world_terrain.py b/tools/validation/test_world_terrain.py index df8f16856..ae6b3f385 100644 --- a/tools/validation/test_world_terrain.py +++ b/tools/validation/test_world_terrain.py @@ -12,6 +12,18 @@ def test_native_118_block_states(self): section = {"block_states": {"palette": [{"Name": "minecraft:deepslate"}]}} self.assertEqual(["minecraft:deepslate"] * 4096, list(terrain.block_names(section))) + def test_native_263_block_state_objects(self): + section = {"block_states": {"palette": [{"id": "minecraft:deepslate"}]}} + self.assertEqual(["minecraft:deepslate"] * 4096, list(terrain.block_names(section))) + + def test_native_263_compact_block_states(self): + section = {"block_states": {"palette": ["minecraft:stone"]}} + self.assertEqual(["minecraft:stone"] * 4096, list(terrain.block_names(section))) + + def test_native_263_mixed_palette_wrappers(self): + section = {"block_states": {"palette": [{"": "minecraft:stone"}]}} + self.assertEqual(["minecraft:stone"] * 4096, list(terrain.block_names(section))) + def test_contiguous_and_padded_palettes(self): for palette_size in (16, 17, 33, 65): bits = max(4, (palette_size - 1).bit_length()) @@ -41,6 +53,25 @@ def gravel(x=0.5, y=35, z=0.5, dimension=""): class FallingTerrainTest(unittest.TestCase): + def test_falling_blocks_accept_old_and_native_state_encodings(self): + for state in ({'Name': 'minecraft:gravel'}, {'id': 'minecraft:gravel'}, + 'minecraft:gravel', {'': 'minecraft:gravel'}): + entity = {'id': 'minecraft:falling_block', 'BlockState': state, 'Pos': [0.5, 35, 0.5]} + + with self.subTest(state=state), patch.object(terrain, 'dimensions', return_value=[]): + actual = terrain.falling_blocks(Path('unused'), {('', 0, 0): {'Entities': [entity]}}) + + self.assertEqual({('', 0, 0, 0): [(35, 'minecraft:gravel')]}, actual) + + def test_dropped_falling_blocks_keep_the_native_item_count(self): + entity = {'id': 'minecraft:item', 'Item': {'id': 'minecraft:sand', 'count': 2}, 'Pos': [0.5, 35, 0.5]} + dropped = [] + + with patch.object(terrain, 'dimensions', return_value=[]): + terrain.falling_blocks(Path('unused'), {('', 0, 0): {'Entities': [entity]}}, dropped) + + self.assertEqual(2, dropped[0]['count']) + def test_exposed_dirt_beside_old_grass_can_grow_normally(self): self.compare_surface("minecraft:dirt", "minecraft:grass_block", nearby_grass=True) diff --git a/tools/validation/vanilla_registry.js b/tools/validation/vanilla_registry.js new file mode 100644 index 000000000..d3f4b6973 --- /dev/null +++ b/tools/validation/vanilla_registry.js @@ -0,0 +1,10 @@ +'use strict'; + +// Minecraft 26.3 no longer registers this AI flag. +function isRetiredVanillaMemorySnapshot(registry, entries) { + return registry === 'minecraft:memory_module_type' + && entries.length > 0 + && entries.every(entry => entry === 'minecraft:is_tempted'); +} + +module.exports = { isRetiredVanillaMemorySnapshot };