diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 912c9ac..d4548a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,16 @@ -name: Power Advantage 1.13 CI +name: Power Advantage 1.14 CI on: push: - branches: [master-1.13.2, 'feature/**'] + branches: [master-1.14.4, 'feature/**'] pull_request: - branches: [master-1.13.2] + branches: [master-1.14.4] permissions: contents: read concurrency: - group: poweradvantage-1.13-${{ github.workflow }}-${{ github.ref }} + group: poweradvantage-1.14-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -25,16 +25,16 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/OreSpawn - ref: 6a7c2bb07b9a7771128812fcc07d185f7d9df58b + ref: 9c2488cba02d317e3f31e24441dcc58273d8d0e6 path: .ci-dependencies/OreSpawn - name: Use canonical LF dependency inputs - # Base Metals predates archive text normalization; its LF artifact is separately pinned. + # Use the same source bytes on hosted and local dependency builds. run: git config --global core.autocrlf false - name: Check out pinned Base Metals uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/BaseMetals - ref: 1376a20d7d5ba22d3abc77dc34b49a2c88943697 + ref: 713802f93f2d1f650e1593fa35c487ea43add4a5 path: .ci-dependencies/BaseMetals - name: Install Java 8 toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 @@ -58,8 +58,8 @@ jobs: chmod +x "$orespawn/gradlew" "$basemetals/gradlew" (cd "$orespawn" && ./gradlew jar --no-daemon --stacktrace) (cd "$basemetals" && ./gradlew jar --no-daemon --stacktrace) - osjar="$orespawn/build/libs-dev/OreSpawn-4.1.0.113021-deobf.jar" - bmjar="$basemetals/build/libs-dev/BaseMetals-3.0.1.113021-deobf.jar" + osjar="$orespawn/build/libs-dev/OreSpawn-4.1.1.114041-deobf.jar" + bmjar="$basemetals/build/libs-dev/BaseMetals-3.0.1.114041-deobf.jar" test -f "$osjar" && test -f "$bmjar" echo "orespawn=$osjar" >> "$GITHUB_OUTPUT" echo "basemetals=$bmjar" >> "$GITHUB_OUTPUT" @@ -73,20 +73,20 @@ jobs: - name: Upload audited release candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: PowerAdvantage-1.13.2-${{ github.sha }} + name: PowerAdvantage-1.14.4-${{ github.sha }} if-no-files-found: error retention-days: 30 path: | - build/libs/PowerAdvantage-2.4.4.113021.jar - build/libs/PowerAdvantage-2.4.4.113021-sources.jar - build/libs/PowerAdvantage-2.4.4.113021-javadoc.jar + build/libs/PowerAdvantage-2.4.5.114041.jar + build/libs/PowerAdvantage-2.4.5.114041-sources.jar + build/libs/PowerAdvantage-2.4.5.114041-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt - name: Upload diagnostics on failure if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: PowerAdvantage-1.13.2-diagnostics-${{ github.sha }} + name: PowerAdvantage-1.14.4-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 f403b0c..c8493f6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,9 +2,9 @@ name: CodeQL on: push: - branches: [master-1.13.2, 'feature/**'] + branches: [master-1.14.4, 'feature/**'] pull_request: - branches: [master-1.13.2] + branches: [master-1.14.4] schedule: - cron: '47 7 * * 4' @@ -25,16 +25,16 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/OreSpawn - ref: 6a7c2bb07b9a7771128812fcc07d185f7d9df58b + ref: 9c2488cba02d317e3f31e24441dcc58273d8d0e6 path: .ci-dependencies/OreSpawn - name: Use canonical LF dependency inputs - # Base Metals predates archive text normalization; its LF artifact is separately pinned. + # Use the same source bytes on hosted and local dependency builds. run: git config --global core.autocrlf false - name: Check out pinned Base Metals uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: MinecraftModDevelopmentMods/BaseMetals - ref: 1376a20d7d5ba22d3abc77dc34b49a2c88943697 + ref: 713802f93f2d1f650e1593fa35c487ea43add4a5 path: .ci-dependencies/BaseMetals - name: Install Java 8 toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 @@ -58,8 +58,8 @@ jobs: chmod +x "$orespawn/gradlew" "$basemetals/gradlew" (cd "$orespawn" && ./gradlew jar --no-daemon --stacktrace) (cd "$basemetals" && ./gradlew jar --no-daemon --stacktrace) - echo "orespawn=$orespawn/build/libs-dev/OreSpawn-4.1.0.113021-deobf.jar" >> "$GITHUB_OUTPUT" - echo "basemetals=$basemetals/build/libs-dev/BaseMetals-3.0.1.113021-deobf.jar" >> "$GITHUB_OUTPUT" + echo "orespawn=$orespawn/build/libs-dev/OreSpawn-4.1.1.114041-deobf.jar" >> "$GITHUB_OUTPUT" + echo "basemetals=$basemetals/build/libs-dev/BaseMetals-3.0.1.114041-deobf.jar" >> "$GITHUB_OUTPUT" - name: Initialize CodeQL uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 with: diff --git a/.github/workflows/validate-gradle-build.yml b/.github/workflows/validate-gradle-build.yml index d4d55ba..c471b09 100644 --- a/.github/workflows/validate-gradle-build.yml +++ b/.github/workflows/validate-gradle-build.yml @@ -3,11 +3,11 @@ name: Validate Gradle Wrapper on: push: branches: - - master-1.13.2 + - master-1.14.4 - 'feature/**' pull_request: branches: - - master-1.13.2 + - master-1.14.4 permissions: contents: read diff --git a/.gitignore b/.gitignore index f5eaedc..03f14c4 100644 --- a/.gitignore +++ b/.gitignore @@ -64,7 +64,7 @@ /test-rig/migration/.venv/ /test-rig/migration/__pycache__/ -# Local dependency artifacts; 1.13 converters use Forge Energy instead. +# Local dependency artifacts; converters use Forge Energy instead. /libs/* *.jar *.zip diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 256e545..59e968a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,15 @@ +Power Advantage 2.4.5.114041 (Minecraft 1.14.4) + +- Move to Minecraft 1.14.4 and Forge 28.2.26, retaining existing registry names, saved-state keys and public API packages. +- Use native Forge fluid rendering and sided capabilities, registered machine menus, tag updates and parts-loot events. +- Keep the pre-flattening world migration hook and handle the retired vanilla villager profession index without discarding custom entries. +- Add native block loot, including portable tank contents, and invalidate machine capabilities when their tiles are removed. +- Keep water in tanks when reading saves that use its old unqualified fluid name. +- Keep incoming Forge Energy when converting it to steam, electricity or quantum power; the RF- and Tech Reborn-labelled converters no longer discard it. +- Register the world-save hook after parallel mod loading to avoid intermittent autosave failures when Base Metals also registers a hook. +- Carry forward the restart and moving-drill fixes. The 1.14 validation status is tracked separately from the earlier 1.13 results. +- Manual visuals and recipe unlocks are accepted. Machine functionality is covered by automated tests, not personal play-testing; the 1.14 qualification notes retain the exact tested jars and remaining coverage limits. + Power Advantage 2.4.4.113021 - Exclude empty directories so development jars match between existing workspaces and fresh CI checkouts. diff --git a/README.md b/README.md index a59588c..71f188a 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,29 @@ # PowerAdvantage -Power Advantage provides shared power, fluid, item transport, and compatibility APIs for its Minecraft 1.13.2 expansion mods. +**Minecraft 1.14.4** -This port keeps existing registry names and includes migration from 1.10 and 1.12. +This port has passed the automated machine/migration tests and a manual check of +visuals and recipe unlocks. See the [qualification report](docs/1.14/BROADER-QUALIFICATION.md) +for coverage and limits. Use disposable copies for upgrades. + +Power Advantage provides shared power, fluids, item transport and compatibility APIs for Steam Advantage and Electric Advantage. + +This port keeps existing registry names and carries forward migration from 1.10 and 1.12. Always back up a world before upgrading, and check that its other mods also support the target version. -The [qualification notes](docs/1.13/CURRENT-QUALIFICATION.md) separate tested scenarios from remaining gaps; +The archived [1.13 qualification notes](docs/1.13/CURRENT-QUALIFICATION.md) separate its tested scenarios from remaining gaps; they are not a guarantee that an entire modpack can be upgraded without losses. +The [1.14 qualification report](docs/1.14/BROADER-QUALIFICATION.md) records the +repeated checks, exact tested jars and remaining limits for this candidate. Recipe-book discovery now follows relevant ingredients. See the [all-Advantage material checklist and automated discovery tests](docs/1.13/RECIPE-DISCOVERY.md). To regenerate the checked-in unlock JSON after changing recipes, run `node tools/generate-recipe-unlocks.js`; `check` rejects missing or stale ingredient criteria. The [manufacturing progression audit](docs/1.13/MANUFACTURING-PROGRESSION.md) traces survival parts from raw resources, including configured material aliases, processing, loot-only parts and automated restart checks. -## Building Minecraft 1.13.2 +## Building Minecraft 1.14.4 -The `master-1.13.2` build uses ForgeGradle 7.0.34 and Gradle 9.6.1. Run Gradle with Java 17; Gradle resolves the Java 8 production toolchain. +The `master-1.14.4` build uses Forge 28.2.26, ForgeGradle 7.0.34 and Gradle 9.6.1. Run Gradle with Java 17; Gradle resolves the Java 8 production toolchain. -Build the pinned Base Metals `3.0.1.113021` and OreSpawn `4.1.0.113021` deobf jars in their sibling checkouts, then run: +Build the pinned Base Metals `3.0.1.114041` and OreSpawn `4.1.1.114041` deobf jars in their sibling checkouts, then run: ```text gradlew.bat clean check build javadoc verifyReleaseDependencies verifyReleaseArtifacts writeReleaseChecksums verifyEclipseProductionClasspath @@ -23,12 +31,9 @@ gradlew.bat clean check build javadoc verifyReleaseDependencies verifyReleaseArt The default dependency paths follow the local MMD checkout layout. Other layouts can pass `-PbaseMetalsDeobfJar=` and `-PoreSpawnDeobfJar=`. Dependencies are present on development classpaths but are never embedded in PowerAdvantage artifacts. -The pinned Base Metals development jar predates archive line-ending normalization. -Both the original Windows-input jar and a clean LF-checkout build have exact -checksums in `gradle.properties`. Their class bytes and archive entries match; -only text line endings differ. CI uses the LF build. Unknown hashes still fail. -Advantage's own archives normalize text to LF. -All Advantage archives also exclude empty directories left behind by older source layouts. +Dependency checksums and source commits are recorded in `gradle.properties`. +Unknown artifacts fail the dependency audit. Archives use reproducible ordering, +LF text and no empty directories, so fresh checkouts produce the same bytes. Release jars are written to `build/libs`; `deobfJar` writes the sibling-development jar to `build/libs-dev`. @@ -42,16 +47,21 @@ Refresh the project in Eclipse, allow its Java build to finish, and launch the g Eclipse copies production resources from `build/resources/main`, not directly from `src/main/resources`. This prevents its builder from overwriting the expanded `mods.toml` with raw `${version}` placeholders. After updating this setup, use **Gradle > Refresh Gradle Project**, then **Project > Clean** before relaunching. After editing resources or version properties, run `processResources` or refresh the Gradle project before the Eclipse build. See the [metadata regression notes](docs/1.13/ECLIPSE-METADATA.md). -To test all three Advantage mods together, run `gradlew.bat prepareCombinedEclipseLaunch`, refresh Eclipse, and launch `runClientAllAdvantage.launch`. This additional profile loads the pinned Steam/Electric deobf jars, Base Metals and OreSpawn alongside Power's Eclipse output. It uses the same `run` directory and test saves; it does not copy, open or modify a save automatically. The ordinary `runClient.launch` remains Power-only. Alternative checkout layouts can pass `-PsteamAdvantageDeobfJar=` and `-PelectricAdvantageDeobfJar=`; supplied jars must match the `test-rig/profiles/dev-core.json` pins. Regenerate the combined launch after refreshing its dependencies or the base launch. +To test all three Advantage mods together, run `gradlew.bat prepareCombinedEclipseLaunch`, refresh Eclipse, and launch `runClientAllAdvantage.launch`. This additional profile loads the pinned Steam/Electric deobf jars, Base Metals and OreSpawn alongside Power's Eclipse output. It uses the same `run` directory and test saves; it does not copy, open or modify a save automatically. The ordinary `runClient.launch` remains Power-only. Alternative checkout layouts can pass `-PsteamAdvantageDeobfJar=` and `-PelectricAdvantageDeobfJar=`; supplied jars must match the `test-rig/profiles/native114/dev-core.json` pins. Regenerate the combined launch after refreshing its dependencies or the base launch. -The combined [client resource probe](test-rig/client-resources/README.md), with `-RequireExtensions`, checks item names, baked models, textures and every valid block state across all three Advantage mods. The legacy Steam/Electric model-path and selector failures are repaired; [the qualification notes](docs/1.13/OIL-TRANSFER.md#combined-resource-repair-21-october-2026) retain the failing evidence and subsequent full-scope result. Native resource checks remain separate from manual graphical/gameplay acceptance. +The combined [client resource probe](test-rig/client-resources/README.md) checks native baked models, textures, creative entries, translated item names, fluid rendering and tank gauges without opening a save. Static resource and locale regressions also run under `check`. Automated recipe-book fixtures and the accepted manual visual/recipe-unlock checks have separate scopes; see the [current port status](docs/1.14/PORT-STATUS.md). -The [client resource probe](test-rig/client-resources/README.md) checks native baked models, textures, creative entries, and translated item names without opening a world. Static resource and locale regressions also run under `check`. +The [broader 1.14 test results and play-test checklist](docs/1.14/BROADER-QUALIFICATION.md) separate migration, connected machinery, endurance and manual checks. Test worlds and reports stay local; use disposable copies when trying an old save. ## Compatibility The implementation namespace is `zone.moddev.mc.poweradvantage`. The historical `cyano.poweradvantage.api` surface, including `FluidNetworkApi`, remains available to dependent mods. Registry names, fluids, tile IDs, NBT keys, OreSpawn provider IDs, and configuration meaning remain stable. +Forge 1.14 handles oil rendering, fluid containers, registered menus, tag updates +and parts-loot events directly. Advantage's public fluid contract is bridged to +native sided fluid capabilities; its millibucket units and saved tank keys remain +unchanged. The remaining coremod runs before vanilla's legacy-world data fixes. + PowerAdvantage owns the shared pre-flattening migration hook used by all three Advantage mods. See [the 1.13 migration guide](docs/1.13/MIGRATION.md) before opening an irreplaceable 1.10 or 1.12 world. The [Advantage Works rig](test-rig/README.md) provides repeatable machine, restart, world-generation, and migration checks. The [endurance protocol](docs/1.13/ENDURANCE.md) exercises repeat-batch operation in fresh and migrated disposable worlds, followed by a complete server-process restart. Its coverage limits are separate from frozen-state migration and manual client acceptance. @@ -60,7 +70,7 @@ The [chunk and moving-drill protocol](docs/1.13/CHUNK-AND-DRILLS.md) adds genuin The [partial-network and extended-drill protocol](docs/1.13/PARTIAL-NETWORKS-AND-EXTENDED-DRILLS.md) separates endpoint unloading/recovery from whole-network reloads, and covers repeated moves, vertical track travel, junctions, blocked output and redstone interruption. -The [player and entity migration report](docs/1.13/PLAYER-AND-ENTITY-MIGRATION.md) covers player files, inventories, nested items, ownership, saved entities and full restarts. The eight tested routes pass with the repaired 1.12 bridge. Use Electric Advantage `2.2.3.112021` for the current turbine and drill fixes; the original `2.2.1.112021` jar remains a failing historical control. +The archived [player and entity migration report](docs/1.13/PLAYER-AND-ENTITY-MIGRATION.md) covers player files, inventories, nested items, ownership, saved entities and full restarts on 1.13. Its eight routes passed with the repaired 1.12 bridge. Use Electric Advantage `2.2.3.112021` for that bridge's turbine and drill fixes; the original `2.2.1.112021` jar remains a failing historical control. The [additional-dimension protocol](docs/1.13/DIMENSIONS-AND-ENTITY-LIFECYCLE.md) covers Nether/End player and entity fixtures, native passenger/leash relationships diff --git a/build.gradle b/build.gradle index 3c74089..13acf67 100644 --- a/build.gradle +++ b/build.gradle @@ -70,9 +70,9 @@ tasks.withType(Javadoc).configureEach { } tasks.withType(AbstractArchiveTask).configureEach { - includeEmptyDirs = false preserveFileTimestamps = false reproducibleFileOrder = true + includeEmptyDirs = false } def archiveTextSuffixes = [ @@ -94,7 +94,7 @@ minecraft { def forgeRunModClassesDirectory = layout.buildDirectory.dir('forge-run-mod-classes/main') def forgeMainOutputPath = forgeRunModClassesDirectory.get().asFile.absolutePath def mainModClasses = "${mod_id}%%${forgeMainOutputPath}${File.pathSeparator}${mod_id}%%${forgeMainOutputPath}" - def forge25RunTargets = [client: 'fmluserdevclient', server: 'fmluserdevserver', data: 'fmluserdevdata'] + def forge28RunTargets = [client: 'fmluserdevclient', server: 'fmluserdevserver', data: 'fmluserdevdata'] runs { configureEach { mainClass = 'net.minecraftforge.userdev.LaunchTesting' @@ -102,7 +102,7 @@ minecraft { systemProperty 'forge.logging.markers', 'REGISTRIES' systemProperty 'forge.logging.console.level', 'info' mods { poweradvantage { source sourceSets.main } } - environment 'target', forge25RunTargets[name] + environment 'target', forge28RunTargets[name] environment 'MCP_MAPPINGS', "${mapping_channel}_${mapping_version}" environment 'MCP_VERSION', mcp_version environment 'FORGE_VERSION', forge_version @@ -130,7 +130,7 @@ sourceSets.main.resources { def forgeRunModClassesDirectory = layout.buildDirectory.dir('forge-run-mod-classes/main') def prepareForgeRunModClasses = tasks.register('prepareForgeRunModClasses', Sync) { group = 'ide' - description = 'Merges classes and processed resources for Forge 1.13 launches.' + description = 'Merges classes and processed resources for Forge 1.14 launches.' dependsOn tasks.named('classes') from sourceSets.main.output into forgeRunModClassesDirectory @@ -165,11 +165,11 @@ repositories { // The default paths mirror the local MMD checkout layout without tying the // build to a drive letter. CI and other layouts can override either path. def baseMetalsDeobfJar = file(providers.gradleProperty('baseMetalsDeobfJar') - .getOrElse('../../../BaseMetals/BaseMetals 113/BaseMetals/build/libs-dev/' - + 'BaseMetals-3.0.1.113021-deobf.jar')) + .getOrElse('../../../BaseMetals/BaseMetals 114/BaseMetals/build/libs-dev/' + + 'BaseMetals-3.0.1.114041-deobf.jar')) def oreSpawnDeobfJar = file(providers.gradleProperty('oreSpawnDeobfJar') - .getOrElse('../../../MinecraftMineralogy/OS113/OreSpawn/build/libs-dev/' - + 'OreSpawn-4.1.0.113021-deobf.jar')) + .getOrElse('../../../MinecraftMineralogy/OS114/OreSpawn/build/libs-dev/' + + 'OreSpawn-4.1.1.114041-deobf.jar')) dependencies { implementation minecraft.dependency( @@ -314,13 +314,14 @@ tasks.register('verifyReleaseConfiguration') { group = 'verification' description = 'Validates release identity, target metadata, and optional integration policy.' doLast { - if (project.mod_version != '2.4.4.113021') { + if (project.mod_version != '2.4.5.114041') { throw new GradleException("Unexpected PowerAdvantage release version: ${project.mod_version}") } - if (project.minecraft_version != '1.13.2' - || project.forge_version != '25.0.223' + if (project.minecraft_version != '1.14.4' + || project.forge_version != '28.2.26' || project.mapping_channel != 'snapshot' - || project.mapping_version != '20180921-1.13') { + || project.mapping_version != '20190719-1.14.3' + || project.mcp_version != '20190829.143755') { throw new GradleException('Unexpected Minecraft, Forge, or mappings target') } if (project.loader_name != 'forge' || project.loader_code != '1' @@ -337,19 +338,19 @@ tasks.register('verifyReleaseConfiguration') { } if (project.cf_requirements != 'base-metals,mmd-orespawn' || project.cf_optionals != 'additional-loot-tables' - || project.cf_versions != '1.13.2') { + || project.cf_versions != '1.14.4') { throw new GradleException('Unexpected CurseForge dependency or game-version metadata') } - if (project.basemetals_version != '3.0.1.113021' - || project.orespawn_version != '4.1.0.113021' - || project.basemetals_source_commit != '1376a20d7d5ba22d3abc77dc34b49a2c88943697' - || project.orespawn_source_commit != '6a7c2bb07b9a7771128812fcc07d185f7d9df58b') { + if (project.basemetals_version != '3.0.1.114041' + || project.orespawn_version != '4.1.1.114041' + || project.basemetals_source_commit != '713802f93f2d1f650e1593fa35c487ea43add4a5' + || project.orespawn_source_commit != '9c2488cba02d317e3f31e24441dcc58273d8d0e6') { throw new GradleException('Unexpected Base Metals or OreSpawn dependency identity') } String mainSource = file( 'src/main/java/zone/moddev/mc/poweradvantage/PowerAdvantage.java').getText('UTF-8') if (!mainSource.contains('MODID = "poweradvantage"') - || !mainSource.contains('VERSION = "2.4.4.113021"') + || !mainSource.contains('VERSION = "2.4.5.114041"') || !mainSource.contains('@Mod(PowerAdvantage.MODID)')) { throw new GradleException('PowerAdvantage source identity changed') } @@ -430,9 +431,6 @@ tasks.register('verifyReleaseDependencies') { String actualHash = MessageDigest.getInstance('SHA-256') .digest(dependencyJar.bytes).encodeHex().toString().toUpperCase() Set acceptedHashes = [expectedHash] as Set - if (dependencyJar == baseMetalsDeobfJar) { - acceptedHashes.add(project.basemetals_deobf_lf_sha256.toString()) - } if (!acceptedHashes.contains(actualHash)) { throw new GradleException("${dependencyJar.name} SHA-256 is ${actualHash}, expected ${acceptedHashes}") } @@ -1134,9 +1132,9 @@ tasks.register('clientResourceProbeJar', Jar) { // these deobf jars are runtime inputs, never release contents or compile dependencies. def advantageExtensionJars = [ steamadvantage: file(providers.gradleProperty('steamAdvantageDeobfJar').getOrElse( - '../SteamAdvantage/SteamAdvantage/build/libs-dev/SteamAdvantage-2.2.5.113021-deobf.jar')), + '../SteamAdvantage/SteamAdvantage/build/libs-dev/SteamAdvantage-2.2.6.114041-deobf.jar')), electricadvantage: file(providers.gradleProperty('electricAdvantageDeobfJar').getOrElse( - '../ElectricAdvantage/ElectricAdvantage/build/libs-dev/ElectricAdvantage-2.2.5.113021-deobf.jar')) + '../ElectricAdvantage/ElectricAdvantage/build/libs-dev/ElectricAdvantage-2.2.6.114041-deobf.jar')) ] tasks.register('prepareCombinedEclipseLaunch') { @@ -1144,7 +1142,7 @@ tasks.register('prepareCombinedEclipseLaunch') { description = 'Creates an additional production-only Eclipse client launch with all three Advantage mods.' dependsOn tasks.named('verifyEclipseProductionClasspath') doLast { - def profile = new JsonSlurper().parse(file('test-rig/profiles/dev-core.json')) + def profile = new JsonSlurper().parse(file('test-rig/profiles/native114/dev-core.json')) advantageExtensionJars.each { String id, File jar -> def pin = profile.mods.find { it.id == id } if (!jar.isFile()) throw new GradleException("Build ${id} first, or override its *AdvantageDeobfJar property: ${jar}") diff --git a/docs/1.14/BROAD-WORKS.md b/docs/1.14/BROAD-WORKS.md new file mode 100644 index 0000000..88c3717 --- /dev/null +++ b/docs/1.14/BROAD-WORKS.md @@ -0,0 +1,180 @@ +# Walkable Works Checks + +The current manual-testing candidate is `i`; see the hook-safe rerun below. +The earlier `h` results are retained as history, and its viewer is now guarded +against startup because of the subsequently confirmed persistence-hook race. + +## What This Checks + +The broad sweep operates all 21 handbook bays in a new disposable native 1.14.4 +world, stops the server process, and checks the same world after another boot. +It is a useful commissioning check, not proof that every machine behaviour or +every item in the handbook has been exercised. + +The original station definitions remain unchanged. The old failed broad runs +remain available as evidence; their verdicts are not edited. + +## Drill Assertion Repair + +`Q-RIG-001` was an assertion defect: two connected drills can move into their +declared track cells, but the old assertions still demanded a drill at the +original coordinate. Simply removing those checks would hide lost drills. + +The repaired server assertion accepts exactly one correctly facing drill in +either its original cell or its one declared rear-track cell. It rejects a +missing drill, two drills in those cells, a wrong facing and movement to an +undeclared cell. It does not search arbitrarily for something that will pass. + +The separate `migration/native114_broad.py` runner adds stricter phase checks: + +- Before activation, all twelve drills must be at their exact original cells, + and every declared rear track must still be present. +- Commissioning gives each drill a distinct persistent tile marker and seven + tagged diamonds. These are finite witnesses, not a refill during testing. +- After operation, all twelve tile identities, facings, coordinates and finite + inventory totals must match. Stock may move to the existing rear chests, but + it may not disappear or duplicate. +- The Steam down-facing and Electric up-facing reproduction lanes must actually + move. A run in which everything stays still does not clear `Q-RIG-001`. +- The old cell must no longer contain a second copy of the drill. Its temporary + conduit is not a stable post-operation witness: a rearward-moving Steam drill + can subsequently drill that cell and put a helper bit there. +- The second process performs no build, reset, start, data merge, replacement + or refill. Position, persistent identity, facing, stock and energy bits must + match the stopped first-process state exactly. + +Every original assertion is retained. Missing optional integrations are explicit +skips, not fabricated passes. The runner verifies actual loaded mod identities +and versions against its frozen manifest; the decoupled run excludes Base Metals +and Mineralogy but retains the required OreSpawn dependency. + +## Running It + +Use Java 8 for the packaged server, Python 3.11 or later with the existing +`test-rig/migration/requirements.txt`, a hash-frozen native target manifest and +two free loopback ports. Run from the PowerAdvantage checkout: + +```text +python test-rig/migration/native114_broad.py --target-manifest --java8 --output test-rig/build/ --port 25588 --run-seconds 8 +``` + +The output path must be new and inside `test-rig/build`. The runner stages only +the manifest's jars, not arbitrary mods from the installed server seed. It +never opens a protected source world. Use `--profile packaged-current` or +`--profile packaged-decoupled` for a single-profile rerun. + +Fresh works use the established deterministic works seed, disabled structures, +Peaceful difficulty and no incidental mob spawning. The migration staging helper's +tile freeze is explicitly disabled for this operating campaign. The original +errors caused by omitting those settings remain recorded, not waived by the +log checker. Hostile-target manual trials need an attended proving ground and +an appropriate difficulty; they are not exercised by this quiet commissioning +configuration. + +Java negative controls cover initial and moved witnesses in all twelve cases, +duplicates, missing/wrong-facing/off-track drills and undeclared track cells. +Python negative controls also cover premature movement, stock loss/duplication, +lost tile identity, coordinate drift, restart energy loss, omitted assertions, +omitted bays and a mistakenly coupled mod set. + +## Limits + +Each bay's manual checks remain `MANUAL_REQUIRED` even when every automated +assertion passes. These include in-world GUI operation, rendering, sound, +controls, weapons and behaviours which the broad layout only represents +structurally. Dedicated bounded lifecycle, partial-network, migration and +endurance campaigns remain separate evidence. A static registry or tile check +must not be described as an operating test of the entire machine. + +Foreign Forge Energy/fluid capabilities, portable-tank break/drop state and +locked-chest automation are separate interoperability checks, not silently +covered by this sweep. The broader campaign records their results separately. + +## Local Candidate Result + +On 25 October 2026, both profiles passed on the frozen `h` candidate, with no +runtime errors and a complete server-process restart for each world: + +| Profile | Assertions Before Restart | Assertions After Restart | Drill Witnesses | +| --- | --- | --- | --- | +| `packaged-current` | 96 pass, 3 Mineralogy skips | 96 pass, 3 Mineralogy skips | 12 preserved; 2 actual track moves | +| `packaged-decoupled` | 95 pass, 4 integration skips | 95 pass, 4 integration skips | 12 preserved; 2 actual track moves | + +The extra decoupled skip is the Base Metals steel-plate integration. Each world +retains all 84 tagged inventory items and exactly the same twelve saved energy +values after restart. Two Electric buffers retain a nonzero value of 20; the +Steam buffers have naturally decayed to zero by the stopped checkpoint. Charged +Steam retention remains a separate bounded lifecycle test, not a claim made by +these zero-buffer witnesses. Every bay still reports `MANUAL_REQUIRED` for its +manual observations. + +The three Java oracle tests and fifteen Python regression tests pass. The old +fixed-position implementation fails two of the Java tests before the repair. +`Q-RIG-001` is resolved for this native broad campaign without changing the +historical station specification or removing any of its assertions. + +Local evidence, intentionally excluded from Git: + +```text +test-rig/build/native114-inputs-20261025-fixed-h-archived.json +test-rig/build/native114-broader-works-20261025-layout-fixed-h/native114-broad-results.json +test-rig/build/native114-broader-works-20261025-layout-diagnosis.json +``` + +The earlier `first`, `feedback-fixed`, `operation-fixed` and `fixed-h` run folders +are retained. They record, respectively, the feedback-text mismatch, omitted +operating permission, incorrect temporary-conduit expectation, and the generated +village/POI issue. Their failed verdicts were not rewritten. The POI diagnosis +records the real village bounds and the source file hashes. + +| Candidate Artifact | SHA-256 | +| --- | --- | +| Power `2.4.5.114041` | `15BAF794507BED84C879358CADD1E4C6F8D0A6B88179597391A675BE3315B834` | +| Steam `2.2.6.114041` | `444872113E13D806D4520CCBD9B3230C931353D43875BAE57EB9B5FC86614669` | +| Electric `2.2.6.114041` | `5BE41447AF5AA3AF60C145B3A862BC42802C620C637D1E65810B350A6A9AFBA9` | +| Test harness `1.1.0.114041` | `64BC8390743999C5C2823EDB9E8D25CC557DC87E679ED1F1104A01AA882ECDD9` | + +For that earlier run, a manual viewer was prepared under `test-rig/build/manual114-20261025`, +using those pinned inputs and an unchanged copy of the stopped passing current +world. It was not started and is now superseded and guarded. Its local README and +`manual-provenance.json` contain the start command, localhost binding, offline +operator identity and source/save fingerprint checks. No user or Eclipse save +was copied. These are local, uncommitted candidates, not published releases. + +## Hook-Safe Candidate Rerun + +The later candidate `i` defers persistence-hook registration to common setup, +avoiding the confirmed unsafe Forge collection update during parallel mod +construction. Both broad profiles were rerun from fresh worlds with the same +oracles, station definitions and full-process restart protocol. + +| Profile | Before Restart | After Restart | Drill State | +| --- | --- | --- | --- | +| `packaged-current` | 96 pass, 3 expected skips | 96 pass, 3 expected skips | 12 exact; 2 track moves | +| `packaged-decoupled` | 95 pass, 4 expected skips | 95 pass, 4 expected skips | 12 exact; 2 track moves | + +Both runs passed without runtime errors. Inventory witnesses and all saved +energy bits match after restart. All 21 manual-check lists remain open. +The earlier results and worlds were not overwritten. + +```text +Manifest: test-rig/build/native114-inputs-20261025-hook-i-archived.json +Results: test-rig/build/native114-broader-works-20261025-hook-i/native114-broad-results.json +Power release SHA-256: 33F57A1401A13597DCA53F4E49EAEDDD2D5CB3C3F406D5200902D5BA9CB41352 +Power deobf SHA-256: 8D8831D54B700FBFB2B9C43EE3D8D70D2B7E57185A4DBDF12202C47D5E387B39 +Tested harness: 64BC8390743999C5C2823EDB9E8D25CC557DC87E679ED1F1104A01AA882ECDD9 +``` + +The ten source profiles now pin the updated Power release and the actual +working harness build (`09DE1F38...`, deobf `690C49EA...`). All ten pass input +preflight, which checks files, hashes and mod identities without opening worlds. +That is not a claim that every profile was runtime-qualified with that newer +fixture harness. This broad rerun and the new viewer use the accepted frozen +`64BC8390...` harness from the unchanged `i` manifest. + +The new, not-started viewer is `test-rig/build/manual114-20261025-i`. Its README, +checked start script and provenance record identify the tested inputs and the +unchanged copied save. The previous viewer's original start script is preserved +as text, and its replacement fails immediately with a `SUPERSEDED` notice. +The old world, jars and original provenance remain unchanged. No active viewer +was found or stopped, and no production code or launch account was changed. diff --git a/docs/1.14/BROADER-QUALIFICATION.md b/docs/1.14/BROADER-QUALIFICATION.md new file mode 100644 index 0000000..28fc511 --- /dev/null +++ b/docs/1.14/BROADER-QUALIFICATION.md @@ -0,0 +1,205 @@ +# Advantage 1.14.4: broader qualification + +This records the local candidate and the checks run on 25 October 2026. Visuals +and recipe unlocks were manually checked and accepted on 26 October 2026, and +the tested candidate is accepted for merging. Machine functionality is covered +by automated tests, not personal play-testing. The remaining limits below still +apply; this is not a guarantee for an entire modpack or proof of publication. + +## Candidate + +| Mod | Version | Release SHA-256 | +| --- | --- | --- | +| Power Advantage | 2.4.5.114041 | `33F57A1401A13597DCA53F4E49EAEDDD2D5CB3C3F406D5200902D5BA9CB41352` | +| Steam Advantage | 2.2.6.114041 | `444872113E13D806D4520CCBD9B3230C931353D43875BAE57EB9B5FC86614669` | +| Electric Advantage | 2.2.6.114041 | `5BE41447AF5AA3AF60C145B3A862BC42802C620C637D1E65810B350A6A9AFBA9` | + +The disposable server inputs are frozen in +`test-rig/build/native114-inputs-20261025-hook-i-archived.json`. +That manifest pins genuine Forge 28.2.26, Base Metals 3.0.1.114041, +OreSpawn 4.1.1.114041 and the isolated test harness. Each campaign records its +actual inputs; later harness observations must not be attributed to an earlier +harness jar. These were working-tree candidates at qualification time; the hashes +identify the tested jars, not a later source commit or published release. + +## Defects found and repaired + +The broader capability test exposed an incoming converter sign error. All four +RF- and Tech Reborn-labelled converters consumed 100 Forge Energy but added +negative energy to an empty Advantage buffer, leaving nothing stored. Incoming +extracted energy is now added positively. Both conversion directions preserve +their configured equivalence; neither conversion rates nor capacities changed. + +The packaged failure, failing regression test, corrected test and packaged rerun +are retained. A bytewise archive comparison finds only +`cyano/poweradvantage/api/modsupport/TileEntityConverter.class` changed between +the earlier and corrected Power release jars. Registry names and saved data did +not change. This repair is confined to the 1.14 candidate; it is not a claim that +another Minecraft branch has been repaired. + +The early extended run also crashed while saving the world. Forge's persistence +hooks live in a plain `ArrayList`; Power and Base Metals both added hooks during +parallel mod construction. An isolated two-thread control using the actual +Forge API produced null or lost entries, while its sequential control passed. +That establishes the unsafe mechanism, not the exact interleaving of the crashed +game process. Power now registers through `DeferredWorkQueue.runLater` during +common setup, after parallel construction. A regression fails against the old +constructor and passes with the queued registration. Only `PowerAdvantage.class` +differs between the converter-fix jar and this hook-fix jar. + +The `h` candidate's earlier passes remain recorded, but the table below does not +attribute them to the newer `i` artifact. All final results below are recorded +against `i`. + +## Checks + +| Check | Current result | What it establishes | +| --- | --- | --- | +| Native capability and portable-item witnesses | PASS | Twenty server-side checks and two full-restart checks | +| Connected legacy assemblies | PASS, final i candidate | Five sources; initial preservation, 512 ticks, full restart and another 512 ticks; no rebuilding or refill | +| Frozen migration matrix | PASS, final i candidate | Thirteen original/maintained and direct/chained routes, full restart and idempotence | +| Partial-network and extended drills | PASS, final i candidate | Real unload/load events, both load orders, pipe-only bridge interruption, eighteen drill routes, persisted work and redstone | +| Players, entities, dimensions and respawn | PASS, final i candidate | Eight Overworld routes, sixteen Nether/End routes, six nested-storage layouts and 72 server-side respawn cases | +| Live migration-transformer ordering | Final i semantic PASS | Six runtimes, observed/reversed real Base/Power transformers, full restart and evidence replay; backup caveat below | +| Walkable current and decoupled Works | PASS, final i candidate | Initial equipment, twelve tagged drill witnesses, bounded movement and restart; human observations remain manual | +| Measured endurance | PASS, final i candidate | Two thirty-minute runs, followed by full restart and resumed production; independent raw-checkpoint replay PASS | +| Power release and Eclipse audits | PASS | Full task suite and actual Eclipse batch compiler | +| Independent Power build | PASS | Uncached source export in a path with spaces; all four jars match | +| Combined client resource probe | PASS, final i candidate | 94 items, 905 block states, 87 creative entries, fluids, gauge and recipe-book rendering | + +The capability witnesses cover receive/extract simulation, negative requests, +capacity limits, both converter directions, invalidated/replaced capabilities, +fluid identity and conservation, native portable-tank loot and placement, and +locked/unlocked chest automation. The tank retains exactly 500 mB after replacing +its native dropped item and restarting the entire process. The conveyor retains +exactly sixteen delivered dirt blocks. + +The decoupled Works profile excludes Base Metals and Mineralogy; it retains +OreSpawn, a required dependency of the current port. It does not establish that +the port starts without OreSpawn. Its extra integration skip is the Base Metals +steel-plate check. The ordinary profile has 96 passing assertions and three +Mineralogy skips; the decoupled profile has 95 passes and four integration skips, +both before and after restart. Manual visual/recipe acceptance does not sign off +all 21 itemized human observation lists or establish personal machine play-tests. + +Transformer-order testing uses an audited, test-only coremod observer library to +observe and reverse the real transformers, retaining the production mod jars +unchanged. Blocks, items, actors and restart state agree across the orders. +Original-byte custody can belong to different per-mod backup files depending on +which hook ran first. The original bytes survive in the combined backup set; +keep that entire set, and make a whole-world backup before any upgrade. +Real client login, death-screen interaction, portal travel, mounted-player +travel and arbitrary third-party entities are not covered by those server tests. + +The endurance protocol feeds native inventory commands and fluid handlers, not +tile NBT. Each minute reconciles five processing lanes, plain and filtered +conveyors and 2:1 oil distillation. Closed tanks, geothermal heat, stored battery +items and valid energy ranges are checked alongside throughput. The migrated +copy receives rebuilt Works bays; that is not an endurance claim for every +original machine in its source save. + +Both environments completed 30 measured batches over 1,801.56 seconds, followed +by three restart-related checkpoints. The fresh world advanced 36,833 game +ticks and the migrated copy 36,033. Each produced 120 items in each of the five +processing lanes, routed 480 items in each of the four conveyor/filter lanes, +and distilled 60,000 mB crude oil into exactly 30,000 mB refined oil. Stored +water, battery items and heat remained within their stated invariants. The +read-only replay reconstructs all 33 checkpoints per environment from raw +SNBT, and independently rechecks inventory-command acknowledgements, jar pins, +protocol hashes and the protected source fingerprint. + +Final regression checks pass: 60 Power, 27 Steam and 34 Electric unit tests, +46 isolated-harness Java tests, 342 Python tests and 15 launcher/preflight +checks. Power's complete release suite and the child compilation, assembly and +Eclipse preparation checks pass; the children retain the publication source-pin +gate described below. + +The partial-network fixtures account for Forge's separate FULL availability +and holder-removal distances. Anchors are 32 chunks apart and release their +thirteen-chunk support halos. Original drill geometry, sustained absence, +actual callbacks, object identity and conservation oracles remain intact. +The final trace records 108 genuine unloads and 108 traced reloads across both +worlds, with no acquisition stack from Power's network implementation. See +[the partial-network report](PARTIAL-NETWORKS-AND-EXTENDED-DRILLS.md) and +[the broad bay report](BROAD-WORKS.md) for limits and retained failures. + +## Evidence and repeatability + +Local reports, saves, console logs, screenshots and pinned jars remain below +`test-rig/build/` and `build/client-resource-runs/`, excluded from Git. Earlier +failures remain separate from successful reruns. Test-fixture mistakes are not +recorded as production fixes: the portable placement probe initially supplied a +null player, and the frozen refresh initially rejected two byte-identical +snapshots with the same approved hash. + +Final evidence roots, deliberately untracked: + +```text +test-rig/build/native114-broader-frozen-20261025-i +test-rig/build/native114-broader-connected-20261025-i +test-rig/build/native114-broader-endurance-20261025-full-i +test-rig/build/native114-broader-interop-20261025-green-i +test-rig/build/native114-broader-works-20261025-hook-i +test-rig/build/native114-broader-partial-20261025-*-i1 +test-rig/build/native114-broader-actors-20261025-i +test-rig/build/native114-broader-dimensions-*-20261025-i +test-rig/build/native114-broader-player-storage-20261025-i +test-rig/build/native114-broader-respawn-*-20261025-i +test-rig/build/native114-broader-order-20261025-i +build/client-resource-runs/20261025-075733-909c9110 +``` + +The new runners reuse the existing state and stock oracles. Native adapters +handle actual asynchronous chunk readiness and target-platform field names; +they may not fabricate events, extend an oracle's budget, or wait for a desired +machine result before selecting an observation. No protected source fixture or +original Sylvester world is launched or changed. + +## Manual inspection + +Visuals and recipe unlocks have been manually accepted. That acceptance is +separate from the automated machine, migration and endurance results above; +machine functionality was not personally tested. It does not close every item +in the original play-test checklist. + +Use `runClientAllAdvantage.launch` in the PA114 Power project for the coordinated +client. Refresh the Gradle project and let Eclipse rebuild before launching. +The ordinary `runClient.launch` intentionally remains Power-only. + +The separate disposable viewer was prepared under +`test-rig/build/manual114-20261025-i/`. Its `Start-Manual.ps1` verifies the pinned +inputs before starting on `localhost:25565`; its README covers controls and +operator access. Eclipse's default player name is `Dev`, so its console may need +`op Dev`. Use only this viewer: the earlier `manual114-20261025` is superseded and +refuses startup. Its original script, world, jars and provenance remain retained. + +Remaining personal play-test coverage: + +- Open the machine screens and compare pressure, power, fluid and progress + displays with actual operation, including a same-session reload, a complete + Minecraft restart and a multiplayer rejoin. +- Check moving drill bits, turbine blades and turrets during actual operation. + The accepted visuals and recipe unlocks are not a machine-function play-test. +- Try controls, rotation tools, elevators carrying a player, muskets, weapon + effects, sounds and multiplayer interaction. +- Try real third-party energy and fluid equipment. The native contract tests + do not qualify every external mod's implementation. +- Upgrade only disposable world copies. Unported unrelated mods and their + blocks, items and entities are outside this candidate's compatibility claim. + +## Before publication + +Commit, push, PR and merge are authorized for the tested candidate. Ancestry +reconciliation and publication are handled separately: preserve the port while +reconciling the final 1.13 merge, pin the exact tested Power source commit and +deobf hash in Steam and Electric, and rerun their complete dependency/release +audits. Their deliberate `UNQUALIFIED-1.14-PORT` dependency source pins remain +a provenance gate, not a failed gameplay test to bypass. This record does not +claim those steps, a release or public-download verification are complete. + +Native114 profile metadata still uses `PORTED_UNQUALIFIED` and `UNQUALIFIED`. +These are historical launch/schema guards required by `WorksRunner`, +`native114.py` and `Native114ProfileTest`, not a summary of today's acceptance. +They remain unchanged; the reports carry the actual automated PASS results, +manual acceptance and coverage limits. Working-tree source markers remain +pending until the real commit is available. diff --git a/docs/1.14/PARTIAL-NETWORKS-AND-EXTENDED-DRILLS.md b/docs/1.14/PARTIAL-NETWORKS-AND-EXTENDED-DRILLS.md new file mode 100644 index 0000000..8723f21 --- /dev/null +++ b/docs/1.14/PARTIAL-NETWORKS-AND-EXTENDED-DRILLS.md @@ -0,0 +1,276 @@ +# Partial Networks And Extended Drills + +The final registration-fixed **i** candidate passes all four campaigns and +their independent audits in both worlds. Earlier failures and fixture controls +remain recorded below. See [Broader Qualification](BROADER-QUALIFICATION.md) +for the complete candidate and manual-testing handoff. + +## Candidate And Method + +This is a local 1.14.4 qualification record, not a release approval. The first +broader runs use the immutable `native114-inputs-20261023-d-archived.json` +manifest. Power, Steam and Electric release hashes are: + +| Mod | Version | SHA-256 | +| --- | --- | --- | +| Power Advantage | 2.4.5.114041 | `A0C884C3A61F1B5555F2BD5B5472D8ACBEA53EBE7291A306EBA9AFBFCCF9A011` | +| Steam Advantage | 2.2.6.114041 | `444872113E13D806D4520CCBD9B3230C931353D43875BAE57EB9B5FC86614669` | +| Electric Advantage | 2.2.6.114041 | `5BE41447AF5AA3AF60C145B3A862BC42802C620C637D1E65810B350A6A9AFBA9` | + +The native adapters reuse the historical fixture geometry, movement-cost, +inventory, fluid, retained-identity, sustained-unload and restart oracles without +editing their definitions. Canonical observations are exact byte copies of real +server snapshots selected by actual chunk callbacks and advancing world ticks, +never by whether machine NBT happens to match an expectation. + +Each campaign runs in a fresh native world and a disposable copy of the retained +original-1.10-through-1.13 migration fixture. The source runtime is fingerprinted +before and after. The copied world must first pass a separate frozen conversion +comparison before construction or operating authority is granted. + +The evidence stays in ignored `test-rig/build/` directories. The original +fixtures, original player saves and historical scripts remain unchanged. + +## First Results + +| Campaign | Evidence directory | Result | +| --- | --- | --- | +| Partial network trace | `native114-broader-partial-20261025-trace-a` | FAIL in both worlds; independent evidence audit PASS | +| Empty-bay ticket control | `native114-broader-partial-20261025-empty-control-a` | Same unload failure without constructing any Advantage network; evidence audit PASS | +| Pipe-only bridge | `native114-broader-partial-20261025-bridge-a` | FAIL in both worlds; bridge-isolation command rejected before a complete cycle | +| Extended drills and networks | `native114-broader-partial-20261025-extended-a` | FAIL; fresh autosave crash, copied-world drill stages pass but partial-unload stages fail | + +All 18 drills in each extended world completed four moves, with all 18 strict +movement and insufficient-reserve case verdicts passing. This establishes 144 +observed moves, including every facing, vertical tracks and down-first junction +selection. The copied world also passes all 18 blocked-output, post-restart +blocked-output, resumed-output and redstone cases, plus all 21 restart-state +cases. The fresh world did not reach those later stages. Neither world qualifies +partial unloading. + +## Native Fixture Limitation + +The partial-network failure is not evidence of the old unwanted network-loading +bug returning. The selected chunks never produced unload callbacks in these +runs. The same failure occurs with empty bays and no network or machine blocks. + +Native Forge 28.2.26's `TicketManager.func_219364_a` installs a level-31 forced +ticket. That ticket keeps neighboring chunks loaded. The old fixtures put both +endpoints in adjacent chunks and initially force every chunk intersecting their +bounds; their release groups do not remove all those spare bound tickets. +Removing a selected endpoint's own ticket therefore does not establish that it +can actually become absent on this platform. + +The existing harness safety limit is 32 blocks per fixture axis. A follow-up +needs a separately bounded, ticket-halo-aware native layout and explicit support +ticket controls. Keep the original fixture and failed evidence, retain the +forty-real-tick absence requirement, and still require actual unload/load +callbacks, unchanged retained tile objects, exact zero disconnected acceptance, +and normal recovery without replacement. Do not label the present runs PASS or +remove the safety guard merely to get a result. + +## Autosave Blocker + +The fresh extended process crashed on its first timed autosave, after earlier +`save-all flush` commands had reported an unexpected command error. The crash +starts in `WorldPersistenceHooks.lambda$handleWorldDataSave$0`, line 40. Its +original report is retained under the fresh runtime's `crash-reports/` folder. + +Forge stores persistence hooks in a plain static `ArrayList`. Both Power and +Base Metals register their hooks during parallel mod construction. This makes a +registration race a plausible explanation for a null hook, but the crash stack +alone does not establish the exact registration order or root cause. Production +and sibling code have not been changed by this test campaign. This is a manual +testing blocker until diagnosed, repaired if necessary, and retested against a +new pinned artifact manifest. + +An isolated control subsequently exercised the actual Forge 28.2.26 +`WorldPersistenceHooks.addHook` method, using two threads and only non-null hooks. +Across 250,000 trials it produced 258 null entries and 13 trials with lost +entries; the sequential control passed. Its source, bytecode and JSON result +are retained under `native114-broader-partial-20261025-hook-race`. The native +recompiled Forge/Minecraft classpath hash is +`E95D6915D12D46E441B20CDC9964B9DB2F75158581BDAC2BBB2DEB489EFE7B5B`. +This demonstrates the unsafe registration mechanism, not the exact interleaving +of the crashed game process. Queuing Power's registration after parallel mod +construction is a narrow repair candidate. + +## Converter-Fix Candidate Rerun + +The newer immutable manifest +`native114-inputs-20261025-fixed-h-archived.json` pins Power release +`15BAF794507BED84C879358CADD1E4C6F8D0A6B88179597391A675BE3315B834` +and harness +`64BC8390743999C5C2823EDB9E8D25CC557DC87E679ED1F1104A01AA882ECDD9`. +Steam, Electric, Base Metals and OreSpawn remain the same pinned artifacts. +The only Power archive change is `TileEntityConverter.class`; the hook +registration risk has not been repaired by that converter change. + +The trace and bridge reruns in `-trace-h` and `-bridge-h` retain the same native +ticket-control failures. The independent trace evidence audit passes while its +qualification stays FAIL. The bridge cannot provide a complete isolation cycle. +An empty-bay `-empty-control-h` repeat is retained separately. + +The separate `-extended-reads-h` run and its independent audit both PASS in fresh +and copied worlds. Each world has 72 moves, eighteen normal mining/output cases, +eighteen unfinished-work chunk-reload cases, eighteen unfinished-work full-process +restart cases, and eighteen cases for each of the legacy and changed-target +saved-read guards. The first loaded observation after a real tick is selected, +even if a later observation would be more convenient. The original source runtime +and protocol hashes match before and after. + +The full `-extended-h` campaign and independent evidence audit completed. Every +drill/output/restart/redstone group passes in both worlds; its overall +qualification stays FAIL because the partial-unload groups never obtained +genuine unload events. Existing evidence is not overwritten or relabelled to +the newer candidate. + +## Registration-Fixed Candidate + +Power release `33F57A1401A13597DCA53F4E49EAEDDD2D5CB3C3F406D5200902D5BA9CB41352` +queues `LegacyWorldDataHook.register` through `DeferredWorkQueue.runLater` in +common setup, after parallel construction. Base Metals is unchanged. The +production regression is `PersistenceRegistrationThreadTest`; the fix changes +only `PowerAdvantage.class` from the converter-fixed h archive. Its deobf SHA-256 +is `8D8831D54B700FBFB2B9C43EE3D8D70D2B7E57185A4DBDF12202C47D5E387B39`. + +The independent campaign uses the newly generated immutable +`native114-inputs-20261025-final-holder-i1-archived.json`, retaining i's production +jars and the holder-aware harness +`09DE1F386774C4C2B36AE95A4E636D35D6A2BCCAEE938BD5A05E6EE356652C3B`. +Neither parent manifest was rewritten. + +The final-i holder-aware partial trace, pure-pipe bridge, eighteen-route +saved-read and combined extended campaigns, plus their independent audits, +PASS in both worlds. These are new final-candidate runs, not passes inherited +from an earlier artifact. + +| Final-i campaign | Evidence directory suffix | Result | +| --- | --- | --- | +| Partial trace and both load orders | `-holder-trace-i1` | PASS, independent audit PASS | +| Three pipe-only bridge cycles | `-holder-bridge-i1` | PASS, independent audit PASS | +| Eighteen moving-drill saved-state routes | `-extended-reads-i1` | PASS, independent audit PASS | +| Combined extended drills and partial networks | `-holder-extended-i1` | PASS, independent audit PASS | + +Every suffix above belongs to the ignored prefix +`test-rig/build/native114-broader-partial-20261025`. Each run retains the source +fingerprints, exact input identities and hashes, native console logs, raw/canonical +snapshots, actual chunk events, protocol copies and its independent audit. + +The final trace records 54 genuine unloads and 54 traced reloads per world, +with zero acquisition stacks from Power's conduit-network implementation. +The combined run passes four partial cycles and both asymmetric load orders +for each of steam, electricity and fluid. Each world also passes 18 cases in +each of insufficient reserve, four-move routes, blocked output, +blocked-after-restart, resumed output and redstone groups, plus all 21 +restart-state cases. The independent audit checks 50 raw/canonical byte pairs +per world; it does not accept edited observations or summary-only verdicts. + +All four campaigns report unchanged protected source runtimes and helpers. +The final target manifest SHA-256 is +`E1707FF45B59E7103E73444B3D6FBF24BA4EAD694A9A3215B02B98EA7EDA82FF`. +The combined run completed at `2026-10-25T08:19:07Z`, with normal save/stop +sequences before and after a full process restart. Owned ports 25581-25583 +were checked free afterward. No unresolved production network defect remains +within this tested scope; graphical acceptance and the separately owned timed +endurance results are not asserted here. + +## Running The Adapters + +All entry points require an archived source manifest, its approved world +fingerprint, the pinned native destination manifest, Java 8 and a new output +directory under `test-rig/build`. They refuse source/output overlap and verify +the staged artifacts. The allocated loopback ports are 25581 through 25583. + +- `migration/native114_partial.py --campaign partial`: original two partial + directions and both asymmetric load orders. +- `migration/native114_partial.py --campaign bridge`: original three pipe-only + cycles and unchanged independent bridge oracle. +- `migration/native114_partial_diagnostic.py`: empty-bay ticket control, not a + replacement for a real network test. +- `migration/native114_extended.py`: four moves per drill, four partial cycles, + both load orders, blocked output, process restart and redstone cancellation. +- `migration/native114_extended_reads.py`: all eighteen routes through normal + mining, whole-chunk and process restart with unfinished work, and both saved + target-state read guards. +- `migration/audit_native114_partial.py --run --output `: + reconstruct verdicts from raw snapshots and verify exact canonical bytes, + source protection, fixture geometry, artifacts, boots and shutdown. + +## Ticket-Aware Follow-Up + +`native114_ticket_networks.py` adds separate first-attempt native layouts rather than changing +the historical ones. The three linear networks have endpoints eight chunks +apart and 128-block paths. The three U-shaped pipe bridges have both endpoints +in one chunk and a remote bridge chunk, also eight chunks away. Non-overlapping +bays and explicit support-ticket groups keep each selected core outside the +two-chunk halo of every retained ticket. + +That first attempt loses FULL availability but still does not receive a genuine +unload event. Its failed `-halo-*-h1` evidence remains retained. FULL availability +and actual holder removal are distinct on this target; a false snapshot +`loaded` flag is not permission to manufacture an unload callback. + +The exact installed source defines +`ChunkManager.field_219249_a = 33 + ChunkStatus.func_222600_b()`. +`ChunkStatus.field_222620_p` has eleven entries, so holders remain through level +44. `ChunkManager.func_219213_a` schedules removal only above that level, and +`func_219212_a` posts the real Forge unload event from the later save task. +`TicketManager.func_219364_a` gives forced chunks level 31. Consequently, a +two-chunk FULL halo is insufficient: the holder-dependency radius is thirteen. +These are facts from Forge 28.2.26's exact native sources, not inferred loading +behavior from a newer Minecraft version. + +The inspected `injected-sources.jar` locations are +`net/minecraft/world/server/ChunkManager.java:95` (maximum holder level), +`:275` (level transition), `:392` and `:402` (queued removal and real unload +event); `net/minecraft/world/server/TicketManager.java:198` (level-31 forced +ticket); and `net/minecraft/world/chunk/ChunkStatus.java:96` and `:164` +(eleven-entry dependency list and its size). The archive belongs to the local +ForgeGradle Mavenizer cache for `forge/1.14.4-28.2.26`, not a newer engine. + +`native114_holder_networks.py` therefore adds separately named fixtures with +32-chunk-separated anchors and explicit thirteen-chunk support-halo release. +Linear networks are 512 blocks long; U-shaped paths contain 1,027 passive pipes. +Every retained forced ticket lies outside the selected core's complete holder +radius. All eighteen historical drill layouts are unchanged. Six additional +Python tests protect the threshold, bounds, non-overlap, paths and clearing. + +The converter-fixed h2 trace and bridge, plus their independent audits, PASS in +fresh and copied worlds. The trace records 108 genuine unload events and 108 +traced reload events across the two worlds, with zero acquisition stacks from +Power's network implementation. The bridge completes all six network cycles +across the two worlds, each checking all three networks: eighteen network cases. +It never replaces an endpoint, accepts exactly zero while disconnected, then +retains precisely 3,000 mB per world after measured reconnect offers. + +Releasing a core records the baseline before releasing its support halo, then +releases the core's own ticket. Loading restores only the requested halo before +applying the ordinary chunk/tick readiness checks. The historical sustained +absence, callback, identity, conservation and recovery oracles remain unchanged. +Large clearing operations are divided into native sub-chunk-sized fill commands, +all confined exactly to the declared bay. + +The Java harness retains its 32-block legacy limit. Only an explicitly marked +`C-STEAM`, `C-ELECTRIC` or `C-FLUID` network with declared endpoint groups may use +one of the fixed 160-by-32 or 544-by-32 footprints; drill/oil fixtures, excessive height, two expanded +axes, arbitrary IDs and out-of-bounds chunk groups are rejected. The native +layouts use rebuilt, separately pinned harnesses. Old manifests still resolve +their original archive bytes. + +The related focused Python suite has 87 passing tests, including eleven native +observation controls, nine ticket-layout/control tests and six holder tests. +The complete Gradle harness test, assembly and isolation audit pass with 46 Java +tests. Earlier direct Java 8-targeted compilation and 27 probe tests are retained +as separate development evidence, not substituted for the Gradle suite. + +Some large-fixture runs log native catch-up warnings while synchronously loading +the test region, before commissioning. The runner records their counts and the +independent auditor retains their exact lines. Sustained absence still requires +forty actual game ticks, not elapsed sleep. These operating/state campaigns are +not a claim of lag-free region creation or a substitute for the separate timed +endurance campaign. + +No production network change, commit, push or publication was performed as part +of this independent campaign. The registration repair was built and pinned +separately, then supplied to these final-candidate reruns. diff --git a/docs/1.14/PORT-STATUS.md b/docs/1.14/PORT-STATUS.md new file mode 100644 index 0000000..6c260a2 --- /dev/null +++ b/docs/1.14/PORT-STATUS.md @@ -0,0 +1,193 @@ +# Advantage 1.14.4 Port + +## Starting Point + +The tested 1.14.4 candidate is accepted for merging. Visuals and recipe unlocks +were manually checked and accepted on 26 October 2026. Machine functionality is +covered by automated tests, not personal play-testing. The acceptance does not +close the remaining coverage limits below. + +The three `master-1.14.4` branches start from the tested 1.13.2 port. The approved +1.10, 1.12 and 1.13 changes are merged upstream. Commit, push, PR and merge are +authorized for this candidate; ancestry reconciliation and publication work are +handled separately from this qualification record. + +The checkouts follow the existing layout: + +```text +PA114/PowerAdvantage +PA114/SteamAdvantage/SteamAdvantage +PA114/ElectricAdvantage/ElectricAdvantage +``` + +Target: Minecraft 1.14.4, Forge 28.2.26, ForgeGradle 7.0.34, Gradle 9.6.1, +Java 17 for Gradle and Java 8 production bytecode. Base Metals' maintained +1.14 branch is the platform reference, not a source of Advantage gameplay policy. + +## Native API Review + +- Forge 28.2.26 has `DeferredRegister`, `FluidAttributes`, `ForgeFlowingFluid`, + `IFluidHandler` and `LazyOptional`. Use native fluid rendering and capabilities + instead of the Forge 25 renderer transformer. Keep the public Advantage fluid + API as an adapter, with its existing millibucket and input-immutability rules. +- Containers now have registered `ContainerType` objects and client screen + factories. Use `NetworkHooks.openGui` with a named provider and explicit tile + position; replace the old GUI extension-point packet interception. +- Inventory and energy capabilities must follow tile lifecycle and invalidate + their optionals when the tile is removed. Loading a GUI must not be required + to restore a network or machine. +- Inspect native recipe conditions, reload listeners and loot-table events before + carrying over the material-tag and parts-loot transformers. Remove a transformer + only after its replacement preserves recipe modes, data-pack overrides and loot. +- The 1.14 villager profession and trade changes require a real migration policy; + do not quietly drop saved non-vanilla profession mappings. +- Keep the pre-flattening world migration hook where vanilla/Forge still cannot + interpret legacy mod numeric IDs. Requalify its descriptors and interaction + with Base Metals; do not copy 1.13 bytecode patches blindly. + +References: [Forge registries](https://docs.minecraftforge.net/en/1.14.x/concepts/registries/), +[Forge 28.2.26 sources](https://github.com/MinecraftForge/MinecraftForge/tree/1.14.x/src/main/java/net/minecraftforge), +and the local published Base Metals 1.14 implementation. The exact installed +Forge source jar was inspected to verify these APIs exist on this target. + +## Compatibility Gates + +Preserve mod IDs, registry and fluid identities, tile/entity aliases, recipe modes, +NBT keys, provider IDs, configuration meaning and legacy public API packages. +New native platform types may replace implementation glue, not saved identities. + +Run the existing 1.10/1.12 direct and chained migration fixtures, then add a +1.13-to-1.14 route. Compare state before ticking, operate connected assemblies, +save, stop the process completely and compare again. Source fixtures and the +original Sylvester world remain untouched. + +Required checks include native fluid/bucket capability interoperability, +drain-pipe-container conservation, native menu synchronization, every drill facing, +movement and unfinished work, partial-network unloading, inventories, batteries, +player/entity migration, all recipe modes, client resources and endurance. +The older qualification gaps remain open until tested, including the broad +station oracle and real-client bed/death/respawn behavior. + +## Current Status + +The current broader run is recorded in +[Broader Qualification](BROADER-QUALIFICATION.md), including exact candidate +hashes, retained failures, manual acceptance and the disposable viewer. Earlier +results below retain their original scope; they are not a claim of whole-modpack +acceptance. + +- Local branches and source-only checkouts created. +- Exact target Forge API and sibling dependency versions inspected. +- All three production source sets compile against the native 1.14 target. +- Power's 60 unit tests, release, dependency, artifact and Eclipse audits passed. + Steam's 27 unit tests and Electric's 34 unit tests passed; these are + not connected-machine or migration results. + The isolated harness has 46 passing Java tests; its Python adapters and + unchanged semantic oracles have 342 passing regression tests. +- Native fluid attributes/capabilities and containers replace the transitional + 1.13 implementations. Native tag and loot events replace their transformers. +- The first packaged Power server exposed an immutable-tag-collection failure. + The original log is retained locally. The collection is now rebuilt through + `registerAll`, with a regression fixture. The fixed combined packaged server + started cleanly and passed the W-01, W-02 and P-API-01 server assertions. +- Native block loot includes portable tank NBT. Registered tile types declare + valid blocks. The legacy migration scripts have been adapted to the target + chunk converter, with both Base Metals transformer orders covered by unit tests. +- Thirteen original/maintained and chained frozen fixture routes pass native + conversion and a full process restart: 355 block, 332 tile and 2,654 item + witnesses in the standard route, with 2,656 items in the 1.12 fixture. These + are state-preservation checks and have passed again on the final candidate. +- Five archived connected assemblies also pass initial preservation and + 512+512 operating ticks, including another process restart. Conveyors, + processors, both drill types and battery charging resume without rebuilding, + replacement or refill; bounded inputs and outputs reconcile. +- Those fixtures caught a fluid-name lookup defect: Forge's empty-fluid default + hid the vanilla `water` fallback. Registry membership is now checked explicitly, + with three regression tests. The original failing evidence is retained. +- Vanilla 1.14 omits empty chest lock keys. Only vanilla chest roots normalize + missing `Lock` to the canonical empty string; changed or lost nonempty locks + still fail comparison. Raw snapshots remain unchanged. +- The combined native client resource probe passed for 94 items, 905 block + states and 87 creative entries. It also checked both oil buckets, oil/water/lava + rendering, the tank floor and a 10,000 mB water gauge. Manual visuals and recipe + unlocks are now accepted; in-world machine operation was not personally tested. +- The original 21-bay failure is retained. `Q-RIG-001` is now repaired with exact + initial positions, bounded track destinations, persistent tile markers, + inventory conservation and saved energy checks. Both current and decoupled + profiles pass before and after restart. Visual acceptance does not sign off all + 21 itemized human observation lists; machine-function results are automated. + The decoupled profile omits Base Metals but retains required OreSpawn. +- Native fallback generation and legacy import passed. The genuine pinned + Mineralogy 1.14 build also passed both distillation lanes and ore-source/profile + checks, including restart persistence and the existing boundary-aware oil + generation oracle. This does not qualify every fuel or bucket interaction. +- Native recipe discovery passes five boots, with 109 cases per operation and + no skips. Manufacturing progression passes 18 checks across all recipe modes, + reloads, restarts and both rubber-alias settings. The combined graphical probe + also passes recipe-book grouping in all three modes and renders all 13 target + buttons with the existing pixel threshold. +- Six oil-transfer lanes pass bucket placement, drains, descending pipes and both + container types, including Mineralogy crude oil. Exact 1,000/2,000 mB quantities + and fluid identities survive two process restarts; refill does not replace the + machines. Earlier unsupported-fixture evidence is retained separately. +- All three production source sets also pass Eclipse's actual batch compiler. + Power's clean source export in a path with spaces and a fresh Gradle cache + passes its full suite with build caching disabled. Release, source, Javadoc + and deobf hashes match the working checkout. This is an uncommitted source + export, not a claim of published-commit provenance. +- Steam and Electric also pass independent, uncached production builds in paths + with spaces. All twelve release, deobf, source and Javadoc artifacts match the + corresponding working-checkout artifacts byte-for-byte. +- The first movement-aware run traced a Steam drill reload loop to a world lookup + during unload-save. Saving now uses cached facing only; the new unit regression + fails against the old method and passes with the fix. +- The native lifecycle campaign now passes in both a fresh world and the copied + 1.10-to-1.12-to-1.13 world. Each completes all seven twelve-drill groups and five + genuine unload/reload cycles, network processing, 5,000 mB transfer, mining + output, inventory and energy conservation, mid-block work, full restarts and + deferred legacy/changed-target read guards. +- Native observation readiness explicitly restores fixture tickets after restart + and waits for real Load events and the first measured tick. Unload observations + also await the actual queued callback within the original 45-second budget. + No flags, events, ticks or work values are fabricated. The original fixtures, + semantic oracles, thresholds and failed evidence remain unchanged. +- Wider actors, nested storage, both dimensions, 72 server-side respawn cases + and observed/reversed real migration-transformer orders pass on the final + candidate. Keep the complete backup set: which mod's backup holds the original + bytes depends on callback order. Real client travel and death-screen handling + remain manual. +- Broader tests caught incoming converter energy being subtracted instead of + added, and unsafe registration of Forge's world-save hook during parallel mod + construction. Both have focused red/green regressions and packaged reruns. + Registration now uses queued common-setup work. Existing identities and + conversion rates are unchanged. +- Native partial-unload fixtures require wider geometry: Forge's level-31 + forced tickets retain holders through level 44. The separate holder-aware + layouts preserve genuine unload/load, sustained absence and conservation + assertions. Their final partial, bridge, saved-read and extended campaigns + pass, with independent evidence audits. Both final thirty-minute environments + also pass full restart and resumed processing; raw-checkpoint replay passes. +- Steam and Electric dependency source/hash pins remain deliberately + `UNQUALIFIED-1.14-PORT` until the exact Power candidate can be pinned. +- These results identify the tested local artifacts, not a completed upstream + merge or published release. All world conversions used disposable copies; + no protected source save was opened or converted in place. +- Native114 profiles retain `PORTED_UNQUALIFIED` and `UNQUALIFIED` as historical + launch/schema guards. The runner, adapter and profile regression require those + exact tokens; they are not the current acceptance status. The reports record + the actual automated PASS results and manual scope. No harness code or profile + metadata was changed to rename them. + +## Remaining Coverage and Delivery + +- Visuals and recipe unlocks are manually accepted. Personal checks of machine + operation, GUI readings during use, sound, controls, weapons, multiplayer and + real third-party equipment remain outside that acceptance. These results do + not qualify the entire Sylvester modpack or any unrelated unported mod. +- For the authorized commit, push, PR and merge work, reconcile the final 1.13 + merge ancestry while preserving the port. Pin the exact tested Power source + commit and deobf hash in Steam/Electric, then run their complete release audits. + Their current `UNQUALIFIED-1.14-PORT` dependency pins intentionally fail those + gates; acceptance is not a substitute for exact source/artifact provenance. +- Publication is handled separately. This record does not claim a completed + merge, tag, release or independently verified public download. diff --git a/gradle.properties b/gradle.properties index 88a84aa..31855df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,15 +5,15 @@ org.gradle.caching=true org.gradle.parallel=false net.minecraftforge.gradle.merge-source-sets=false -minecraft_version=1.13.2 -minecraft_version_range=[1.13.2] -forge_version=25.0.223 -forge_installer_sha256=D46CB326E7DC32695084090C8169BB57F32F1C23B801DDB35A336F252E381191 -forge_version_range=[25,) -loader_version_range=[25,) +minecraft_version=1.14.4 +minecraft_version_range=[1.14.4] +forge_version=28.2.26 +forge_installer_sha256=E68A0E2D589B2954C5D109A04B80747AC8C061ACABE074DB1607F9C2FD7D2CE1 +forge_version_range=[28,) +loader_version_range=[28,) mapping_channel=snapshot -mapping_version=20180921-1.13 -mcp_version=20190213.203750 +mapping_version=20190719-1.14.3 +mcp_version=20190829.143755 loader_name=forge loader_code=1 @@ -26,25 +26,21 @@ curseforge_project_id=240982 mod_id=poweradvantage mod_name=Power Advantage mod_license=LGPL-3.0-only -mod_version=2.4.4.113021 +mod_version=2.4.5.114041 mod_group=zone.moddev.mc.poweradvantage mod_authors=DrCyano (aka Cyanobacterium) mod_description=API and shared machines for pressure, electricity, quantum power, fluids, and item transport. cf_requirements=base-metals,mmd-orespawn cf_optionals=additional-loot-tables -cf_versions=1.13.2 +cf_versions=1.14.4 -basemetals_version=3.0.1.113021 -basemetals_source_commit=1376a20d7d5ba22d3abc77dc34b49a2c88943697 +basemetals_version=3.0.1.114041 +basemetals_source_commit=713802f93f2d1f650e1593fa35c487ea43add4a5 basemetals_curse_project_id=240967 -basemetals_curse_file_id=9063696 -basemetals_deobf_sha256=2E4B0F619AD6B516A341DD332FABB66FE882A9DBA8E5C74F5BB52942302CFD18 -# Same pinned source and class bytes, with a clean LF checkout on Linux. -basemetals_deobf_lf_sha256=D1843CFAB23E7DB4B8A64ADB280D7EA51E3909B8E3490D4F5D471F0C94E1D0BF +basemetals_deobf_sha256=A14A87E3BF30964F901E715A9B4EF2F1E7317ABE7F9ACEBE60D8E6A900CA6EB2 -orespawn_version=4.1.0.113021 -orespawn_source_commit=6a7c2bb07b9a7771128812fcc07d185f7d9df58b +orespawn_version=4.1.1.114041 +orespawn_source_commit=9c2488cba02d317e3f31e24441dcc58273d8d0e6 orespawn_curse_project_id=245586 -orespawn_curse_file_id=9035885 -orespawn_deobf_sha256=F5A015584000CCEF0E94D8BB567E7B75603B37F10A88BFC5A129176D487BB038 +orespawn_deobf_sha256=1807C3A43F6E854F4EC5741F75A52750B733CD81DC588CF0F666C6C3A489FBEF diff --git a/src/main/java/cyano/poweradvantage/api/ConduitBlock.java b/src/main/java/cyano/poweradvantage/api/ConduitBlock.java index ba27e15..b3cd267 100644 --- a/src/main/java/cyano/poweradvantage/api/ConduitBlock.java +++ b/src/main/java/cyano/poweradvantage/api/ConduitBlock.java @@ -2,7 +2,7 @@ import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry; import net.minecraft.block.material.Material; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.util.math.BlockPos; import net.minecraft.world.Explosion; import net.minecraft.world.World; @@ -41,8 +41,8 @@ protected ConduitBlock(Material mat, float hardness) { * This method is called whenever the block is placed into the world */ @Override - public void onBlockAdded(IBlockState state, World w, BlockPos coord, IBlockState oldState) { - super.onBlockAdded(state, w, coord, oldState); + public void onBlockAdded(BlockState state, World w, BlockPos coord, BlockState oldState, boolean isMoving) { + super.onBlockAdded(state, w, coord, oldState, isMoving); ConduitRegistry.getInstance().conduitBlockPlacedEvent(w, w.dimension.getType().getId(), coord, getTypes()); } @@ -50,7 +50,7 @@ public void onBlockAdded(IBlockState state, World w, BlockPos coord, IBlockState * This method is called when the block is removed from the world by an entity. */ @Override - public void onReplaced(IBlockState state, World w, BlockPos coord, IBlockState newState, boolean isMoving) { + public void onReplaced(BlockState state, World w, BlockPos coord, BlockState newState, boolean isMoving) { if (state.getBlock() != newState.getBlock()) { ConduitRegistry.getInstance().conduitBlockRemovedEvent(w, w.dimension.getType().getId(), coord, getTypes()); } diff --git a/src/main/java/cyano/poweradvantage/api/FluidCategoryRegistry.java b/src/main/java/cyano/poweradvantage/api/FluidCategoryRegistry.java index ba5718c..12c3a2c 100644 --- a/src/main/java/cyano/poweradvantage/api/FluidCategoryRegistry.java +++ b/src/main/java/cyano/poweradvantage/api/FluidCategoryRegistry.java @@ -9,7 +9,7 @@ import net.minecraft.fluid.Fluid; import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.IRegistry; +import net.minecraft.util.registry.Registry; /** * Registry of semantic fluid categories shared by Advantage mods. @@ -45,7 +45,7 @@ public static synchronized boolean registerAlias(String category, String fluidNa /** Returns true when the fluid's registry name belongs to the category. */ public static synchronized boolean matches(String category, Fluid fluid) { if (fluid == null) return false; - ResourceLocation id = IRegistry.field_212619_h.getKey(fluid); + ResourceLocation id = net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(fluid); return id != null && matches(category, id.toString()); } @@ -70,7 +70,7 @@ public static synchronized Set getAliases(String category) { /** Returns the currently registered fluids represented by a category. */ public static synchronized Set getRegisteredFluids(String category) { LinkedHashSet fluids = new LinkedHashSet<>(); - for (Fluid fluid : IRegistry.field_212619_h) { + for (Fluid fluid : net.minecraftforge.registries.ForgeRegistries.FLUIDS) { if (matches(category, fluid)) fluids.add(fluid); } return Collections.unmodifiableSet(fluids); diff --git a/src/main/java/cyano/poweradvantage/api/GUIBlock.java b/src/main/java/cyano/poweradvantage/api/GUIBlock.java index 7ca1917..62e3f97 100644 --- a/src/main/java/cyano/poweradvantage/api/GUIBlock.java +++ b/src/main/java/cyano/poweradvantage/api/GUIBlock.java @@ -3,15 +3,15 @@ import net.minecraft.block.material.Material; import net.minecraft.block.Block; import net.minecraft.block.SoundType; -import net.minecraft.block.state.IBlockState; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.block.BlockState; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.ServerPlayerEntity; import net.minecraft.inventory.IInventory; import net.minecraft.inventory.InventoryHelper; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumBlockRenderType; -import net.minecraft.util.EnumFacing; -import net.minecraft.util.EnumHand; +import net.minecraft.block.BlockRenderType; +import net.minecraft.util.Direction; +import net.minecraft.util.Hand; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; import net.minecraft.world.IBlockReader; @@ -32,7 +32,7 @@ * @author DrCyano * */ -public abstract class GUIBlock extends net.minecraft.block.BlockContainer{ +public abstract class GUIBlock extends net.minecraft.block.ContainerBlock{ /** * Constructor for GUI block @@ -97,21 +97,20 @@ public Object getGuiOwner(){ /** * Boilerplate code */ - @Override - public boolean isFullCube(IBlockState bs) { + public boolean isFullCube(BlockState bs) { return false; } /** * Boilerplate code */ - public boolean isOpaqueCube(IBlockState bs) { + public boolean isOpaqueCube(BlockState bs) { return false; } /** 1.13 uses this flag, not the legacy opacity overload, to cull neighbours. */ @Override - public boolean isSolid(IBlockState state) { + public boolean isSolid(BlockState state) { return false; } @@ -120,8 +119,8 @@ public boolean isSolid(IBlockState state) { * -1 = special renderer */ @Override - public EnumBlockRenderType getRenderType(IBlockState bs) { - return EnumBlockRenderType.MODEL; + public BlockRenderType getRenderType(BlockState bs) { + return BlockRenderType.MODEL; } /** @@ -133,9 +132,9 @@ public EnumBlockRenderType getRenderType(IBlockState bs) { */ @SuppressWarnings("deprecation") @Override - public boolean onBlockActivated(IBlockState bs, World w, BlockPos coord, - EntityPlayer player, EnumHand hand, EnumFacing facing, - float hitX, float hitY, float hitZ) { + public boolean onBlockActivated(BlockState bs, World w, BlockPos coord, + PlayerEntity player, Hand hand, net.minecraft.util.math.BlockRayTraceResult hit) { + Direction facing = hit.getFace(); if (w.isRemote) { return true; } @@ -152,18 +151,18 @@ public boolean onBlockActivated(IBlockState bs, World w, BlockPos coord, // open GUI if(this.getGuiOwner() == null) return false; - if(tileEntity instanceof PoweredEntity && player instanceof EntityPlayerMP){ - ((PoweredEntity)tileEntity).syncPowerAdvantageDataTo((EntityPlayerMP)player); + if(tileEntity instanceof PoweredEntity && player instanceof ServerPlayerEntity){ + ((PoweredEntity)tileEntity).syncPowerAdvantageDataTo((ServerPlayerEntity)player); } - if (!(player instanceof EntityPlayerMP)) return false; - MachineGUIRegistry.open((EntityPlayerMP) player, w, coord, this.getGuiID()); + if (!(player instanceof ServerPlayerEntity)) return false; + MachineGUIRegistry.open((ServerPlayerEntity) player, w, coord, this.getGuiID()); return true; } @Override - public void onReplaced(IBlockState state, World world, BlockPos pos, IBlockState newState, + public void onReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean isMoving) { if (state.getBlock() != newState.getBlock()) { TileEntity te = world.getTileEntity(pos); diff --git a/src/main/java/cyano/poweradvantage/api/PowerConnectorContext.java b/src/main/java/cyano/poweradvantage/api/PowerConnectorContext.java index f2592fb..5c2ae8d 100644 --- a/src/main/java/cyano/poweradvantage/api/PowerConnectorContext.java +++ b/src/main/java/cyano/poweradvantage/api/PowerConnectorContext.java @@ -1,7 +1,7 @@ package cyano.poweradvantage.api; -import net.minecraft.block.state.IBlockState; -import net.minecraft.util.EnumFacing; +import net.minecraft.block.BlockState; +import net.minecraft.util.Direction; import net.minecraft.util.math.BlockPos; import net.minecraft.world.IBlockReader; @@ -22,7 +22,7 @@ public class PowerConnectorContext { /** * Block that is asking for the connection */ - public final IBlockState thisBlock; + public final BlockState thisBlock; /** * Position of the block that is asking for the connection */ @@ -30,11 +30,11 @@ public class PowerConnectorContext { /** * Face on the block that is asking for the connection */ - public final EnumFacing thisBlockSide; + public final Direction thisBlockSide; /** * Block that is being asked for the connection */ - public final IBlockState otherBlock; + public final BlockState otherBlock; /** * Position of the block that is being asked for the connection */ @@ -42,7 +42,7 @@ public class PowerConnectorContext { /** * Face on the block that is being asked for the connection */ - public final EnumFacing otherBlockSide; + public final Direction otherBlockSide; /** * Constructs a data structure instance @@ -59,12 +59,12 @@ public class PowerConnectorContext { public PowerConnectorContext( final ConduitType powerType, final IBlockReader world, - final IBlockState connectorBlock, + final BlockState connectorBlock, final BlockPos connectorPosition, - final EnumFacing connectorSide, - final IBlockState connecteeBlock, + final Direction connectorSide, + final BlockState connecteeBlock, final BlockPos connecteePosition, - final EnumFacing connecteeSide + final Direction connecteeSide ) { this.world = world; this.powerType = powerType; @@ -79,9 +79,9 @@ public PowerConnectorContext( public PowerConnectorContext( final ConduitType powerType, final IBlockReader world, - final IBlockState connectorBlock, + final BlockState connectorBlock, final BlockPos connectorPosition, - final EnumFacing connectorSide + final Direction connectorSide ) { this.world = world; this.powerType = powerType; diff --git a/src/main/java/cyano/poweradvantage/api/PoweredEntity.java b/src/main/java/cyano/poweradvantage/api/PoweredEntity.java index e6531eb..36d607e 100644 --- a/src/main/java/cyano/poweradvantage/api/PoweredEntity.java +++ b/src/main/java/cyano/poweradvantage/api/PoweredEntity.java @@ -1,13 +1,13 @@ package cyano.poweradvantage.api; import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry; -import net.minecraft.block.state.IBlockState; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.network.Packet; +import net.minecraft.block.BlockState; +import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.network.IPacket; import net.minecraft.tileentity.TileEntity; import net.minecraft.tileentity.TileEntityType; -import net.minecraft.util.ITickable; +import net.minecraft.tileentity.ITickableTileEntity; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; @@ -25,10 +25,29 @@ * @author DrCyano * */ -public abstract class PoweredEntity extends TileEntity implements ITickable, IPowerMachine { +public abstract class PoweredEntity extends TileEntity implements ITickableTileEntity, IPowerMachine { private final int powerUpdateInterval = 8; private int conduitRefreshDelay = 2; + private final zone.moddev.mc.poweradvantage.util.MachineCapabilities machineCapabilities = + new zone.moddev.mc.poweradvantage.util.MachineCapabilities(this); + + @Override + public net.minecraftforge.common.util.LazyOptional getCapability( + net.minecraftforge.common.capabilities.Capability capability, net.minecraft.util.Direction face) { + net.minecraftforge.common.util.LazyOptional local = machineCapabilities.get(capability, face); + return local.isPresent() ? local : super.getCapability(capability, face); + } + + @Override protected void invalidateCaps() { + super.invalidateCaps(); + machineCapabilities.invalidate(); + } + + @Override public void validate() { + super.validate(); + reviveCaps(); + } protected PoweredEntity(TileEntityType type) { super(type); @@ -109,14 +128,10 @@ public float subtractEnergy(float energy, ConduitType type) { * Tells Minecraft to synchronize this tile entity */ protected final void sync() { + if (this.world == null || this.world.isRemote) return; this.markDirty(); - //this.getWorld().markBlockForUpdate(getPos()); - Packet packet = this.getUpdatePacket(); - if (packet == null) return; - List players = this.getWorld().getPlayers(EntityPlayerMP.class, (EntityPlayerMP p) -> p.getPosition().distanceSq(getPos()) < 256); - for (EntityPlayerMP player : players) { - player.connection.sendPacket(packet); - } + BlockState state = this.world.getBlockState(getPos()); + this.world.notifyBlockUpdate(getPos(), state, state, 2); } /** @@ -128,7 +143,7 @@ protected final void sync() { * @param newSate State after change * @return true if this TileEntity should be invalidated (deleted and recreated), false otherwise */ - public boolean shouldRefresh(World world, BlockPos pos, IBlockState oldState, IBlockState newSate) { + public boolean shouldRefresh(World world, BlockPos pos, BlockState oldState, BlockState newSate) { return oldState.getBlock() != newSate.getBlock(); } @@ -156,9 +171,9 @@ public void onLoad() { * * @param player player receiving the current machine state */ - public final void syncPowerAdvantageDataTo(EntityPlayerMP player) { + public final void syncPowerAdvantageDataTo(ServerPlayerEntity player) { this.markDirty(); - Packet packet = this.getUpdatePacket(); + IPacket packet = this.getUpdatePacket(); if (packet != null) { player.connection.sendPacket(packet); } @@ -241,7 +256,7 @@ private final int getTickOffset() { * packet. */ @Override - public void read(final NBTTagCompound tagRoot) { + public void read(final CompoundNBT tagRoot) { super.read(tagRoot); this.conduitRefreshDelay = 2; if (tagRoot.contains("Energy", 11)) { @@ -258,13 +273,13 @@ public void read(final NBTTagCompound tagRoot) { * the network. */ @Override - public NBTTagCompound write(final NBTTagCompound tagRoot) { + public CompoundNBT write(final CompoundNBT tagRoot) { super.write(tagRoot); int[] data = new int[this.getTypes().length]; for (int i = 0; i < data.length; i++) { data[i] = Float.floatToIntBits(this.getEnergy(this.getTypes()[i])); } - tagRoot.setIntArray("Energy", data); + tagRoot.putIntArray("Energy", data); return tagRoot; } diff --git a/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java b/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java index 300455d..77e8834 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/FluidPoweredEntity.java @@ -6,9 +6,9 @@ import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry; import zone.moddev.mc.poweradvantage.init.Fluids; import net.minecraft.fluid.Fluid; -import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.CompoundNBT; import net.minecraft.tileentity.TileEntityType; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import java.util.List; @@ -79,11 +79,11 @@ protected List getRequestsForFluid(ConduitType powerType) { * @param tagRoot An NBT tag */ @Override - public NBTTagCompound write(final NBTTagCompound tagRoot) { + public CompoundNBT write(final CompoundNBT tagRoot) { super.write(tagRoot); - NBTTagCompound tankTag = new NBTTagCompound(); + CompoundNBT tankTag = new CompoundNBT(); this.getTank().writeToNBT(tankTag); - tagRoot.setTag("Tank", tankTag); + tagRoot.put("Tank", tankTag); return tagRoot; } @@ -93,10 +93,10 @@ public NBTTagCompound write(final NBTTagCompound tagRoot) { * @param tagRoot An NBT tag */ @Override - public void read(final NBTTagCompound tagRoot) { + public void read(final CompoundNBT tagRoot) { super.read(tagRoot); if (tagRoot.contains("Tank", 10)) { - NBTTagCompound tankTag = tagRoot .getCompound("Tank"); + CompoundNBT tankTag = tagRoot .getCompound("Tank"); getTank().readFromNBT(tankTag); if (tankTag.contains("Empty", 1)) { // empty the tank if NBT says its empty (not default behavior of Tank.readFromNBT(...) ) @@ -201,7 +201,7 @@ public FluidStack drain(int amount, boolean forReal) { * @param fluid The fluid being added/removed * @param forReal if true, then the fluid in the tank will change */ - public int fill(EnumFacing face, FluidStack fluid, boolean forReal) { + public int fill(Direction face, FluidStack fluid, boolean forReal) { if (fluid != null && (getTank().getFluidAmount() <= 0 || getTank().getFluid().getFluid().isEquivalentTo(fluid.getFluid()))) { return getTank().fill(fluid, forReal); } else { @@ -216,12 +216,8 @@ public int fill(EnumFacing face, FluidStack fluid, boolean forReal) { * @param fluid The fluid being added/removed * @param forReal if true, then the fluid in the tank will change */ - public FluidStack drain(EnumFacing face, FluidStack fluid, boolean forReal) { - if (fluid != null && getTank().getFluidAmount() > 0 && getTank().getFluid().getFluid().isEquivalentTo(fluid.getFluid())) { - return getTank().drain(fluid.amount, forReal); - } else { - return new FluidStack(fluid.getFluid(), 0); - } + public FluidStack drain(Direction face, FluidStack fluid, boolean forReal) { + return getTank().drain(fluid, forReal); } /** @@ -231,7 +227,7 @@ public FluidStack drain(EnumFacing face, FluidStack fluid, boolean forReal) { * @param amount The amount of fluid being added/removed * @param forReal if true, then the fluid in the tank will change */ - public FluidStack drain(EnumFacing face, int amount, boolean forReal) { + public FluidStack drain(Direction face, int amount, boolean forReal) { if (getTank().getFluidAmount() > 0) { return getTank().drain(amount, forReal); } else { @@ -245,7 +241,7 @@ public FluidStack drain(EnumFacing face, int amount, boolean forReal) { * @param face Face of the block being polled * @param fluid The fluid being added/removed */ - public boolean canFill(EnumFacing face, Fluid fluid) { + public boolean canFill(Direction face, Fluid fluid) { if (fluid == null) return false; if (getTank().getFluid() == null) return true; return getTank().getFluidAmount() <= getTank().getCapacity() && fluid.equals(getTank().getFluid().getFluid()); @@ -257,7 +253,7 @@ public boolean canFill(EnumFacing face, Fluid fluid) { * @param face Face of the block being polled * @param fluid The fluid being added/removed */ - public boolean canDrain(EnumFacing face, Fluid fluid) { + public boolean canDrain(Direction face, Fluid fluid) { if (fluid == null) return false; if (getTank().getFluid() == null) return false; return getTank().getFluidAmount() > 0 && fluid.equals(getTank().getFluid().getFluid()); @@ -269,7 +265,7 @@ public boolean canDrain(EnumFacing face, Fluid fluid) { * @param face Face of the block being polled * @return array of FluidTankInfo describing all of the FluidTanks */ - public FluidTankInfo[] getTankInfo(EnumFacing face) { + public FluidTankInfo[] getTankInfo(Direction face) { FluidTankInfo[] arr = new FluidTankInfo[1]; arr[0] = getTank().getInfo(); return arr; diff --git a/src/main/java/cyano/poweradvantage/api/fluid/FluidStack.java b/src/main/java/cyano/poweradvantage/api/fluid/FluidStack.java index 63750f7..6a3021c 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/FluidStack.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/FluidStack.java @@ -3,19 +3,19 @@ import javax.annotation.Nullable; import net.minecraft.fluid.Fluid; -import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.CompoundNBT; import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.IRegistry; +import net.minecraft.util.registry.Registry; import zone.moddev.mc.poweradvantage.init.Fluids; /** - * Forge 25's transitional fluid stack is unfinished, so Advantage keeps its - * millibucket state in this native-fluid-backed value object. + * Stable Advantage millibucket value object. Native Forge fluid capabilities + * are bridged without changing this API or its saved NBT representation. */ public final class FluidStack { private final Fluid fluid; public int amount; - @Nullable public NBTTagCompound tag; + @Nullable public CompoundNBT tag; public FluidStack(Fluid fluid, int amount) { if (fluid == null) throw new IllegalArgumentException("fluid"); @@ -36,16 +36,16 @@ public boolean isFluidEqual(@Nullable FluidStack other) { && (tag == null ? other.tag == null : tag.equals(other.tag)); } - public NBTTagCompound writeToNBT(NBTTagCompound nbt) { - ResourceLocation id = IRegistry.field_212619_h.getKey(fluid); - if (id != null) nbt.setString("FluidName", id.toString()); - nbt.setInt("Amount", amount); - if (tag != null) nbt.setTag("Tag", tag.copy()); + public CompoundNBT writeToNBT(CompoundNBT nbt) { + ResourceLocation id = net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(fluid); + if (id != null) nbt.putString("FluidName", id.toString()); + nbt.putInt("Amount", amount); + if (tag != null) nbt.put("Tag", tag.copy()); return nbt; } @Nullable - public static FluidStack loadFluidStackFromNBT(NBTTagCompound nbt) { + public static FluidStack loadFluidStackFromNBT(CompoundNBT nbt) { if (nbt == null || !nbt.contains("FluidName", 8)) return null; Fluid fluid = Fluids.byName(nbt.getString("FluidName")); if (fluid == null) return null; diff --git a/src/main/java/cyano/poweradvantage/api/fluid/FluidTank.java b/src/main/java/cyano/poweradvantage/api/fluid/FluidTank.java index e2b749a..8d18bf4 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/FluidTank.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/FluidTank.java @@ -3,9 +3,9 @@ import javax.annotation.Nullable; import net.minecraft.fluid.Fluid; -import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.CompoundNBT; -/** Minimal millibucket tank backed by Minecraft 1.13 native fluids. */ +/** Millibucket tank retaining the Advantage fluid API and saved NBT format. */ public class FluidTank { public static final int BUCKET_VOLUME = 1000; @@ -71,13 +71,13 @@ protected void onContentsChanged() {} public FluidTankInfo getInfo() { return new FluidTankInfo(fluid, capacity); } - public NBTTagCompound writeToNBT(NBTTagCompound nbt) { - if (fluid == null) nbt.setBoolean("Empty", true); + public CompoundNBT writeToNBT(CompoundNBT nbt) { + if (fluid == null) nbt.putBoolean("Empty", true); else fluid.writeToNBT(nbt); return nbt; } - public FluidTank readFromNBT(NBTTagCompound nbt) { + public FluidTank readFromNBT(CompoundNBT nbt) { setFluid(nbt.contains("Empty", 1) ? null : FluidStack.loadFluidStackFromNBT(nbt)); return this; } diff --git a/src/main/java/cyano/poweradvantage/api/fluid/IAdvantageFluidHandler.java b/src/main/java/cyano/poweradvantage/api/fluid/IAdvantageFluidHandler.java index e96a49d..54352df 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/IAdvantageFluidHandler.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/IAdvantageFluidHandler.java @@ -3,17 +3,17 @@ import javax.annotation.Nullable; import net.minecraft.fluid.Fluid; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; /** - * Stable millibucket fluid contract used by Advantage machines on Forge 1.13. - * It avoids Forge 25's unfinished transitional FluidStack implementation. + * Stable millibucket fluid contract used by Advantage machines. On Forge 1.14 + * it is also exposed through the native sided fluid capability. */ public interface IAdvantageFluidHandler { - FluidTankInfo[] getTankInfo(@Nullable EnumFacing face); - int fill(@Nullable EnumFacing face, FluidStack resource, boolean doFill); - FluidStack drain(@Nullable EnumFacing face, FluidStack resource, boolean doDrain); - FluidStack drain(@Nullable EnumFacing face, int amount, boolean doDrain); - boolean canFill(@Nullable EnumFacing face, Fluid fluid); - boolean canDrain(@Nullable EnumFacing face, Fluid fluid); + FluidTankInfo[] getTankInfo(@Nullable Direction face); + int fill(@Nullable Direction face, FluidStack resource, boolean doFill); + FluidStack drain(@Nullable Direction face, FluidStack resource, boolean doDrain); + FluidStack drain(@Nullable Direction face, int amount, boolean doDrain); + boolean canFill(@Nullable Direction face, Fluid fluid); + boolean canDrain(@Nullable Direction face, Fluid fluid); } diff --git a/src/main/java/cyano/poweradvantage/api/fluid/InteractiveFluidBlock.java b/src/main/java/cyano/poweradvantage/api/fluid/InteractiveFluidBlock.java index 5584bb4..fd8bb78 100644 --- a/src/main/java/cyano/poweradvantage/api/fluid/InteractiveFluidBlock.java +++ b/src/main/java/cyano/poweradvantage/api/fluid/InteractiveFluidBlock.java @@ -3,42 +3,42 @@ import java.util.function.BiConsumer; import net.minecraft.block.Block; -import net.minecraft.block.BlockFlowingFluid; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.FlowingFluidBlock; +import net.minecraft.block.BlockState; import net.minecraft.entity.Entity; -import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.LivingEntity; import net.minecraft.fluid.FlowingFluid; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.math.BlockPos; import net.minecraft.world.IBlockReader; import net.minecraft.world.World; /** A native 1.13 fluid block which can affect immersed living entities. */ -public class InteractiveFluidBlock extends BlockFlowingFluid { - private final BiConsumer immersionEffect; +public class InteractiveFluidBlock extends FlowingFluidBlock { + private final BiConsumer immersionEffect; private final boolean flammable; public InteractiveFluidBlock(FlowingFluid fluid, Block.Properties properties, boolean flammable, - BiConsumer immersionEffect) { + BiConsumer immersionEffect) { super(fluid, properties); this.flammable = flammable; this.immersionEffect = immersionEffect; } @Override - public void onEntityCollision(IBlockState state, World world, BlockPos pos, Entity entity) { - if (immersionEffect != null && entity instanceof EntityLivingBase) { - immersionEffect.accept(world, (EntityLivingBase) entity); + public void onEntityCollision(BlockState state, World world, BlockPos pos, Entity entity) { + if (immersionEffect != null && entity instanceof LivingEntity) { + immersionEffect.accept(world, (LivingEntity) entity); } } @Override - public int getFlammability(IBlockState state, IBlockReader world, BlockPos pos, EnumFacing face) { + public int getFlammability(BlockState state, IBlockReader world, BlockPos pos, Direction face) { return flammable ? 60 : 0; } @Override - public int getFireSpreadSpeed(IBlockState state, IBlockReader world, BlockPos pos, EnumFacing face) { + public int getFireSpreadSpeed(BlockState state, IBlockReader world, BlockPos pos, Direction face) { return flammable ? 30 : 0; } } diff --git a/src/main/java/cyano/poweradvantage/api/migration/LegacyMigrationApi.java b/src/main/java/cyano/poweradvantage/api/migration/LegacyMigrationApi.java index 0e9c8f3..086b1a5 100644 --- a/src/main/java/cyano/poweradvantage/api/migration/LegacyMigrationApi.java +++ b/src/main/java/cyano/poweradvantage/api/migration/LegacyMigrationApi.java @@ -8,8 +8,8 @@ import java.util.Set; import net.minecraft.block.Block; -import net.minecraft.block.state.IBlockState; -import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.block.BlockState; +import net.minecraft.nbt.CompoundNBT; import net.minecraft.util.ResourceLocation; /** @@ -20,12 +20,12 @@ public final class LegacyMigrationApi { @FunctionalInterface public interface BlockStateCodec { - IBlockState decode(Block currentBlock, int legacyMetadata); + BlockState decode(Block currentBlock, int legacyMetadata); } @FunctionalInterface public interface NbtMigration { - boolean migrate(NBTTagCompound compound); + boolean migrate(CompoundNBT compound); } private static final Set NAMESPACES = new LinkedHashSet<>(); @@ -100,7 +100,7 @@ public static synchronized ResourceLocation itemTarget(ResourceLocation legacyId return target == null ? legacyId : target; } - public static synchronized IBlockState decodeBlockState(ResourceLocation legacyId, Block block, int metadata) { + public static synchronized BlockState decodeBlockState(ResourceLocation legacyId, Block block, int metadata) { BlockStateCodec codec = BLOCK_CODECS.get(legacyId); return codec == null ? block.getDefaultState() : codec.decode(block, metadata & 15); } @@ -129,7 +129,7 @@ public static synchronized boolean isDurableItem(ResourceLocation id) { return DURABLE_ITEMS.contains(id) || DURABLE_ITEMS.contains(itemTarget(id)); } - public static synchronized boolean migrateRegisteredNbt(NBTTagCompound compound) { + public static synchronized boolean migrateRegisteredNbt(CompoundNBT compound) { boolean changed = false; for (NbtMigration migration : NBT_MIGRATIONS.values()) changed |= migration.migrate(compound); return changed; diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/ILightWeightPowerAcceptor.java b/src/main/java/cyano/poweradvantage/api/modsupport/ILightWeightPowerAcceptor.java index ec1f8ab..71c2edf 100644 --- a/src/main/java/cyano/poweradvantage/api/modsupport/ILightWeightPowerAcceptor.java +++ b/src/main/java/cyano/poweradvantage/api/modsupport/ILightWeightPowerAcceptor.java @@ -2,7 +2,7 @@ import cyano.poweradvantage.api.ConduitType; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; /** * This interface is used in the LightWeightPowerRegistry to allow established mods to optionally @@ -21,7 +21,7 @@ public interface ILightWeightPowerAcceptor { * @param inputSide Side through which the power is coming * @return True if you accept this type, false if you reject it. */ - public default boolean canAcceptEnergyType(ConduitType powerType, EnumFacing inputSide) { + public default boolean canAcceptEnergyType(ConduitType powerType, Direction inputSide) { return canAcceptEnergyType(powerType); } diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java index e38b605..6d9865a 100644 --- a/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java +++ b/src/main/java/cyano/poweradvantage/api/modsupport/TileEntityConverter.java @@ -4,7 +4,7 @@ import cyano.poweradvantage.api.PowerRequest; import cyano.poweradvantage.api.simple.TileEntitySimplePowerMachine; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.CompoundNBT; import net.minecraft.tileentity.TileEntityType; import java.util.Arrays; @@ -50,7 +50,7 @@ public void powerUpdate() { // convert from other mod energy if (getOtherEnergy() > 0) { this.addEnergy( - -1 * subtractEnergyFromOther(-1 * delta, type), type); + subtractEnergyFromOther(-1 * delta, type), type); } } if (!Arrays.equals(dataArray, dataArrayOld)) { @@ -174,7 +174,7 @@ public byte getMinimumSinkPriority() { } @Override - public void read(final NBTTagCompound tagRoot) { + public void read(final CompoundNBT tagRoot) { super.read(tagRoot); if (tagRoot.contains("Other", 99)) { this.setOtherEnergy(tagRoot.getDouble("Other")); @@ -182,9 +182,9 @@ public void read(final NBTTagCompound tagRoot) { } @Override - public NBTTagCompound write(final NBTTagCompound tagRoot) { + public CompoundNBT write(final CompoundNBT tagRoot) { super .write(tagRoot); - tagRoot.setDouble("Other", this.getOtherEnergy()); + tagRoot.putDouble("Other", this.getOtherEnergy()); return tagRoot; } diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/rf/BlockRFConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/rf/BlockRFConverter.java index 9e1eb3f..7966297 100644 --- a/src/main/java/cyano/poweradvantage/api/modsupport/rf/BlockRFConverter.java +++ b/src/main/java/cyano/poweradvantage/api/modsupport/rf/BlockRFConverter.java @@ -4,7 +4,7 @@ import cyano.poweradvantage.api.simple.BlockSimplePowerMachine; import net.minecraft.block.material.Material; import net.minecraft.world.IBlockReader; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; @@ -29,6 +29,6 @@ public TileEntityRFConverter createNewTileEntity(IBlockReader world) { @Override public boolean isPowerSink(ConduitType powerType) { return true; } @Override public boolean isPowerSource(ConduitType powerType) { return true; } - @Override public boolean hasComparatorInputOverride(IBlockState state) { return false; } - @Override public int getComparatorInputOverride(IBlockState state, World world, BlockPos pos) { return 0; } + @Override public boolean hasComparatorInputOverride(BlockState state) { return false; } + @Override public int getComparatorInputOverride(BlockState state, World world, BlockPos pos) { return 0; } } diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFConverter.java index c1cc285..4d3757a 100644 --- a/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFConverter.java +++ b/src/main/java/cyano/poweradvantage/api/modsupport/rf/TileEntityRFConverter.java @@ -7,7 +7,7 @@ import cyano.poweradvantage.api.modsupport.TileEntityConverter; import net.minecraft.tileentity.TileEntity; import net.minecraft.tileentity.TileEntityType; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.energy.CapabilityEnergy; @@ -18,7 +18,7 @@ public class TileEntityRFConverter extends TileEntityConverter implements IEnergyStorage { private final int forgeBufferSize; private final int halfBuffer; - private final LazyOptional energyCapability = LazyOptional.of(() -> this); + private LazyOptional energyCapability = LazyOptional.of(() -> this); private int forgeBuffer; protected TileEntityRFConverter(TileEntityType tileType, ConduitType type, @@ -31,8 +31,8 @@ protected TileEntityRFConverter(TileEntityType tileType, ConduitType type, @Override public void tickUpdate(boolean server) { if (!server) return; - int index = (int) (getWorld().getGameTime() % EnumFacing.values().length); - EnumFacing side = EnumFacing.values()[index]; + int index = (int) (getWorld().getGameTime() % Direction.values().length); + Direction side = Direction.values()[index]; TileEntity neighbor = getWorld().getTileEntity(getPos().offset(side)); if (neighbor == null) return; neighbor.getCapability(CapabilityEnergy.ENERGY, side.getOpposite()).ifPresent(storage -> { @@ -64,8 +64,18 @@ public void tickUpdate(boolean server) { @Override public boolean canReceive() { return true; } @Override - public LazyOptional getCapability(@Nonnull Capability capability, @Nullable EnumFacing side) { - return capability == CapabilityEnergy.ENERGY ? energyCapability.cast() : super.getCapability(capability, side); + public LazyOptional getCapability(@Nonnull Capability capability, @Nullable Direction side) { + return !isRemoved() && capability == CapabilityEnergy.ENERGY ? energyCapability.cast() : super.getCapability(capability, side); + } + + @Override protected void invalidateCaps() { + super.invalidateCaps(); + energyCapability.invalidate(); + } + + @Override public void validate() { + super.validate(); + if (!energyCapability.isPresent()) energyCapability = LazyOptional.of(() -> this); } @Override public double getOtherEnergy() { return forgeBuffer; } diff --git a/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/TileEntityTRElectricityConverter.java b/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/TileEntityTRElectricityConverter.java index 725931a..2eed190 100644 --- a/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/TileEntityTRElectricityConverter.java +++ b/src/main/java/cyano/poweradvantage/api/modsupport/techreborn/TileEntityTRElectricityConverter.java @@ -1,7 +1,7 @@ package cyano.poweradvantage.api.modsupport.techreborn; import cyano.poweradvantage.api.ConduitType; -import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.PlayerEntity; import zone.moddev.mc.poweradvantage.init.Entities; /** @@ -14,7 +14,7 @@ public TileEntityTRElectricityConverter() { } @Override - public boolean isUsableByPlayer(EntityPlayer player) { + public boolean isUsableByPlayer(PlayerEntity player) { return true; } } diff --git a/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidConduit.java b/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidConduit.java index 055926b..93aba87 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidConduit.java +++ b/src/main/java/cyano/poweradvantage/api/simple/BlockSimpleFluidConduit.java @@ -6,12 +6,12 @@ import zone.moddev.mc.poweradvantage.util.PowerHelper; import net.minecraft.block.Block; import net.minecraft.block.material.Material; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.item.BlockItemUseContext; import net.minecraft.pathfinding.PathType; import net.minecraft.state.BooleanProperty; import net.minecraft.state.StateContainer; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.shapes.VoxelShape; import net.minecraft.util.math.shapes.VoxelShapes; @@ -43,31 +43,31 @@ public BlockSimpleFluidConduit(Material material, float hardness, float pipeRadi @Override public boolean isPowerSource(ConduitType conduitType) { return false; } @Override - protected void fillStateContainer(StateContainer.Builder builder) { + protected void fillStateContainer(StateContainer.Builder builder) { builder.add(WEST, DOWN, SOUTH, EAST, UP, NORTH); } @Override - public IBlockState getStateForPlacement(BlockItemUseContext context) { + public BlockState getStateForPlacement(BlockItemUseContext context) { return connectedState(getDefaultState(), context.getWorld(), context.getPos()); } @Override - public IBlockState updatePostPlacement(IBlockState state, EnumFacing facing, IBlockState facingState, + public BlockState updatePostPlacement(BlockState state, Direction facing, BlockState facingState, IWorld world, BlockPos currentPos, BlockPos facingPos) { return state.with(property(facing), canConnectTo(world, currentPos, state, facing)); } - private IBlockState connectedState(IBlockState state, IBlockReader world, BlockPos pos) { - return state.with(WEST, canConnectTo(world, pos, state, EnumFacing.WEST)) - .with(DOWN, canConnectTo(world, pos, state, EnumFacing.DOWN)) - .with(SOUTH, canConnectTo(world, pos, state, EnumFacing.SOUTH)) - .with(EAST, canConnectTo(world, pos, state, EnumFacing.EAST)) - .with(UP, canConnectTo(world, pos, state, EnumFacing.UP)) - .with(NORTH, canConnectTo(world, pos, state, EnumFacing.NORTH)); + private BlockState connectedState(BlockState state, IBlockReader world, BlockPos pos) { + return state.with(WEST, canConnectTo(world, pos, state, Direction.WEST)) + .with(DOWN, canConnectTo(world, pos, state, Direction.DOWN)) + .with(SOUTH, canConnectTo(world, pos, state, Direction.SOUTH)) + .with(EAST, canConnectTo(world, pos, state, Direction.EAST)) + .with(UP, canConnectTo(world, pos, state, Direction.UP)) + .with(NORTH, canConnectTo(world, pos, state, Direction.NORTH)); } - private static BooleanProperty property(EnumFacing facing) { + private static BooleanProperty property(Direction facing) { switch (facing) { case DOWN: return DOWN; case UP: return UP; @@ -79,8 +79,8 @@ private static BooleanProperty property(EnumFacing facing) { } } - protected boolean canConnectTo(IBlockReader world, BlockPos pos, IBlockState state, - EnumFacing face) { + protected boolean canConnectTo(IBlockReader world, BlockPos pos, BlockState state, + Direction face) { for (ConduitType type : getTypes()) { if (PowerHelper.areConnectable(new PowerConnectorContext(type, world, state, pos, face))) { return true; @@ -90,12 +90,12 @@ protected boolean canConnectTo(IBlockReader world, BlockPos pos, IBlockState sta } @Override - public VoxelShape getShape(IBlockState state, IBlockReader world, BlockPos pos) { + public VoxelShape getShape(BlockState state, IBlockReader world, BlockPos pos, net.minecraft.util.math.shapes.ISelectionContext context) { double min = 8.0D - pipeRadius * 16.0D; double max = 8.0D + pipeRadius * 16.0D; VoxelShape shape = Block.makeCuboidShape(min, min, min, max, max, max); boolean connected = false; - for (EnumFacing facing : EnumFacing.values()) { + for (Direction facing : Direction.values()) { if (!state.get(property(facing))) continue; connected = true; switch (facing) { @@ -114,9 +114,9 @@ public VoxelShape getShape(IBlockState state, IBlockReader world, BlockPos pos) return shape; } - @Override public boolean isFullCube(IBlockState state) { return false; } - @Override public boolean allowsMovement(IBlockState state, IBlockReader world, BlockPos pos, PathType type) { return false; } + public boolean isFullCube(BlockState state) { return false; } + @Override public boolean allowsMovement(BlockState state, IBlockReader world, BlockPos pos, PathType type) { return false; } /** Legacy metadata bridge retained for migration tests. */ - public int getMetaFromState(IBlockState state) { return 0; } + public int getMetaFromState(BlockState state) { return 0; } } diff --git a/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerConduit.java b/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerConduit.java index 8fe0d58..9ebc29b 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerConduit.java +++ b/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerConduit.java @@ -6,12 +6,12 @@ import zone.moddev.mc.poweradvantage.util.PowerHelper; import net.minecraft.block.Block; import net.minecraft.block.material.Material; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.item.BlockItemUseContext; import net.minecraft.pathfinding.PathType; import net.minecraft.state.BooleanProperty; import net.minecraft.state.StateContainer; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.shapes.VoxelShape; import net.minecraft.util.math.shapes.VoxelShapes; @@ -53,31 +53,31 @@ public boolean canAcceptConnection(PowerConnectorContext connection) { @Override public boolean isPowerSource(ConduitType conduitType) { return false; } @Override - protected void fillStateContainer(StateContainer.Builder builder) { + protected void fillStateContainer(StateContainer.Builder builder) { builder.add(WEST, DOWN, SOUTH, EAST, UP, NORTH); } @Override - public IBlockState getStateForPlacement(BlockItemUseContext context) { + public BlockState getStateForPlacement(BlockItemUseContext context) { return connectedState(getDefaultState(), context.getWorld(), context.getPos()); } @Override - public IBlockState updatePostPlacement(IBlockState state, EnumFacing facing, IBlockState facingState, + public BlockState updatePostPlacement(BlockState state, Direction facing, BlockState facingState, IWorld world, BlockPos currentPos, BlockPos facingPos) { return state.with(property(facing), canConnectTo(world, currentPos, state, facing, facingPos)); } - private IBlockState connectedState(IBlockState state, IBlockReader world, BlockPos pos) { - return state.with(WEST, canConnectTo(world, pos, state, EnumFacing.WEST, pos.west())) - .with(DOWN, canConnectTo(world, pos, state, EnumFacing.DOWN, pos.down())) - .with(SOUTH, canConnectTo(world, pos, state, EnumFacing.SOUTH, pos.south())) - .with(EAST, canConnectTo(world, pos, state, EnumFacing.EAST, pos.east())) - .with(UP, canConnectTo(world, pos, state, EnumFacing.UP, pos.up())) - .with(NORTH, canConnectTo(world, pos, state, EnumFacing.NORTH, pos.north())); + private BlockState connectedState(BlockState state, IBlockReader world, BlockPos pos) { + return state.with(WEST, canConnectTo(world, pos, state, Direction.WEST, pos.west())) + .with(DOWN, canConnectTo(world, pos, state, Direction.DOWN, pos.down())) + .with(SOUTH, canConnectTo(world, pos, state, Direction.SOUTH, pos.south())) + .with(EAST, canConnectTo(world, pos, state, Direction.EAST, pos.east())) + .with(UP, canConnectTo(world, pos, state, Direction.UP, pos.up())) + .with(NORTH, canConnectTo(world, pos, state, Direction.NORTH, pos.north())); } - private static BooleanProperty property(EnumFacing facing) { + private static BooleanProperty property(Direction facing) { switch (facing) { case DOWN: return DOWN; case UP: return UP; @@ -89,8 +89,8 @@ private static BooleanProperty property(EnumFacing facing) { } } - protected boolean canConnectTo(IBlockReader world, BlockPos thisBlock, IBlockState state, - EnumFacing face, BlockPos otherBlock) { + protected boolean canConnectTo(IBlockReader world, BlockPos thisBlock, BlockState state, + Direction face, BlockPos otherBlock) { for (ConduitType type : types) { if (PowerHelper.areConnectable(new PowerConnectorContext(type, world, state, thisBlock, face))) { return true; @@ -100,12 +100,12 @@ protected boolean canConnectTo(IBlockReader world, BlockPos thisBlock, IBlockSta } @Override - public VoxelShape getShape(IBlockState state, IBlockReader world, BlockPos pos) { + public VoxelShape getShape(BlockState state, IBlockReader world, BlockPos pos, net.minecraft.util.math.shapes.ISelectionContext context) { double min = 8.0D - pipeRadius * 16.0D; double max = 8.0D + pipeRadius * 16.0D; VoxelShape shape = Block.makeCuboidShape(min, min, min, max, max, max); boolean connected = false; - for (EnumFacing facing : EnumFacing.values()) { + for (Direction facing : Direction.values()) { if (!state.get(property(facing))) continue; connected = true; switch (facing) { @@ -124,9 +124,9 @@ public VoxelShape getShape(IBlockState state, IBlockReader world, BlockPos pos) return shape; } - @Override public boolean isFullCube(IBlockState state) { return false; } - @Override public boolean allowsMovement(IBlockState state, IBlockReader world, BlockPos pos, PathType type) { return false; } + public boolean isFullCube(BlockState state) { return false; } + @Override public boolean allowsMovement(BlockState state, IBlockReader world, BlockPos pos, PathType type) { return false; } /** Legacy metadata bridge retained for migration tests. */ - public int getMetaFromState(IBlockState state) { return 0; } + public int getMetaFromState(BlockState state) { return 0; } } diff --git a/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerMachine.java b/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerMachine.java index 9ad474b..3c3d012 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerMachine.java +++ b/src/main/java/cyano/poweradvantage/api/simple/BlockSimplePowerMachine.java @@ -2,20 +2,20 @@ import cyano.poweradvantage.api.*; import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry; -import net.minecraft.block.BlockHorizontal; +import net.minecraft.block.HorizontalBlock; import net.minecraft.block.material.Material; import net.minecraft.state.IProperty; import net.minecraft.state.DirectionProperty; import net.minecraft.state.StateContainer; -import net.minecraft.block.state.IBlockState; -import net.minecraft.entity.EntityLivingBase; +import net.minecraft.block.BlockState; +import net.minecraft.entity.LivingEntity; import net.minecraft.inventory.IInventory; import net.minecraft.inventory.InventoryHelper; import net.minecraft.item.Item; import net.minecraft.item.BlockItemUseContext; import net.minecraft.item.ItemStack; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.Mirror; import net.minecraft.util.Rotation; import net.minecraft.util.math.BlockPos; @@ -46,7 +46,7 @@ public abstract class BlockSimplePowerMachine extends GUIBlock implements ITyped /** * Blockstate property indicating which direction the block is facing */ - public static final DirectionProperty FACING = BlockHorizontal.HORIZONTAL_FACING; + public static final DirectionProperty FACING = HorizontalBlock.HORIZONTAL_FACING; /** * Standard constructor for a machine block. Remember to set the GUI ID so that a GUI can pop-up * when the player interacts with this block. @@ -61,7 +61,7 @@ public abstract class BlockSimplePowerMachine extends GUIBlock implements ITyped public BlockSimplePowerMachine(Material blockMaterial, float hardness, ConduitType... energyType){ super(blockMaterial, hardness); this.type = energyType; - this.setDefaultState(this.getStateContainer().getBaseState().with(FACING, EnumFacing.NORTH)); + this.setDefaultState(this.getStateContainer().getBaseState().with(FACING, Direction.NORTH)); } /** @@ -71,7 +71,7 @@ public BlockSimplePowerMachine(Material blockMaterial, float hardness, ConduitTy protected BlockSimplePowerMachine(Block.Properties properties, ConduitType... energyType) { super(properties); this.type = energyType; - this.setDefaultState(this.getStateContainer().getBaseState().with(FACING, EnumFacing.NORTH)); + this.setDefaultState(this.getStateContainer().getBaseState().with(FACING, Direction.NORTH)); } /** @@ -87,9 +87,9 @@ protected BlockSimplePowerMachine(Block.Properties properties, ConduitType... en * Override of default block behavior */ @Override - public void onBlockAdded(final IBlockState state, final World world, final BlockPos coord, - final IBlockState oldState) { - super.onBlockAdded(state, world, coord, oldState); + public void onBlockAdded(final BlockState state, final World world, final BlockPos coord, + final BlockState oldState, final boolean isMoving) { + super.onBlockAdded(state, world, coord, oldState, isMoving); this.setDefaultFacing(world, coord, state); ConduitRegistry.getInstance().conduitBlockPlacedEvent(world, world.dimension.getType().getId(), coord, getTypes()); } @@ -97,7 +97,7 @@ public void onBlockAdded(final IBlockState state, final World world, final Block * This method is called when the block is removed from the world by an entity. */ @Override - public void onReplaced(IBlockState state, World w, BlockPos coord, IBlockState newState, + public void onReplaced(BlockState state, World w, BlockPos coord, BlockState newState, boolean isMoving) { if (state.getBlock() != newState.getBlock()) { ConduitRegistry.getInstance().conduitBlockRemovedEvent(w, w.dimension.getType().getId(), coord, getTypes()); @@ -144,7 +144,7 @@ public boolean canAcceptConnection(PowerConnectorContext connection){ * Creates the blockstate of this block when it is placed in the world */ @Override - public IBlockState getStateForPlacement(final BlockItemUseContext context) { + public BlockState getStateForPlacement(final BlockItemUseContext context) { return this.getDefaultState().with(FACING, context.getPlacementHorizontalFacing().getOpposite()); } @@ -152,8 +152,8 @@ public IBlockState getStateForPlacement(final BlockItemUseContext context) { * Creates the blockstate of this block when it is placed in the world */ @Override - public void onBlockPlacedBy(final World world, final BlockPos coord, final IBlockState bs, - final EntityLivingBase placer, final ItemStack srcItemStack) { + public void onBlockPlacedBy(final World world, final BlockPos coord, final BlockState bs, + final LivingEntity placer, final ItemStack srcItemStack) { world.setBlockState(coord, bs.with((IProperty) FACING, (Comparable)placer.getHorizontalFacing().getOpposite()), 2); if (srcItemStack.hasDisplayName()) { final TileEntity tileEntity = world.getTileEntity(coord); @@ -173,26 +173,26 @@ public void onBlockPlacedBy(final World world, final BlockPos coord, final IBloc * @param coord Block coordinate * @param state Block state */ - protected void setDefaultFacing(final World w, final BlockPos coord, final IBlockState state) { + protected void setDefaultFacing(final World w, final BlockPos coord, final BlockState state) { if (w.isRemote) { return; } - final IBlockState block = w.getBlockState(coord.north()); - final IBlockState block2 = w.getBlockState(coord.south()); - final IBlockState block3 = w.getBlockState(coord.west()); - final IBlockState block4 = w.getBlockState(coord.east()); - EnumFacing enumFacing = (EnumFacing)state .get(FACING); - if (enumFacing == EnumFacing.NORTH && block.isFullCube() && !block2.isFullCube()) { - enumFacing = EnumFacing.SOUTH; + final BlockState block = w.getBlockState(coord.north()); + final BlockState block2 = w.getBlockState(coord.south()); + final BlockState block3 = w.getBlockState(coord.west()); + final BlockState block4 = w.getBlockState(coord.east()); + Direction enumFacing = (Direction)state .get(FACING); + if (enumFacing == Direction.NORTH && block.isNormalCube(w, coord.north()) && !block2.isNormalCube(w, coord.south())) { + enumFacing = Direction.SOUTH; } - else if (enumFacing == EnumFacing.SOUTH && block2.isFullCube() && !block.isFullCube()) { - enumFacing = EnumFacing.NORTH; + else if (enumFacing == Direction.SOUTH && block2.isNormalCube(w, coord.south()) && !block.isNormalCube(w, coord.north())) { + enumFacing = Direction.NORTH; } - else if (enumFacing == EnumFacing.WEST && block3.isFullCube() && !block4.isFullCube()) { - enumFacing = EnumFacing.EAST; + else if (enumFacing == Direction.WEST && block3.isNormalCube(w, coord.west()) && !block4.isNormalCube(w, coord.east())) { + enumFacing = Direction.EAST; } - else if (enumFacing == EnumFacing.EAST && block4.isFullCube() && !block3.isFullCube()) { - enumFacing = EnumFacing.WEST; + else if (enumFacing == Direction.EAST && block4.isNormalCube(w, coord.east()) && !block3.isNormalCube(w, coord.west())) { + enumFacing = Direction.WEST; } w.setBlockState(coord, state.with((IProperty) FACING, (Comparable)enumFacing), 2); } @@ -214,7 +214,7 @@ else if (enumFacing == EnumFacing.EAST && block4.isFullCube() && !block3.isFullC * false otherwise */ @Override - public abstract boolean hasComparatorInputOverride(IBlockState state); + public abstract boolean hasComparatorInputOverride(BlockState state); /** * This method gets the output for a redstone comparator placed against this @@ -234,16 +234,16 @@ else if (enumFacing == EnumFacing.EAST && block4.isFullCube() && !block3.isFullC * @return a number from 0 to 15 */ @Override - public abstract int getComparatorInputOverride(IBlockState state, final World world, final BlockPos coord); + public abstract int getComparatorInputOverride(BlockState state, final World world, final BlockPos coord); /** * Converts metadata into blockstate */ - public IBlockState getStateFromMeta(final int metaValue) { - EnumFacing enumFacing = EnumFacing.byIndex(metaValue); - if (enumFacing.getAxis() == EnumFacing.Axis.Y) { - enumFacing = EnumFacing.NORTH; + public BlockState getStateFromMeta(final int metaValue) { + Direction enumFacing = Direction.byIndex(metaValue); + if (enumFacing.getAxis() == Direction.Axis.Y) { + enumFacing = Direction.NORTH; } return this.getDefaultState().with( FACING, enumFacing); } @@ -251,26 +251,26 @@ public IBlockState getStateFromMeta(final int metaValue) { /** * Converts blockstate into metadata */ - public int getMetaFromState(final IBlockState bs) { - return ((EnumFacing)bs .get( FACING)).getIndex(); + public int getMetaFromState(final BlockState bs) { + return ((Direction)bs .get( FACING)).getIndex(); } /** * Creates a blockstate */ @Override - protected void fillStateContainer(StateContainer.Builder builder) { + protected void fillStateContainer(StateContainer.Builder builder) { builder.add(FACING); } @Override - public IBlockState rotate(IBlockState state, Rotation rot) { - return state.with(FACING, rot.rotate((EnumFacing)state .get(FACING))); + public BlockState rotate(BlockState state, Rotation rot) { + return state.with(FACING, rot.rotate((Direction)state .get(FACING))); } @Override - public IBlockState mirror(IBlockState state, Mirror mirrorIn) { - return state.rotate(mirrorIn.toRotation((EnumFacing)state .get(FACING))); + public BlockState mirror(BlockState state, Mirror mirrorIn) { + return state.rotate(mirrorIn.toRotation((Direction)state .get(FACING))); } /** diff --git a/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java b/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java index 6bc2cef..1e26dfc 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java +++ b/src/main/java/cyano/poweradvantage/api/simple/SimpleMachineGUI.java @@ -4,17 +4,20 @@ import zone.moddev.mc.poweradvantage.PowerAdvantage; import zone.moddev.mc.poweradvantage.math.Integer2D; import zone.moddev.mc.poweradvantage.registry.ITileEntityGUI; -import net.minecraft.client.gui.inventory.GuiContainer; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.InventoryPlayer; +import net.minecraft.client.gui.screen.inventory.ContainerScreen; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.Slot; +import net.minecraft.inventory.container.Slot; import net.minecraft.item.ItemStack; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; -import org.lwjgl.opengl.GL11; +import com.mojang.blaze3d.platform.GlStateManager; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.StringTextComponent; +import zone.moddev.mc.poweradvantage.registry.MachineGUIRegistry; import java.util.HashMap; import java.util.Map; @@ -99,8 +102,8 @@ public SimpleMachineGUI(String guiImage, Integer2D... inventorySlotCoordinates){ * @param guiContainer This is the instance of GUIContainer that is drawing * the GUI. You need to use it to draw on the screen. For example:
-guiContainer.mc.getTextureManager().bindTexture(arrowTexture);
-guiContainer.drawTexturedModalRect(x+79, y+35, 0, 0, arrowLength, 17); // x, y, textureOffsetX, textureOffsetY, width, height)
+guiContainer.minecraft.getTextureManager().bindTexture(arrowTexture);
+guiContainer.blit(x+79, y+35, 0, 0, arrowLength, 17); // x, y, textureOffsetX, textureOffsetY, width, height)
 	   
* @param x This is the x coordinate (in pixels) from the top-left corner of * the GUI @@ -111,40 +114,49 @@ public SimpleMachineGUI(String guiImage, Integer2D... inventorySlotCoordinates){ public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int x, int y, float z){} /** - * Gets an instance of net.minecraft.inventory.Container for the + * Gets an instance of net.minecraft.inventory.container.Container for the * server-side portion of a GUI interaction. * @param e The TileEntity that is presenting this GUI * @param player The player that is going to use the GUI - * @return An instance of net.minecraft.inventory.Container + * @return An instance of net.minecraft.inventory.container.Container */ @Override - public net.minecraft.inventory.Container getContainer(TileEntity e, EntityPlayer player) { + public net.minecraft.inventory.container.Container getContainer(TileEntity e, PlayerEntity player) { return new Container(player.inventory,(IInventory)e); } + public Container createContainer(int windowId, PlayerInventory player, IInventory inventory) { + return new Container(windowId, player, inventory); + } + /** - * Gets an instance of net.minecraft.client.gui.inventory.GuiContainer for + * Gets an instance of net.minecraft.client.gui.screen.inventory.ContainerScreen for * the client-side portion of a GUI interaction. * @param e The TileEntity that is presenting this GUI * @param player The player that is going to use the GUI - * @return An instance of net.minecraft.client.gui.inventory.GuiContainer + * @return An instance of net.minecraft.client.gui.screen.inventory.ContainerScreen */ @Override @OnlyIn(Dist.CLIENT) - public GuiContainer getContainerGUI(TileEntity e, EntityPlayer player) { + public ContainerScreen getContainerGUI(TileEntity e, PlayerEntity player) { return new GUIContainer(player.inventory,(IInventory)e); } /** * Member class used to provide an instance of - * net.minecraft.inventory.Container to fulfill the requirements of the + * net.minecraft.inventory.container.Container to fulfill the requirements of the * interface ITileEntityGUI. */ - public class Container extends net.minecraft.inventory.Container{ + public class Container extends net.minecraft.inventory.container.Container{ private final IInventory entity; - public Container(InventoryPlayer playerItems, IInventory entity){ + public Container(PlayerInventory playerItems, IInventory entity){ + this(0, playerItems, entity); + } + + public Container(int windowId, PlayerInventory playerItems, IInventory entity) { + super(MachineGUIRegistry.TYPE, windowId); this.entity = entity; int index = 0; for(Integer2D pt : inventorySlotCoordinates){ @@ -158,13 +170,18 @@ public Container(InventoryPlayer playerItems, IInventory entity){ } bindPlayerInventory(playerItems, 140); } + + @OnlyIn(Dist.CLIENT) + public GUIContainer createScreen(PlayerInventory inventory, ITextComponent title) { + return new GUIContainer(this, inventory, title); + } @Override - public boolean canInteractWith(EntityPlayer entityplayer) { + public boolean canInteractWith(PlayerEntity entityplayer) { return entity.isUsableByPlayer(entityplayer); } - protected void bindPlayerInventory(InventoryPlayer inventoryPlayer, int yOffset) { + protected void bindPlayerInventory(PlayerInventory inventoryPlayer, int yOffset) { for (int i = 0; i < 3; i++) { for (int j = 0; j < 9; j++) { addSlot(new Slot(inventoryPlayer, j + i * 9 + 9, 8 + j * 18, yOffset + i * 18)); @@ -176,9 +193,9 @@ protected void bindPlayerInventory(InventoryPlayer inventoryPlayer, int yOffset) } @Override - public ItemStack transferStackInSlot(EntityPlayer player, int slot) { + public ItemStack transferStackInSlot(PlayerEntity player, int slot) { int hostSize = inventorySlotCoordinates.length; - ItemStack stack = null; + ItemStack stack = ItemStack.EMPTY; Slot slotObject = (Slot) inventorySlots.get(slot); //null checks and checks if the item can be stacked (maxStackSize > 1) if (slotObject != null && slotObject.getHasStack()) { @@ -188,22 +205,22 @@ public ItemStack transferStackInSlot(EntityPlayer player, int slot) { //merges the item into player inventory since its in the tileEntity if (slot < hostSize) { if (!this.mergeItemStack(stackInSlot, hostSize, 36+hostSize, true)) { - return null; + return ItemStack.EMPTY; } } //places it into the tileEntity if possible since it's in the player inventory else if (!this.mergeItemStack(stackInSlot, 0, hostSize, false)) { - return null; + return ItemStack.EMPTY; } if (stackInSlot.isEmpty()) { - slotObject.putStack(null); + slotObject.putStack(ItemStack.EMPTY); } else { slotObject.onSlotChanged(); } if (stackInSlot.getCount() == stack.getCount()) { - return null; + return ItemStack.EMPTY; } slotObject.onTake(player, stackInSlot); } @@ -214,39 +231,45 @@ else if (!this.mergeItemStack(stackInSlot, 0, hostSize, false)) { /** * Member class used to provide an instance of - * net.minecraft.client.gui.inventory.GuiContainer to fulfill the + * net.minecraft.client.gui.screen.inventory.ContainerScreen to fulfill the * requirements of the interface ITileEntityGUI. */ @OnlyIn(Dist.CLIENT) - public class GUIContainer extends net.minecraft.client.gui.inventory.GuiContainer{ + public class GUIContainer extends net.minecraft.client.gui.screen.inventory.ContainerScreen{ private final Object entity; - public GUIContainer(InventoryPlayer playerItems, IInventory entity) { - super(new Container(playerItems,entity)); + + public net.minecraft.client.Minecraft getMinecraft() { return minecraft; } + public GUIContainer(PlayerInventory playerItems, IInventory entity) { + this(new Container(playerItems, entity), playerItems, new StringTextComponent("Machine")); + } + + public GUIContainer(Container container, PlayerInventory playerItems, ITextComponent title) { + super(container, playerItems, title); //System.out.println("Size: "+this.xSize+"x"+this.ySize+" at coordinate ("+this.guiLeft+","+this.guiTop+")"); this.xSize = guiWidth; this.ySize = guiHeight; - this.entity = entity; + this.entity = container.entity; } @Override protected void drawGuiContainerBackgroundLayer(float f, int i, int j) { - GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); - this.mc.getTextureManager().bindTexture(playerInventoryBox); + GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F); + this.minecraft.getTextureManager().bindTexture(playerInventoryBox); int x = (width - xSize) / 2; int y = (height - ySize) / 2; final int playerInventoryWidth = 176; final int playerInventoryHeight = 222; - this.drawTexturedModalRect(x, y, 0, 0, playerInventoryWidth, playerInventoryHeight); // x, y, textureOffsetX, textureOffsetY, width, height) - this.mc.getTextureManager().bindTexture(guiDisplayImage); - this.drawTexturedModalRect(x, y, 0, 0, guiWidth, guiHeight); // x, y, textureOffsetX, textureOffsetY, width, height) - drawGUIDecorations(entity, this, x, y, this.zLevel); + this.blit(x, y, 0, 0, playerInventoryWidth, playerInventoryHeight); + this.minecraft.getTextureManager().bindTexture(guiDisplayImage); + this.blit(x, y, 0, 0, guiWidth, guiHeight); + drawGUIDecorations(entity, this, x, y, this.blitOffset); } /** * Gets the z-coordinate of this GUI element * @return screen depth coordinate */ public float getZLevel(){ - return this.zLevel; + return this.blitOffset; } /** * Gets the width of the GUI @@ -286,7 +309,7 @@ public int getTop(){ * inventory slot */ public static class SlotContext { - public final InventoryPlayer playerInventory; + public final PlayerInventory playerInventory; public final IInventory machineInventory; public final int slotIndex; public final int screenPositionX; @@ -302,7 +325,7 @@ public static class SlotContext { * @param screenPositionY coordinate position on screen */ public SlotContext( - final InventoryPlayer playerInventory, + final PlayerInventory playerInventory, final IInventory machineInventory, final int slotIndex, final int screenPositionX, diff --git a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java index 351f3fd..a10227b 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java +++ b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimpleFluidMachine.java @@ -7,18 +7,18 @@ import cyano.poweradvantage.api.fluid.FluidRequest; import zone.moddev.mc.poweradvantage.init.Fluids; import zone.moddev.mc.poweradvantage.util.FluidIdHelper; -import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.inventory.ISidedInventory; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.ListNBT; import net.minecraft.network.NetworkManager; -import net.minecraft.network.play.server.SPacketUpdateTileEntity; +import net.minecraft.network.play.server.SUpdateTileEntityPacket; import net.minecraft.tileentity.TileEntityType; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.text.ITextComponent; -import net.minecraft.util.text.TextComponentString; -import net.minecraft.util.text.TextComponentTranslation; +import net.minecraft.util.text.StringTextComponent; +import net.minecraft.util.text.TranslationTextComponent; import net.minecraft.fluid.Fluid; import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.FluidTank; @@ -37,7 +37,7 @@ * */ @SuppressWarnings("deprecation") -public abstract class TileEntitySimpleFluidMachine extends FluidPoweredEntity implements ISidedInventory{ +public abstract class TileEntitySimpleFluidMachine extends FluidPoweredEntity implements ISidedInventory, net.minecraft.util.INameable { /** * This FluidTank holds the fluids for this machine */ @@ -147,7 +147,7 @@ public void onDataFieldUpdate() { int fluidID = dataFields[DATAFIELD_FLUID_ID]; int fluidVolume = dataFields[DATAFIELD_FLUID_VOLUME]; if(fluidVolume <= 0){ - getTank().setFluid(new FluidStack(net.minecraft.init.Fluids.WATER,0)); + getTank().setFluid(new FluidStack(net.minecraft.fluid.Fluids.WATER,0)); } else { Fluid fluid = FluidIdHelper.getFluid(fluidID); if (fluid != null) { @@ -178,7 +178,7 @@ public void onDataFieldUpdate() { */ public void prepareDataFieldsForSync(){ if(getTank().getFluid() == null || getTank().getFluidAmount() <= 0){ - dataFields[DATAFIELD_FLUID_ID] = FluidIdHelper.getFluidId(net.minecraft.init.Fluids.WATER); + dataFields[DATAFIELD_FLUID_ID] = FluidIdHelper.getFluidId(net.minecraft.fluid.Fluids.WATER); dataFields[DATAFIELD_FLUID_VOLUME] = 0; } else { dataFields[DATAFIELD_FLUID_ID] = FluidIdHelper.getFluidId(getTank().getFluid().getFluid()); @@ -263,16 +263,16 @@ protected int transmitFluidToConsumers(FluidStack available, byte minumimPriorit * @param tagRoot The root of the NBT */ @Override - public void read(final NBTTagCompound tagRoot) { + public void read(final CompoundNBT tagRoot) { super.read(tagRoot); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ for(int i = 0; i < inventory.length; i++){ inventory[i] = null; } - final NBTTagList nbttaglist = tagRoot.getList("Items", 10); + final ListNBT nbttaglist = tagRoot.getList("Items", 10); for (int i = 0; i < nbttaglist.size() && i < inventory.length; ++i) { - final NBTTagCompound nbttagcompound1 = nbttaglist .getCompound(i); + final CompoundNBT nbttagcompound1 = nbttaglist .getCompound(i); final byte n = nbttagcompound1.getByte("Slot"); if (n >= 0 && n < inventory.length) { ItemStack stack = ItemStack.read(nbttagcompound1); @@ -292,23 +292,23 @@ public void read(final NBTTagCompound tagRoot) { * @param tagRoot The root of the NBT */ @Override - public NBTTagCompound write(final NBTTagCompound tagRoot) { + public CompoundNBT write(final CompoundNBT tagRoot) { super .write(tagRoot); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ - final NBTTagList nbttaglist = new NBTTagList(); + final ListNBT nbttaglist = new ListNBT(); for (int i = 0; i < inventory.length; ++i) { if (inventory[i] != null && !inventory[i].isEmpty()) { - final NBTTagCompound nbttagcompound1 = new NBTTagCompound(); - nbttagcompound1.setByte("Slot", (byte)i); + final CompoundNBT nbttagcompound1 = new CompoundNBT(); + nbttagcompound1.putByte("Slot", (byte)i); inventory[i] .write(nbttagcompound1); nbttaglist .add(nbttagcompound1); } } - tagRoot.setTag("Items", nbttaglist); + tagRoot.put("Items", nbttaglist); } if (this.hasCustomName()) { - tagRoot.setString("CustomName", this.customName); + tagRoot.putString("CustomName", this.customName); } return tagRoot; } @@ -393,14 +393,14 @@ public void setCustomInventoryName(final String newName) { * @return A synchronization NBT holding values from the * getDataFieldArray() array */ - public NBTTagCompound createDataFieldUpdateTag(){ + public CompoundNBT createDataFieldUpdateTag(){ this.prepareDataFieldsForSync(); int[] dataFields = this.getDataFieldArray(); - NBTTagCompound nbtTag = new NBTTagCompound(); - NBTTagCompound tankTag = new NBTTagCompound(); + CompoundNBT nbtTag = new CompoundNBT(); + CompoundNBT tankTag = new CompoundNBT(); getTank().writeToNBT(tankTag); - nbtTag.setTag("Tank", tankTag); - nbtTag.setIntArray("[]", dataFields); + nbtTag.put("Tank", tankTag); + nbtTag.putIntArray("[]", dataFields); return nbtTag; } @@ -411,9 +411,9 @@ public NBTTagCompound createDataFieldUpdateTag(){ * @param tag A synchronization NBT holding values from the * getDataFieldArray() array */ - public void readDataFieldUpdateTag(NBTTagCompound tag){ + public void readDataFieldUpdateTag(CompoundNBT tag){ if (tag .contains("Tank", 10)) { - NBTTagCompound tankTag = tag .getCompound("Tank"); + CompoundNBT tankTag = tag .getCompound("Tank"); getTank().readFromNBT(tankTag); if (tankTag.contains("Empty", 1)) { getTank().setFluid(null); @@ -430,18 +430,18 @@ public void readDataFieldUpdateTag(NBTTagCompound tag){ * Turns the data field NBT into a network packet */ @Override - public SPacketUpdateTileEntity getUpdatePacket(){ - NBTTagCompound nbtTag = createDataFieldUpdateTag(); - return new SPacketUpdateTileEntity(this.pos, 0, nbtTag); + public SUpdateTileEntityPacket getUpdatePacket(){ + CompoundNBT nbtTag = createDataFieldUpdateTag(); + return new SUpdateTileEntityPacket(this.pos, 0, nbtTag); } @Override - public NBTTagCompound getUpdateTag() { - return write(new NBTTagCompound()); + public CompoundNBT getUpdateTag() { + return write(new CompoundNBT()); } @Override - public void handleUpdateTag(NBTTagCompound tag) { + public void handleUpdateTag(CompoundNBT tag) { read(tag); } @@ -449,7 +449,7 @@ public void handleUpdateTag(NBTTagCompound tag) { * Receives the network packet made by getDescriptionPacket() */ @Override - public void onDataPacket(NetworkManager net, SPacketUpdateTileEntity packet) { + public void onDataPacket(NetworkManager net, SUpdateTileEntityPacket packet) { readDataFieldUpdateTag(packet.getNbtCompound()); } @@ -480,7 +480,7 @@ public boolean isEmpty() { * executes when a player right-clicks and opens the inventory GUI */ @Override - public void openInventory(EntityPlayer arg0) { + public void openInventory(PlayerEntity arg0) { // do nothing } @@ -489,7 +489,7 @@ public void openInventory(EntityPlayer arg0) { * executes when a player stops interacting with this machine */ @Override - public void closeInventory(EntityPlayer arg0) { + public void closeInventory(PlayerEntity arg0) { // do nothing } @@ -518,8 +518,7 @@ public ItemStack decrStackSize(int slot, int decrement) { /** * gets a field from the data array */ - @Override - public int getField(final int fieldIndex) { + public int getField(final int fieldIndex) { if(this.getDataFieldArray() != null){ return this.getDataFieldArray()[fieldIndex]; } else { @@ -530,8 +529,7 @@ public int getField(final int fieldIndex) { /** * sets a field in the data array */ - @Override - public void setField(final int fieldIndex, final int fieldValue) { + public void setField(final int fieldIndex, final int fieldValue) { if(this.getDataFieldArray() != null){ this.getDataFieldArray()[fieldIndex] = fieldValue; } else { @@ -543,7 +541,6 @@ public void setField(final int fieldIndex, final int fieldValue) { * Gets the number of data fields * @return The size of the data array */ - @Override public int getFieldCount() { if(this.getDataFieldArray() != null){ return this.getDataFieldArray().length; @@ -627,7 +624,7 @@ public boolean isItemValidForSlot(final int slot, final ItemStack item) { * boilerplate inventory code */ @Override - public boolean isUsableByPlayer(final EntityPlayer p_isUseableByPlayer_1_) { + public boolean isUsableByPlayer(final PlayerEntity p_isUseableByPlayer_1_) { return this.world.getTileEntity(this.pos) == this && p_isUseableByPlayer_1_.getDistanceSq((double)this.pos.getX() + 0.5, (double)this.pos.getY() + 0.5, (double)this.pos.getZ() + 0.5) <= 64.0; } @@ -665,13 +662,13 @@ public ITextComponent getDisplayName() { */ @Override public ITextComponent getName() { - return this.hasCustomName() ? new TextComponentString(this.customName) - : new TextComponentTranslation(unlocalizedName); + return this.hasCustomName() ? new StringTextComponent(this.customName) + : new TranslationTextComponent(unlocalizedName); } @Override public ITextComponent getCustomName() { - return this.hasCustomName() ? new TextComponentString(this.customName) : null; + return this.hasCustomName() ? new StringTextComponent(this.customName) : null; } /** @@ -692,7 +689,7 @@ public boolean hasCustomName() { * @return true if the item is allowed to be pulled, false otherwise */ @Override - public boolean canExtractItem(final int slot, final ItemStack targetItem, final EnumFacing side) { + public boolean canExtractItem(final int slot, final ItemStack targetItem, final Direction side) { if(this.getInventory() == null) return false; return slot < this.getInventory().length; } @@ -706,7 +703,7 @@ public boolean canExtractItem(final int slot, final ItemStack targetItem, final * @return true if the item is allowed to be inserted, false otherwise */ @Override - public boolean canInsertItem(final int slot, final ItemStack srcItem, final EnumFacing side) { + public boolean canInsertItem(final int slot, final ItemStack srcItem, final Direction side) { if(this.getInventory() == null) return false; return this.isItemValidForSlot(slot, srcItem); } @@ -728,7 +725,7 @@ public boolean canInsertItem(final int slot, final ItemStack srcItem, final Enum * through the given side of this block */ @Override - public int[] getSlotsForFace(EnumFacing side) { + public int[] getSlotsForFace(Direction side) { if(slotAccessCache != null) return slotAccessCache; if(this.getInventory() == null){ slotAccessCache = new int[0]; diff --git a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java index 55fac0f..2d9005c 100644 --- a/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java +++ b/src/main/java/cyano/poweradvantage/api/simple/TileEntitySimplePowerMachine.java @@ -5,18 +5,18 @@ import cyano.poweradvantage.api.PowerRequest; import cyano.poweradvantage.api.PoweredEntity; import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry; -import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.inventory.ISidedInventory; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.ListNBT; import net.minecraft.network.NetworkManager; -import net.minecraft.network.play.server.SPacketUpdateTileEntity; +import net.minecraft.network.play.server.SUpdateTileEntityPacket; import net.minecraft.tileentity.TileEntityType; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.text.ITextComponent; -import net.minecraft.util.text.TextComponentString; -import net.minecraft.util.text.TextComponentTranslation; +import net.minecraft.util.text.StringTextComponent; +import net.minecraft.util.text.TranslationTextComponent; import java.util.Arrays; @@ -31,7 +31,7 @@ * @author DrCyano * */ -public abstract class TileEntitySimplePowerMachine extends PoweredEntity implements ISidedInventory{ +public abstract class TileEntitySimplePowerMachine extends PoweredEntity implements ISidedInventory, net.minecraft.util.INameable { private final ConduitType[] types; private final float[] energyBufferSizes; private final float[] energyBuffers; @@ -288,14 +288,14 @@ public PowerRequest getPowerRequest(ConduitType type){ * @param tagRoot The root of the NBT */ @Override - public void read(final NBTTagCompound tagRoot) { + public void read(final CompoundNBT tagRoot) { super.read(tagRoot); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ Arrays.fill(inventory, null); - final NBTTagList nbttaglist = tagRoot.getList("Items", 10); + final ListNBT nbttaglist = tagRoot.getList("Items", 10); for (int i = 0; i < nbttaglist.size() && i < inventory.length; ++i) { - final NBTTagCompound nbttagcompound1 = nbttaglist .getCompound(i); + final CompoundNBT nbttagcompound1 = nbttaglist .getCompound(i); final byte n = nbttagcompound1.getByte("Slot"); if (n >= 0 && n < inventory.length) { ItemStack stack = ItemStack.read(nbttagcompound1); @@ -315,23 +315,23 @@ public void read(final NBTTagCompound tagRoot) { * @param tagRoot The root of the NBT */ @Override - public NBTTagCompound write(final NBTTagCompound tagRoot) { + public CompoundNBT write(final CompoundNBT tagRoot) { super .write(tagRoot); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ - final NBTTagList nbttaglist = new NBTTagList(); + final ListNBT nbttaglist = new ListNBT(); for (int i = 0; i < inventory.length; ++i) { if (inventory[i] != null && !inventory[i].isEmpty()) { - final NBTTagCompound nbttagcompound1 = new NBTTagCompound(); - nbttagcompound1.setByte("Slot", (byte)i); + final CompoundNBT nbttagcompound1 = new CompoundNBT(); + nbttagcompound1.putByte("Slot", (byte)i); inventory[i] .write(nbttagcompound1); nbttaglist .add(nbttagcompound1); } } - tagRoot.setTag("Items", nbttaglist); + tagRoot.put("Items", nbttaglist); } if (this.hasCustomName()) { - tagRoot.setString("CustomName", this.customName); + tagRoot.putString("CustomName", this.customName); } return tagRoot; } @@ -400,11 +400,11 @@ public void setCustomInventoryName(final String newName) { * @return A synchronization NBT holding values from the * getDataFieldArray() array */ - public NBTTagCompound createDataFieldUpdateTag(){ + public CompoundNBT createDataFieldUpdateTag(){ this.prepareDataFieldsForSync(); int[] dataFields = this.getDataFieldArray(); - NBTTagCompound nbtTag = new NBTTagCompound(); - nbtTag.setIntArray("[]", dataFields); + CompoundNBT nbtTag = new CompoundNBT(); + nbtTag.putIntArray("[]", dataFields); return nbtTag; } @@ -415,7 +415,7 @@ public NBTTagCompound createDataFieldUpdateTag(){ * @param tag A synchronization NBT holding values from the * getDataFieldArray() array */ - public void readDataFieldUpdateTag(NBTTagCompound tag){ + public void readDataFieldUpdateTag(CompoundNBT tag){ int[] newData = tag.getIntArray("[]"); System.arraycopy(newData, 0, this.getDataFieldArray(), 0, Math.min(newData.length, this.getDataFieldArray().length)); this.onDataFieldUpdate(); @@ -426,16 +426,16 @@ public void readDataFieldUpdateTag(NBTTagCompound tag){ * Turns the data field NBT into a network packet */ @Override - public SPacketUpdateTileEntity getUpdatePacket(){ - NBTTagCompound nbtTag = createDataFieldUpdateTag(); - return new SPacketUpdateTileEntity(this.pos, 0, nbtTag); + public SUpdateTileEntityPacket getUpdatePacket(){ + CompoundNBT nbtTag = createDataFieldUpdateTag(); + return new SUpdateTileEntityPacket(this.pos, 0, nbtTag); } /** * Receives the network packet made by getDescriptionPacket() */ @Override - public void onDataPacket(NetworkManager net, SPacketUpdateTileEntity packet) { + public void onDataPacket(NetworkManager net, SUpdateTileEntityPacket packet) { readDataFieldUpdateTag(packet.getNbtCompound()); } @@ -466,7 +466,7 @@ public boolean isEmpty() { * executes when a player right-clicks and opens the inventory GUI */ @Override - public void openInventory(EntityPlayer arg0) { + public void openInventory(PlayerEntity arg0) { // do nothing } @@ -475,7 +475,7 @@ public void openInventory(EntityPlayer arg0) { * executes when a player stops interacting with this machine */ @Override - public void closeInventory(EntityPlayer arg0) { + public void closeInventory(PlayerEntity arg0) { // do nothing } @@ -504,7 +504,6 @@ public ItemStack decrStackSize(int slot, int decrement) { /** * gets a field from the data array */ - @Override public int getField(final int fieldIndex) { if(this.getDataFieldArray() != null){ return this.getDataFieldArray()[fieldIndex]; @@ -516,7 +515,6 @@ public int getField(final int fieldIndex) { /** * sets a field in the data array */ - @Override public void setField(final int fieldIndex, final int fieldValue) { if(this.getDataFieldArray() != null){ this.getDataFieldArray()[fieldIndex] = fieldValue; @@ -529,7 +527,6 @@ public void setField(final int fieldIndex, final int fieldValue) { * Gets the number of data fields * @return The size of the data array */ - @Override public int getFieldCount() { if(this.getDataFieldArray() != null){ return this.getDataFieldArray().length; @@ -613,7 +610,7 @@ public boolean isItemValidForSlot(final int slot, final ItemStack item) { * boilerplate inventory code */ @Override - public boolean isUsableByPlayer(final EntityPlayer p_isUseableByPlayer_1_) { + public boolean isUsableByPlayer(final PlayerEntity p_isUseableByPlayer_1_) { return this.world.getTileEntity(this.pos) == this && p_isUseableByPlayer_1_.getDistanceSq((double)this.pos.getX() + 0.5, (double)this.pos.getY() + 0.5, (double)this.pos.getZ() + 0.5) <= 64.0; } @@ -651,13 +648,13 @@ public ITextComponent getDisplayName() { */ @Override public ITextComponent getName() { - return this.hasCustomName() ? new TextComponentString(this.customName) - : new TextComponentTranslation(unlocalizedName); + return this.hasCustomName() ? new StringTextComponent(this.customName) + : new TranslationTextComponent(unlocalizedName); } @Override public ITextComponent getCustomName() { - return this.hasCustomName() ? new TextComponentString(this.customName) : null; + return this.hasCustomName() ? new StringTextComponent(this.customName) : null; } /** @@ -678,7 +675,7 @@ public boolean hasCustomName() { * @return true if the item is allowed to be pulled, false otherwise */ @Override - public boolean canExtractItem(final int slot, final ItemStack targetItem, final EnumFacing side) { + public boolean canExtractItem(final int slot, final ItemStack targetItem, final Direction side) { if(this.getInventory() == null) return false; return slot < this.getInventory().length; } @@ -692,7 +689,7 @@ public boolean canExtractItem(final int slot, final ItemStack targetItem, final * @return true if the item is allowed to be inserted, false otherwise */ @Override - public boolean canInsertItem(final int slot, final ItemStack srcItem, final EnumFacing side) { + public boolean canInsertItem(final int slot, final ItemStack srcItem, final Direction side) { if(this.getInventory() == null) return false; return this.isItemValidForSlot(slot, srcItem); } @@ -714,7 +711,7 @@ public boolean canInsertItem(final int slot, final ItemStack srcItem, final Enum * through the given side of this block */ @Override - public int[] getSlotsForFace(EnumFacing side) { + public int[] getSlotsForFace(Direction side) { if(slotAccessCache != null) return slotAccessCache; if(this.getInventory() == null){ slotAccessCache = new int[0]; diff --git a/src/main/java/zone/moddev/mc/poweradvantage/PowerAdvantage.java b/src/main/java/zone/moddev/mc/poweradvantage/PowerAdvantage.java index 5bd713d..ebfba63 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/PowerAdvantage.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/PowerAdvantage.java @@ -37,12 +37,12 @@ import zone.moddev.mc.poweradvantage.migration.AdvantageLegacyMigrations; import zone.moddev.mc.poweradvantage.migration.LegacyWorldDataHook; -/** Forge 1.13 entry point for Power Advantage. */ +/** Forge 1.14 entry point for Power Advantage. */ @Mod(PowerAdvantage.MODID) public final class PowerAdvantage { public static final String MODID = "poweradvantage"; public static final String NAME = "Power Advantage"; - public static final String VERSION = "2.4.4.113021"; + public static final String VERSION = "2.4.5.114041"; public static final Logger LOGGER = LogManager.getLogger(MODID); private static PowerAdvantage instance; @@ -53,7 +53,7 @@ public final class PowerAdvantage { public static float chestLootFactor = 0.5F; public static boolean plasticIsAlsoRubber = true; - /** All historical RF- and Tech Reborn-labelled converters use Forge Energy in 1.13. */ + /** All historical RF- and Tech Reborn-labelled converters use Forge Energy in 1.14. */ public static final Map forgeEnergyConversionTable = new HashMap<>(); /** Conversion table for the historical Tech Reborn-labelled Forge Energy converter. */ @@ -75,10 +75,12 @@ public PowerAdvantage() { instance = this; AdvantageLegacyMigrations.registerPowerAdvantage(); - LegacyWorldDataHook.register(); MinecraftForge.EVENT_BUS.addListener(ConduitRegistry.getInstance()::onChunkLoad); MinecraftForge.EVENT_BUS.addListener(ConduitRegistry.getInstance()::onChunkUnload); MinecraftForge.EVENT_BUS.addListener(RecipeDiscovery::onPlayerLogin); + MinecraftForge.EVENT_BUS.addListener(zone.moddev.mc.poweradvantage.recipe.PartsLoot::onLoad); + MinecraftForge.EVENT_BUS.addListener(zone.moddev.mc.poweradvantage.init.Villages::onTrades); + MinecraftForge.EVENT_BUS.addListener(zone.moddev.mc.poweradvantage.recipe.MaterialTags::onTagsUpdated); Fluids.init(); Blocks.init(); @@ -94,8 +96,9 @@ public PowerAdvantage() { FMLJavaModLoadingContext.get().getModEventBus().addListener(this::commonSetup); DistExecutor.runWhenOn(Dist.CLIENT, () -> () -> { - ModLoadingContext.get().registerExtensionPoint( - ExtensionPoint.GUIFACTORY, () -> MachineGUIRegistry::createClientGui); + FMLJavaModLoadingContext.get().getModEventBus().addListener( + (net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent event) -> + net.minecraftforge.fml.DeferredWorkQueue.runLater(MachineGUIRegistry::registerScreens)); zone.moddev.mc.poweradvantage.client.ClientOilRenderer.register(); }); } @@ -134,6 +137,8 @@ private static void migrateLegacyConfiguration() { } private void commonSetup(FMLCommonSetupEvent event) { + // Forge's shared hook list is not safe to mutate during parallel mod construction. + net.minecraftforge.fml.DeferredWorkQueue.runLater(LegacyWorldDataHook::register); PowerAdvantageConfig.apply(); ModSupport.configureForgeEnergyConversions(); Recipes.initDistillationRecipes(PowerAdvantageConfig.distillationRecipes()); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/blocks/BlockFrame.java b/src/main/java/zone/moddev/mc/poweradvantage/blocks/BlockFrame.java index e43ea70..ce22941 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/blocks/BlockFrame.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/blocks/BlockFrame.java @@ -3,8 +3,8 @@ import net.minecraft.block.Block; import net.minecraft.block.SoundType; import net.minecraft.block.material.Material; -import net.minecraft.block.state.IBlockState; -import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.block.BlockState; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.util.BlockRenderLayer; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; @@ -31,18 +31,16 @@ public BlockRenderLayer getRenderLayer() { return BlockRenderLayer.CUTOUT; } - @Override - public boolean isFullCube(IBlockState bs) { + public boolean isFullCube(BlockState bs) { return false; } - @Override - public boolean canSilkHarvest(IBlockState state, IWorldReader world, BlockPos pos, EntityPlayer player) { + public boolean canSilkHarvest(BlockState state, IWorldReader world, BlockPos pos, PlayerEntity player) { return true; } - public boolean isOpaqueCube(IBlockState bs) { + public boolean isOpaqueCube(BlockState bs) { return false; } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/blocks/BlockPowerSwitch.java b/src/main/java/zone/moddev/mc/poweradvantage/blocks/BlockPowerSwitch.java index d696168..168a715 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/blocks/BlockPowerSwitch.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/blocks/BlockPowerSwitch.java @@ -8,7 +8,7 @@ import net.minecraft.block.material.Material; import net.minecraft.state.BooleanProperty; import net.minecraft.state.StateContainer; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.util.math.BlockPos; import net.minecraft.world.Explosion; import net.minecraft.world.IBlockReader; @@ -55,14 +55,14 @@ public BlockPowerSwitch(ConduitType powerType) { * Creates a blockstate */ @Override - protected void fillStateContainer(StateContainer.Builder builder) { + protected void fillStateContainer(StateContainer.Builder builder) { builder.add(ACTIVE); } /** * Converts metadata into blockstate */ - public IBlockState getStateFromMeta(final int metaValue) { + public BlockState getStateFromMeta(final int metaValue) { return this.getDefaultState() .with(ACTIVE, metaValue > 0); } @@ -70,7 +70,7 @@ public IBlockState getStateFromMeta(final int metaValue) { /** * Converts blockstate into metadata */ - public int getMetaFromState(final IBlockState bs) { + public int getMetaFromState(final BlockState bs) { if ((Boolean) (bs .get(ACTIVE))) { return 1; } else { @@ -107,12 +107,11 @@ public boolean isPowerSource(ConduitType powerType) { /** * Used to determine ambient occlusion and culling when rebuilding chunks for render */ - public boolean isOpaqueCube(IBlockState bs) { + public boolean isOpaqueCube(BlockState bs) { return true; } - @Override - public boolean isFullCube(IBlockState bs) { + public boolean isFullCube(BlockState bs) { return true; } @@ -120,8 +119,8 @@ public boolean isFullCube(IBlockState bs) { * Override of default block behavior */ @Override - public void onBlockAdded(final IBlockState state, final World world, final BlockPos coord, final IBlockState oldState) { - super.onBlockAdded(state, world, coord, oldState); + public void onBlockAdded(final BlockState state, final World world, final BlockPos coord, final BlockState oldState, final boolean isMoving) { + super.onBlockAdded(state, world, coord, oldState, isMoving); ConduitRegistry.getInstance().conduitBlockPlacedEvent(world, world.dimension.getType().getId(), coord, getTypes()); } @@ -129,7 +128,7 @@ public void onBlockAdded(final IBlockState state, final World world, final Block * This method is called when the block is removed from the world by an entity. */ @Override - public void onReplaced(IBlockState state, World w, BlockPos coord, IBlockState newState, boolean isMoving) { + public void onReplaced(BlockState state, World w, BlockPos coord, BlockState newState, boolean isMoving) { if (state.getBlock() != newState.getBlock()) { ConduitRegistry.getInstance().conduitBlockRemovedEvent(w, w.dimension.getType().getId(), coord, getTypes()); } @@ -151,7 +150,7 @@ public void onExplosionDestroy(World w, BlockPos coord, Explosion boom) { public void onNeighborChange(IBlockReader w, BlockPos pos, BlockPos neighbor) { if (w instanceof World) { World world = (World) w; - IBlockState state = world.getBlockState(pos); + BlockState state = world.getBlockState(pos); // redstone power given or taken boolean activated = !world.isBlockPowered(pos); world.setBlockState(pos, state.with(ACTIVE, activated), 2); @@ -164,7 +163,7 @@ public void onNeighborChange(IBlockReader w, BlockPos pos, BlockPos neighbor) { } @Override - public void neighborChanged(IBlockState state, World world, BlockPos pos, Block block, BlockPos fromPos) { + public void neighborChanged(BlockState state, World world, BlockPos pos, Block block, BlockPos fromPos, boolean isMoving) { onNeighborChange(world, pos, pos); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/client/ClientOilRenderer.java b/src/main/java/zone/moddev/mc/poweradvantage/client/ClientOilRenderer.java index b583149..ae315e5 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/client/ClientOilRenderer.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/client/ClientOilRenderer.java @@ -1,64 +1,26 @@ package zone.moddev.mc.poweradvantage.client; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.fluid.IFluidState; +import net.minecraft.client.renderer.texture.AtlasTexture; import net.minecraft.util.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.client.event.TextureStitchEvent; import net.minecraftforge.common.MinecraftForge; import zone.moddev.mc.poweradvantage.PowerAdvantage; -import zone.moddev.mc.poweradvantage.init.Fluids.PowerFluid; -/** Forge 25's native fluid renderer otherwise uses water sprites for custom fluids. */ +/** Forge FluidAttributes handle rendering; only the custom atlas sprites need stitching. */ @OnlyIn(Dist.CLIENT) public final class ClientOilRenderer { - private static volatile TextureAtlasSprite[] crudeSprites; - private static volatile TextureAtlasSprite[] refinedSprites; - private ClientOilRenderer() {} public static void register() { - MinecraftForge.EVENT_BUS.addListener(ClientOilRenderer::onTexturePre); - MinecraftForge.EVENT_BUS.addListener(ClientOilRenderer::onTexturePost); - } - - private static ResourceLocation texture(String name) { - return new ResourceLocation(PowerAdvantage.MODID, "blocks/" + name); + MinecraftForge.EVENT_BUS.addListener(ClientOilRenderer::stitch); } - private static void onTexturePre(TextureStitchEvent.Pre event) { - for (String name : new String[]{"crude_oil_still", "crude_oil_flow", "refined_oil_still", "refined_oil_flow"}) { - event.getMap().registerSprite(Minecraft.getInstance().getResourceManager(), texture(name)); + private static void stitch(TextureStitchEvent.Pre event) { + if (event.getMap() != net.minecraft.client.Minecraft.getInstance().getTextureMap()) return; + for (String name : new String[] {"crude_oil_still", "crude_oil_flow", "refined_oil_still", "refined_oil_flow"}) { + event.addSprite(new ResourceLocation(PowerAdvantage.MODID, "blocks/" + name)); } } - - private static void onTexturePost(TextureStitchEvent.Post event) { - crudeSprites = new TextureAtlasSprite[]{event.getMap().getSprite(texture("crude_oil_still")), - event.getMap().getSprite(texture("crude_oil_flow"))}; - refinedSprites = new TextureAtlasSprite[]{event.getMap().getSprite(texture("refined_oil_still")), - event.getMap().getSprite(texture("refined_oil_flow"))}; - } - - /** Avoid the water-overlay branch without putting oil in the lava tag. */ - public static boolean useOilPath(IFluidState state, boolean vanillaValue) { - return isOil(state) || vanillaValue; - } - - public static TextureAtlasSprite[] overrideSprites(IFluidState state, TextureAtlasSprite[] vanillaSprites) { - if (!isOil(state)) return vanillaSprites; - TextureAtlasSprite[] sprites = ((PowerFluid) state.getFluid()).getKind() - == zone.moddev.mc.poweradvantage.init.Fluids.Kind.CRUDE ? crudeSprites : refinedSprites; - return sprites == null ? vanillaSprites : sprites; - } - - /** The existing oil textures already contain their colour; do not apply biome water tint. */ - public static int overrideColor(IFluidState state, int vanillaColor) { - return isOil(state) ? 0xFFFFFF : vanillaColor; - } - - private static boolean isOil(IFluidState state) { - return state != null && state.getFluid() instanceof PowerFluid; - } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManager.java b/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManager.java index a8cf03f..11aded0 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManager.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManager.java @@ -11,8 +11,8 @@ import cyano.poweradvantage.api.PowerConnectorContext; import zone.moddev.mc.poweradvantage.math.BlockPos4D; import zone.moddev.mc.poweradvantage.util.PowerHelper; -import net.minecraft.block.state.IBlockState; -import net.minecraft.util.EnumFacing; +import net.minecraft.block.BlockState; +import net.minecraft.util.Direction; import net.minecraft.util.math.ChunkPos; import net.minecraft.world.World; @@ -193,14 +193,14 @@ public boolean areInSameNetwork(BlockPos4D a, BlockPos4D b) { * @param src The block from which we are scanning * @param type The type of energy that will flow throught the scanned network */ - protected void recursiveScan(World w, BlockPos4D coord, IBlockState src, ConduitType type) { - for (int i = 0; i < EnumFacing.values().length; i++) { - EnumFacing face = EnumFacing.values()[i]; + protected void recursiveScan(World w, BlockPos4D coord, BlockState src, ConduitType type) { + for (int i = 0; i < Direction.values().length; i++) { + Direction face = Direction.values()[i]; BlockPos4D n = coord.offset(face); if (!w.isBlockLoaded(n.pos) || areInSameNetwork(coord, n)) { continue; } - IBlockState blockstate = w.getBlockState(n.pos); + BlockState blockstate = w.getBlockState(n.pos); PowerConnectorContext connection = new PowerConnectorContext(type, w, src, coord.pos, face, blockstate, n.pos, face.getOpposite()); if (PowerHelper.areConnectable(connection)) { addBlockToNetwork(coord, n); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitRegistry.java b/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitRegistry.java index 1c32a0b..ee99d0d 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitRegistry.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitRegistry.java @@ -12,7 +12,7 @@ import net.minecraft.block.Block; import net.minecraft.block.ITileEntityProvider; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; import net.minecraftforge.common.util.LazyOptional; @@ -129,7 +129,7 @@ private List collectRequests(World w, List net, Condui } } } else if (e != null && PowerAdvantage.forgeEnergyConversionTable.containsKey(conduitType)) { - for (EnumFacing side : EnumFacing.values()) { + for (Direction side : Direction.values()) { LazyOptional capability = e.getCapability(CapabilityEnergy.ENERGY, side); if (!capability.isPresent()) continue; capability.ifPresent(storage -> requests.add(Wrappers.wrapForgeEnergyRequest(storage, @@ -223,8 +223,8 @@ public void conduitBlockPlacedEvent(World w, int dimension, BlockPos location, C BlockPos4D coord = new BlockPos4D(dimension, location); ConduitNetworkManager manager = getConduitNetworkManager(type); manager.invalidate(coord); - for (int i = 0; i < EnumFacing.values().length; i++) { - EnumFacing face = EnumFacing.values()[i]; + for (int i = 0; i < Direction.values().length; i++) { + Direction face = Direction.values()[i]; BlockPos4D n = coord.offset(face); manager.invalidate(n); } @@ -296,8 +296,8 @@ public void conduitBlockRemovedEvent(World w, int dimension, BlockPos location, BlockPos4D coord = new BlockPos4D(dimension, location); ConduitNetworkManager manager = getConduitNetworkManager(type); manager.invalidate(coord); - for (int i = 0; i < EnumFacing.values().length; i++) { - EnumFacing face = EnumFacing.values()[i]; + for (int i = 0; i < Direction.values().length; i++) { + Direction face = Direction.values()[i]; BlockPos4D n = coord.offset(face); manager.invalidate(n); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/gui/FilteredFluidTankGUI.java b/src/main/java/zone/moddev/mc/poweradvantage/gui/FilteredFluidTankGUI.java index 2f13f20..89bb5c9 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/gui/FilteredFluidTankGUI.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/gui/FilteredFluidTankGUI.java @@ -3,7 +3,7 @@ import cyano.poweradvantage.api.fluid.FluidPoweredEntity; import cyano.poweradvantage.api.simple.SimpleMachineGUI; import zone.moddev.mc.poweradvantage.math.Integer2D; -import net.minecraft.client.renderer.GlStateManager; +import com.mojang.blaze3d.platform.GlStateManager; import net.minecraft.util.ResourceLocation; import cyano.poweradvantage.api.fluid.FluidStack; @@ -28,8 +28,8 @@ public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int } } GlStateManager.color4f(1f, 1f, 1f, 1f); // reset tint - guiContainer.mc.getTextureManager().bindTexture(guiDisplayImage); - guiContainer.drawTexturedModalRect(x + 77, y + 6, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height) + guiContainer.getMinecraft().getTextureManager().bindTexture(guiDisplayImage); + guiContainer.blit(x + 77, y + 6, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height) } @@ -45,7 +45,7 @@ public static void drawFluidBar( FluidTankGUI.drawFluidFilledRectangle(guiContainer, fs, x + xPos, y + yPos + barSlotHeight - h, w, h, zLevel); //guiContainer.drawModalRectWithCustomSizedTexture(x+xPos, y+yPos+barSlotHeight-h, 0, 0, w, h, 16, h);//h * texPerPixel); // x, y, u, v, width, height, textureWidth, textureHeight - guiContainer.mc.getTextureManager().bindTexture(displayImage); - guiContainer.drawTexturedModalRect(x + xPos - 3, y + yPos - 3, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height) + guiContainer.getMinecraft().getTextureManager().bindTexture(displayImage); + guiContainer.blit(x + xPos - 3, y + yPos - 3, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height) } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/gui/FluidTankGUI.java b/src/main/java/zone/moddev/mc/poweradvantage/gui/FluidTankGUI.java index e21c64c..9595c8d 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/gui/FluidTankGUI.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/gui/FluidTankGUI.java @@ -3,11 +3,11 @@ import cyano.poweradvantage.api.fluid.FluidPoweredEntity; import cyano.poweradvantage.api.simple.SimpleMachineGUI; import zone.moddev.mc.poweradvantage.math.Integer2D; -import net.minecraft.client.renderer.GlStateManager; +import com.mojang.blaze3d.platform.GlStateManager; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.BufferBuilder; import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.client.renderer.texture.AtlasTexture; import net.minecraft.client.renderer.vertex.DefaultVertexFormats; import net.minecraft.util.ResourceLocation; import net.minecraft.world.biome.BiomeColors; @@ -36,8 +36,8 @@ public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int } } GlStateManager.color4f(1f, 1f, 1f, 1f); // reset tint - guiContainer.mc.getTextureManager().bindTexture(guiDisplayImage); - guiContainer.drawTexturedModalRect(x + 77, y + 6, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height) + guiContainer.getMinecraft().getTextureManager().bindTexture(guiDisplayImage); + guiContainer.blit(x + 77, y + 6, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height) } @@ -52,8 +52,8 @@ public static void drawFluidBar( FluidTankGUI.drawFluidFilledRectangle(guiContainer, fs, x + xPos, y + yPos + barSlotHeight - h, w, h, zLevel); //guiContainer.drawModalRectWithCustomSizedTexture(x+xPos, y+yPos+barSlotHeight-h, 0, 0, w, h, 16, h);//h * texPerPixel); // x, y, u, v, width, height, textureWidth, textureHeight - guiContainer.mc.getTextureManager().bindTexture(displayImage); - guiContainer.drawTexturedModalRect(x + xPos - 3, y + yPos - 3, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height) + guiContainer.getMinecraft().getTextureManager().bindTexture(displayImage); + guiContainer.blit(x + xPos - 3, y + yPos - 3, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height) } @@ -64,13 +64,13 @@ public static void drawFluidFilledRectangle( ) { if (fs == null || fs.getFluid() == null || width <= 0 || height <= 0) return; // The atlas supplies the current animation frame; water's sprite is grayscale. - TextureAtlasSprite sprite = guiContainer.mc.getModelManager().getBlockModelShapes() - .getModel(fs.getFluid().getDefaultState().getBlockState()).getParticleTexture(); - guiContainer.mc.getTextureManager().bindTexture(TextureMap.LOCATION_BLOCKS_TEXTURE); - int color = 0xFFFFFF; - if (fs.getFluid().isEquivalentTo(net.minecraft.init.Fluids.WATER)) { - color = guiContainer.mc.world != null && guiContainer.mc.player != null - ? BiomeColors.getWaterColor(guiContainer.mc.world, guiContainer.mc.player.getPosition()) : 0x3F76E4; + TextureAtlasSprite sprite = guiContainer.getMinecraft().getTextureMap() + .getSprite(fs.getFluid().getAttributes().getStillTexture()); + guiContainer.getMinecraft().getTextureManager().bindTexture(AtlasTexture.LOCATION_BLOCKS_TEXTURE); + int color = fs.getFluid().getAttributes().getColor(zone.moddev.mc.poweradvantage.util.ForgeFluidAdapter.toForge(fs)); + if (fs.getFluid().isEquivalentTo(net.minecraft.fluid.Fluids.WATER)) { + color = guiContainer.getMinecraft().world != null && guiContainer.getMinecraft().player != null + ? BiomeColors.getWaterColor(guiContainer.getMinecraft().world, guiContainer.getMinecraft().player.getPosition()) : 0x3F76E4; } float red = ((color >>> 16) & 255) / 255F; float green = ((color >>> 8) & 255) / 255F; diff --git a/src/main/java/zone/moddev/mc/poweradvantage/gui/StillGUI.java b/src/main/java/zone/moddev/mc/poweradvantage/gui/StillGUI.java index e091e2e..2fb2c96 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/gui/StillGUI.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/gui/StillGUI.java @@ -21,8 +21,8 @@ public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int if (((StillTileEntity) srcEntity).isBurning()) { int h = (int) (14 * ((StillTileEntity) srcEntity).getBurnFraction()); int d = 14 - h; - guiContainer.drawTexturedModalRect(x + 80, y + 49 - h, 198, d, 14, h); // x, y, u, v, width, height - guiContainer.drawTexturedModalRect(x + 77, y + 16, 198, 14, 24, 16); // x, y, u, v, width, height + guiContainer.blit(x + 80, y + 49 - h, 198, d, 14, h); // x, y, u, v, width, height + guiContainer.blit(x + 77, y + 16, 198, 14, 24, 16); // x, y, u, v, width, height } FluidStack fs1 = ((StillTileEntity) srcEntity).getInputTank().getFluid(); if (fs1 != null && ((StillTileEntity) srcEntity).getInputTank().getFluidAmount() > 0) { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Blocks.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Blocks.java index f79778e..5c0a137 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Blocks.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Blocks.java @@ -8,12 +8,12 @@ import cyano.poweradvantage.api.GUIBlock; import cyano.poweradvantage.api.fluid.InteractiveFluidBlock; import net.minecraft.block.Block; -import net.minecraft.block.BlockFlowingFluid; +import net.minecraft.block.FlowingFluidBlock; import net.minecraft.block.material.Material; -import net.minecraft.init.MobEffects; +import net.minecraft.potion.Effects; import net.minecraft.item.Item; -import net.minecraft.item.ItemBlock; -import net.minecraft.potion.PotionEffect; +import net.minecraft.item.BlockItem; +import net.minecraft.potion.EffectInstance; import net.minecraftforge.event.RegistryEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; @@ -66,8 +66,8 @@ public final class Blocks { public static GUIBlock infinite_steam; public static GUIBlock infinite_electricity; public static GUIBlock infinite_quantum; - public static BlockFlowingFluid crude_oil_block; - public static BlockFlowingFluid refined_oil_block; + public static FlowingFluidBlock crude_oil_block; + public static FlowingFluidBlock refined_oil_block; private static boolean initialized; @@ -105,11 +105,11 @@ public static synchronized void init() { Block.Properties oilProperties = Block.Properties.create(Material.WATER).doesNotBlockMovement() .hardnessAndResistance(100.0F).variableOpacity(); - refined_oil_block = (BlockFlowingFluid) add(new InteractiveFluidBlock(Fluids.refined_oil, - oilProperties, true, (world, entity) -> entity.addPotionEffect(new PotionEffect(MobEffects.NAUSEA, 200))), + refined_oil_block = (FlowingFluidBlock) add(new InteractiveFluidBlock(Fluids.refined_oil, + oilProperties, true, (world, entity) -> entity.addPotionEffect(new EffectInstance(Effects.NAUSEA, 200))), "refined_oil"); - crude_oil_block = (BlockFlowingFluid) add(new InteractiveFluidBlock(Fluids.crude_oil, - oilProperties, true, (world, entity) -> entity.addPotionEffect(new PotionEffect(MobEffects.SLOWNESS, 200, 2))), + crude_oil_block = (FlowingFluidBlock) add(new InteractiveFluidBlock(Fluids.crude_oil, + oilProperties, true, (world, entity) -> entity.addPotionEffect(new EffectInstance(Effects.SLOWNESS, 200, 2))), "crude_oil"); ModSupport.initBlocks(); @@ -139,10 +139,10 @@ public static void registerBlockItems(RegistryEvent.Register event) { for (Map.Entry entry : ALL_BLOCKS.entrySet()) { Item.Properties properties = new Item.Properties(); if (!"fluid_pipe_terminal".equals(entry.getKey()) - && !(entry.getValue() instanceof BlockFlowingFluid)) { + && !(entry.getValue() instanceof FlowingFluidBlock)) { properties.group(ItemGroups.tab_powerAdvantage); } - ItemBlock item = new ItemBlock(entry.getValue(), properties); + BlockItem item = new BlockItem(entry.getValue(), properties); item.setRegistryName(PowerAdvantage.MODID, entry.getKey()); event.getRegistry().register(item); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Entities.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Entities.java index 5817e73..68d426b 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Entities.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Entities.java @@ -27,7 +27,7 @@ import cyano.poweradvantage.api.modsupport.rf.TileEntityRFSteamConverter; import cyano.poweradvantage.api.modsupport.techreborn.TileEntityTRElectricityConverter; -/** Stable 1.13 tile entity type registrations. */ +/** Stable tile identities with the native 1.14 valid-block lists. */ @Mod.EventBusSubscriber(modid = PowerAdvantage.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) public final class Entities { public static TileEntityType PIPE_TERMINAL; @@ -55,32 +55,33 @@ private Entities() {} @SubscribeEvent public static void register(RegistryEvent.Register> event) { - PIPE_TERMINAL = register(event, "tileentity.pipe_terminal", TerminalFluidPipeTileEntity::new); - FLUID_DRAIN = register(event, "tileentity.fluid_drain", FluidDrainTileEntity::new); - FLUID_DISCHARGE = register(event, "tileentity.fluid_discharge", FluidDischargeTileEntity::new); - STORAGE_TANK = register(event, "tileentity.fluid_storage_tank", StorageTankTileEntity::new); - METAL_TANK = register(event, "tileentity.fluid_metal_tank", MetalTankTileEntity::new); - STILL = register(event, "tileentity.still", StillTileEntity::new); - CONVEYOR = register(event, "item_conveyor", TileEntityConveyor::new); - FILTER_BLOCK = register(event, "item_filter_block", TileEntityBlockFilter::new); - FILTER_FOOD = register(event, "item_filter_food", TileEntityFoodFilter::new); - FILTER_FUEL = register(event, "item_filter_fuel", TileEntityFuelFilter::new); - FILTER_INVENTORY = register(event, "item_filter_inventory", TileEntityInventoryFilter::new); - FILTER_ORE = register(event, "item_filter_ore", TileEntityOreFilter::new); - FILTER_PLANT = register(event, "item_filter_plant", TileEntityPlantFilter::new); - FILTER_SMELT = register(event, "item_filter_smelt", TileEntitySmeltableFilter::new); - FILTER_OVERFLOW = register(event, "item_filter_overflow", TileEntityOverflowFilter::new); - INFINITE_ENERGY = register(event, "infinite_energy_source", InfiniteEnergyTileEntity::new); - RF_STEAM_CONVERTER = register(event, "rf_steam_converter_tileentity", TileEntityRFSteamConverter::new); - RF_ELECTRICITY_CONVERTER = register(event, "rf_electricity_converter_tileentity", TileEntityRFElectricityConverter::new); - RF_QUANTUM_CONVERTER = register(event, "rf_quantum_converter_tileentity", TileEntityRFQuantumConverter::new); - TR_ELECTRICITY_CONVERTER = register(event, "tr_electricity_converter_tileentity", TileEntityTRElectricityConverter::new); + PIPE_TERMINAL = register(event, "tileentity.pipe_terminal", TerminalFluidPipeTileEntity::new, Blocks.fluid_pipe_terminal); + FLUID_DRAIN = register(event, "tileentity.fluid_drain", FluidDrainTileEntity::new, Blocks.fluid_drain); + FLUID_DISCHARGE = register(event, "tileentity.fluid_discharge", FluidDischargeTileEntity::new, Blocks.fluid_discharge); + STORAGE_TANK = register(event, "tileentity.fluid_storage_tank", StorageTankTileEntity::new, Blocks.storage_tank); + METAL_TANK = register(event, "tileentity.fluid_metal_tank", MetalTankTileEntity::new, Blocks.metal_storage_tank); + STILL = register(event, "tileentity.still", StillTileEntity::new, Blocks.still); + CONVEYOR = register(event, "item_conveyor", TileEntityConveyor::new, Blocks.item_conveyor); + FILTER_BLOCK = register(event, "item_filter_block", TileEntityBlockFilter::new, Blocks.item_filter_block); + FILTER_FOOD = register(event, "item_filter_food", TileEntityFoodFilter::new, Blocks.item_filter_food); + FILTER_FUEL = register(event, "item_filter_fuel", TileEntityFuelFilter::new, Blocks.item_filter_fuel); + FILTER_INVENTORY = register(event, "item_filter_inventory", TileEntityInventoryFilter::new, Blocks.item_filter_inventory); + FILTER_ORE = register(event, "item_filter_ore", TileEntityOreFilter::new, Blocks.item_filter_ore); + FILTER_PLANT = register(event, "item_filter_plant", TileEntityPlantFilter::new, Blocks.item_filter_plant); + FILTER_SMELT = register(event, "item_filter_smelt", TileEntitySmeltableFilter::new, Blocks.item_filter_smelt); + FILTER_OVERFLOW = register(event, "item_filter_overflow", TileEntityOverflowFilter::new, Blocks.item_filter_overflow); + INFINITE_ENERGY = register(event, "infinite_energy_source", InfiniteEnergyTileEntity::new, + Blocks.infinite_steam, Blocks.infinite_electricity, Blocks.infinite_quantum); + RF_STEAM_CONVERTER = register(event, "rf_steam_converter_tileentity", TileEntityRFSteamConverter::new, ModSupport.converter_rf_steam); + RF_ELECTRICITY_CONVERTER = register(event, "rf_electricity_converter_tileentity", TileEntityRFElectricityConverter::new, ModSupport.converter_rf_electricity); + RF_QUANTUM_CONVERTER = register(event, "rf_quantum_converter_tileentity", TileEntityRFQuantumConverter::new, ModSupport.converter_rf_quantum); + TR_ELECTRICITY_CONVERTER = register(event, "tr_electricity_converter_tileentity", TileEntityTRElectricityConverter::new, ModSupport.converter_tr_electricity); } private static TileEntityType register( RegistryEvent.Register> event, String id, - java.util.function.Supplier factory) { - TileEntityType type = TileEntityType.Builder.create(factory).build(null); + java.util.function.Supplier factory, net.minecraft.block.Block... validBlocks) { + TileEntityType type = TileEntityType.Builder.create(factory, validBlocks).build(null); type.setRegistryName(PowerAdvantage.MODID, id); event.getRegistry().register(type); return type; diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Fluids.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Fluids.java index c149fe1..586093a 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Fluids.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Fluids.java @@ -4,25 +4,31 @@ import java.util.Map; import cyano.poweradvantage.api.ConduitType; -import net.minecraft.block.BlockFlowingFluid; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.FlowingFluidBlock; +import net.minecraft.block.BlockState; import net.minecraft.fluid.FlowingFluid; import net.minecraft.fluid.Fluid; import net.minecraft.fluid.IFluidState; import net.minecraft.item.Item; import net.minecraft.state.StateContainer; import net.minecraft.util.BlockRenderLayer; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.BlockPos; -import net.minecraft.util.registry.IRegistry; +import net.minecraft.util.registry.Registry; import net.minecraft.world.IWorld; -import net.minecraft.world.IWorldReaderBase; +import net.minecraft.world.IWorldReader; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fluids.FluidAttributes; +import net.minecraftforge.fluids.ForgeFlowingFluid; +import net.minecraftforge.fml.common.Mod; import zone.moddev.mc.poweradvantage.PowerAdvantage; -/** Native 1.13 fluids and their PowerAdvantage conduit identities. */ +/** Native Forge fluids and their persistent PowerAdvantage conduit identities. */ +@Mod.EventBusSubscriber(modid = PowerAdvantage.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) public final class Fluids { public static final ConduitType fluidConduit_general = new ConduitType("fluid"); private static final Map FLUID_TYPES = new IdentityHashMap<>(); @@ -40,8 +46,8 @@ private Fluids() {} public static synchronized void init() { if (initialized) return; - fluidConduit_water = registerNewFluid(net.minecraft.init.Fluids.WATER); - fluidConduit_lava = registerNewFluid(net.minecraft.init.Fluids.LAVA); + fluidConduit_water = registerNewFluid(net.minecraft.fluid.Fluids.WATER); + fluidConduit_lava = registerNewFluid(net.minecraft.fluid.Fluids.LAVA); flowing_crude_oil = new PowerFluid.Flowing(Kind.CRUDE); crude_oil = new PowerFluid.Source(Kind.CRUDE); @@ -60,14 +66,16 @@ public static synchronized void init() { } private static void register(String name, Fluid fluid) { - IRegistry.field_212619_h.put(new ResourceLocation(PowerAdvantage.MODID, name), fluid); - for (IFluidState state : fluid.getStateContainer().getValidStates()) { - Fluid.STATE_REGISTRY.add(state); - } + fluid.setRegistryName(PowerAdvantage.MODID, name); + } + + @SubscribeEvent + public static void registerFluids(RegistryEvent.Register event) { + event.getRegistry().registerAll(crude_oil, flowing_crude_oil, refined_oil, flowing_refined_oil); } public static ConduitType registerNewFluid(Fluid fluid) { - ResourceLocation id = IRegistry.field_212619_h.getKey(fluid); + ResourceLocation id = fluid.getRegistryName(); ConduitType type = new ConduitType(id == null ? "unknown_fluid" : id.toString()); FLUID_TYPES.put(fluid, type); return type; @@ -93,59 +101,55 @@ public static Fluid byName(String name) { if (name == null || name.trim().isEmpty()) return null; ResourceLocation id = name.indexOf(':') >= 0 ? new ResourceLocation(name) : new ResourceLocation(PowerAdvantage.MODID, name); - Fluid fluid = IRegistry.field_212619_h.func_212608_b(id); + Fluid fluid = registeredFluid(id); if (fluid == null && name.indexOf(':') < 0) { - fluid = IRegistry.field_212619_h.func_212608_b(new ResourceLocation("minecraft", name)); + fluid = registeredFluid(new ResourceLocation("minecraft", name)); } return fluid; } + private static Fluid registeredFluid(ResourceLocation id) { + // Forge's defaulted fluid registry returns EMPTY, not null, for unknown names. + // Do not let a missing poweradvantage:water hide the legacy vanilla fallback. + return net.minecraftforge.registries.ForgeRegistries.FLUIDS.containsKey(id) + ? net.minecraftforge.registries.ForgeRegistries.FLUIDS.getValue(id) : null; + } + public enum Kind { CRUDE, REFINED } - public abstract static class PowerFluid extends FlowingFluid { + public abstract static class PowerFluid extends ForgeFlowingFluid { private final Kind kind; - protected PowerFluid(Kind kind) { this.kind = kind; } + protected PowerFluid(Kind kind) { + super(properties(kind)); + this.kind = kind; + } public Kind getKind() { return kind; } - @Override public Fluid getFlowingFluid() { - return kind == Kind.CRUDE ? flowing_crude_oil : flowing_refined_oil; - } - @Override public Fluid getStillFluid() { - return kind == Kind.CRUDE ? crude_oil : refined_oil; - } - @Override public Item getFilledBucket() { - return kind == Kind.CRUDE ? Items.crude_oil_bucket : Items.refined_oil_bucket; - } - @Override @OnlyIn(Dist.CLIENT) public BlockRenderLayer getRenderLayer() { - return BlockRenderLayer.TRANSLUCENT; - } - @Override protected boolean canSourcesMultiply() { return false; } - @Override protected void beforeReplacingBlock(IWorld world, BlockPos pos, IBlockState state) { - state.dropBlockAsItem(world.getWorld(), pos, 0); - } - @Override public int getSlopeFindDistance(IWorldReaderBase world) { return 4; } - @Override public int getLevelDecreasePerBlock(IWorldReaderBase world) { return 1; } - @Override public int getTickRate(IWorldReaderBase world) { return kind == Kind.CRUDE ? 15 : 10; } - @Override protected float getExplosionResistance() { return 100.0F; } - @Override public boolean isEquivalentTo(Fluid fluid) { - return fluid == getStillFluid() || fluid == getFlowingFluid(); - } - @Override protected boolean canOtherFlowInto(IFluidState state, Fluid fluid, EnumFacing direction) { - return direction == EnumFacing.DOWN && !fluid.isEquivalentTo(this); - } - @Override public IBlockState getBlockState(IFluidState state) { - BlockFlowingFluid block = kind == Kind.CRUDE ? Blocks.crude_oil_block : Blocks.refined_oil_block; - return block.getDefaultState().with(BlockFlowingFluid.LEVEL, Integer.valueOf(getLevelFromState(state))); + private static ForgeFlowingFluid.Properties properties(Kind kind) { + String name = kind == Kind.CRUDE ? "crude_oil" : "refined_oil"; + return new ForgeFlowingFluid.Properties( + () -> kind == Kind.CRUDE ? crude_oil : refined_oil, + () -> kind == Kind.CRUDE ? flowing_crude_oil : flowing_refined_oil, + FluidAttributes.builder(new ResourceLocation(PowerAdvantage.MODID, "blocks/" + name + "_still"), + new ResourceLocation(PowerAdvantage.MODID, "blocks/" + name + "_flow")) + .translationKey("fluid.poweradvantage." + name).color(0xFFFFFFFF) + .density(kind == Kind.CRUDE ? 850 : 720) + .viscosity(kind == Kind.CRUDE ? 6000 : 1000).temperature(300)) + .bucket(() -> kind == Kind.CRUDE ? Items.crude_oil_bucket : Items.refined_oil_bucket) + .block(() -> kind == Kind.CRUDE ? Blocks.crude_oil_block : Blocks.refined_oil_block) + .slopeFindDistance(4).levelDecreasePerBlock(1) + .tickRate(kind == Kind.CRUDE ? 15 : 10).explosionResistance(100.0F) + .renderLayer(BlockRenderLayer.TRANSLUCENT); } public static final class Flowing extends PowerFluid { public Flowing(Kind kind) { super(kind); } @Override protected void fillStateContainer(StateContainer.Builder builder) { super.fillStateContainer(builder); - builder.add(LEVEL_1_TO_8); + builder.add(LEVEL_1_8); } - @Override public int getLevel(IFluidState state) { return state.get(LEVEL_1_TO_8); } + @Override public int getLevel(IFluidState state) { return state.get(LEVEL_1_8); } @Override public boolean isSource(IFluidState state) { return false; } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Fuels.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Fuels.java index 3d34318..120b28f 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Fuels.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Fuels.java @@ -21,9 +21,9 @@ public static void init() { FuelRegistry.getInstance().registerFuel(Items.refined_oil_bucket, (short) Math.min(Short.MAX_VALUE, 1000 * REFINED_OIL_FUEL_PER_FLUID_UNIT)); FuelRegistry.getInstance().registerPostBurnItem(Items.crude_oil_bucket, - (ItemStack stack) -> new ItemStack(net.minecraft.init.Items.BUCKET)); + (ItemStack stack) -> new ItemStack(net.minecraft.item.Items.BUCKET)); FuelRegistry.getInstance().registerPostBurnItem(Items.refined_oil_bucket, - (ItemStack stack) -> new ItemStack(net.minecraft.init.Items.BUCKET)); + (ItemStack stack) -> new ItemStack(net.minecraft.item.Items.BUCKET)); initDone = true; diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/ItemGroups.java b/src/main/java/zone/moddev/mc/poweradvantage/init/ItemGroups.java index c55a03c..5b07076 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/ItemGroups.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/init/ItemGroups.java @@ -4,7 +4,7 @@ import zone.moddev.mc.poweradvantage.gui.FunctionalCreativeTab; import net.minecraft.item.ItemGroup; import net.minecraft.item.Item; -import net.minecraft.item.ItemBlock; +import net.minecraft.item.BlockItem; import net.minecraft.item.ItemStack; import net.minecraftforge.registries.ForgeRegistries; @@ -41,8 +41,8 @@ private static String catagorize(ItemStack i) { StringBuilder sb = new StringBuilder(); Item item = i.getItem(); sb.append(ForgeRegistries.ITEMS.getKey(item).getNamespace()); - if (item instanceof ItemBlock) { - if (((ItemBlock) item).getBlock().hasTileEntity(((ItemBlock) item).getBlock().getDefaultState())) { + if (item instanceof BlockItem) { + if (((BlockItem) item).getBlock().hasTileEntity(((BlockItem) item).getBlock().getDefaultState())) { sb.append("A"); } else { sb.append("B"); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Items.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Items.java index 339d1e4..71e7b3c 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Items.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Items.java @@ -5,7 +5,7 @@ import java.util.Set; import net.minecraft.item.Item; -import net.minecraft.item.ItemBucket; +import net.minecraft.item.BucketItem; import net.minecraftforge.event.RegistryEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; @@ -34,10 +34,10 @@ public static synchronized void init() { bioplastic_ingot = add("bioplastic_ingot", new Item(properties())); sprocket = add("sprocket", new Item(properties())); rotator_tool = add("rotator_tool", new RotationTool(properties().maxStackSize(1))); - crude_oil_bucket = add("crude_oil_bucket", new ItemBucket(Fluids.crude_oil, - new Item.Properties().group(ItemGroups.tab_powerAdvantage).maxStackSize(1))); - refined_oil_bucket = add("refined_oil_bucket", new ItemBucket(Fluids.refined_oil, - new Item.Properties().group(ItemGroups.tab_powerAdvantage).maxStackSize(1))); + crude_oil_bucket = add("crude_oil_bucket", new BucketItem(Fluids.crude_oil, + new Item.Properties().group(ItemGroups.tab_powerAdvantage).maxStackSize(1).containerItem(net.minecraft.item.Items.BUCKET))); + refined_oil_bucket = add("refined_oil_bucket", new BucketItem(Fluids.refined_oil, + new Item.Properties().group(ItemGroups.tab_powerAdvantage).maxStackSize(1).containerItem(net.minecraft.item.Items.BUCKET))); initialized = true; } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Recipes.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Recipes.java index eea8f0f..c2e8977 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Recipes.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Recipes.java @@ -4,7 +4,7 @@ import cyano.poweradvantage.api.FluidCategoryRegistry; import net.minecraft.fluid.Fluid; import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.IRegistry; +import net.minecraft.util.registry.Registry; import zone.moddev.mc.poweradvantage.registry.still.recipe.DistillationRecipeRegistry; @@ -41,12 +41,12 @@ public static void initDistillationRecipes(String[] distillRecipes) { private static void initCrudeOilAliasDistillationRecipes() { Fluid refinedOil = Fluids.refined_oil; if (refinedOil == null) return; - ResourceLocation outputId = IRegistry.field_212619_h.getKey(refinedOil); + ResourceLocation outputId = net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(refinedOil); if (outputId == null) return; for (Fluid input : FluidCategoryRegistry.getRegisteredFluids(FluidCategoryRegistry.CRUDE_OIL)) { if (DistillationRecipeRegistry.getInstance().getDistillationRecipeForFluid(input) != null) continue; - ResourceLocation inputId = IRegistry.field_212619_h.getKey(input); + ResourceLocation inputId = net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(input); if (inputId != null) { DistillationRecipeRegistry.addDistillationRecipe(inputId.toString(), 2, outputId.toString(), 1); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Villages.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Villages.java index 25ab569..ea3e71b 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/init/Villages.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Villages.java @@ -1,42 +1,23 @@ package zone.moddev.mc.poweradvantage.init; -import net.minecraft.entity.passive.EntityVillager; -import net.minecraft.util.ResourceLocation; -import zone.moddev.mc.poweradvantage.PowerAdvantage; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.fml.common.registry.VillagerRegistry; +import net.minecraft.entity.merchant.villager.VillagerProfession; +import net.minecraft.item.ItemStack; +import net.minecraft.item.MerchantOffer; +import net.minecraftforge.event.village.VillagerTradesEvent; -public abstract class Villages { - // TODO: add machinist villager - private static final ResourceLocation[] PROFESSION_LIST = { - new ResourceLocation("minecraft:farmer"), - new ResourceLocation("minecraft:librarian"), - new ResourceLocation("minecraft:priest"), - new ResourceLocation("minecraft:smith"), - new ResourceLocation("minecraft:butcher") - }; +/** The old farmer and toolsmith careers now have their own vanilla professions. */ +public final class Villages { + private Villages() {} - private static boolean initDone = false; - - public static void init() { - if (initDone) return; - - try { - insertTrades(3, 3, 1, new EntityVillager.ListItemForEmeralds(Items.sprocket, new EntityVillager.PriceInfo(-4, -1))); - insertTrades(0, 1, 1, new EntityVillager.ListItemForEmeralds(Items.bioplastic_ingot, new EntityVillager.PriceInfo(-8, -4))); - } catch (RuntimeException e) { - PowerAdvantage.LOGGER.error("Failed to add trades to villagers", e); - } - - initDone = true; - } - - private static void insertTrades(int professionID, int careerID, int tradeLevel, EntityVillager.ITradeList... trades) { - VillagerRegistry.VillagerProfession profession = ForgeRegistries.VILLAGER_PROFESSIONS.getValue(PROFESSION_LIST[professionID]); - if (profession == null) { - throw new IllegalStateException("No villager profession registered for " + PROFESSION_LIST[professionID]); - } - VillagerRegistry.VillagerCareer career = profession.getCareer(careerID - 1); - career.addTrade(tradeLevel, trades); - } + public static void onTrades(VillagerTradesEvent event) { + if (event.getType() == VillagerProfession.TOOLSMITH) { + event.getTrades().get(1).add((villager, random) -> new MerchantOffer( + new ItemStack(net.minecraft.item.Items.EMERALD), + new ItemStack(Items.sprocket, 1 + random.nextInt(4)), 7, 1, 0.0F)); + } else if (event.getType() == VillagerProfession.FARMER) { + event.getTrades().get(1).add((villager, random) -> new MerchantOffer( + new ItemStack(net.minecraft.item.Items.EMERALD), + new ItemStack(Items.bioplastic_ingot, 4 + random.nextInt(5)), 7, 1, 0.0F)); + } + } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/items/RotationTool.java b/src/main/java/zone/moddev/mc/poweradvantage/items/RotationTool.java index 3011304..514cab6 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/items/RotationTool.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/items/RotationTool.java @@ -1,10 +1,10 @@ package zone.moddev.mc.poweradvantage.items; -import net.minecraft.block.state.IBlockState; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.util.EnumActionResult; -import net.minecraft.util.EnumFacing; -import net.minecraft.util.EnumHand; +import net.minecraft.block.BlockState; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.ActionResultType; +import net.minecraft.util.Direction; +import net.minecraft.util.Hand; import net.minecraft.util.Rotation; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; @@ -18,12 +18,12 @@ public RotationTool(Item.Properties properties) { } @Override - public EnumActionResult onItemUse(ItemUseContext context) { - final EntityPlayer player = context.getPlayer(); + public ActionResultType onItemUse(ItemUseContext context) { + final PlayerEntity player = context.getPlayer(); final World w = context.getWorld(); final BlockPos coord = context.getPos(); - final EnumFacing facing = context.getFace(); - IBlockState bs = w.getBlockState(coord); + final Direction facing = context.getFace(); + BlockState bs = w.getBlockState(coord); /* // comment this line to turn the wrench into a hacker tool try { // hacker info FMLLog.info("%s",new StringBuilder() @@ -38,13 +38,13 @@ public EnumActionResult onItemUse(ItemUseContext context) { } //*/ - IBlockState rotated = bs.rotate(w, coord, Rotation.COUNTERCLOCKWISE_90); + BlockState rotated = bs.rotate(w, coord, Rotation.COUNTERCLOCKWISE_90); if (rotated != bs) { w.setBlockState(coord, rotated); - return EnumActionResult.SUCCESS; + return ActionResultType.SUCCESS; } - return EnumActionResult.PASS; + return ActionResultType.PASS; } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyor.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyor.java index a939312..c1f80b9 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyor.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyor.java @@ -4,13 +4,13 @@ import net.minecraft.block.Block; import net.minecraft.block.SoundType; import net.minecraft.block.material.Material; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.item.BlockItemUseContext; import net.minecraft.state.DirectionProperty; import net.minecraft.state.StateContainer; import net.minecraft.state.properties.BlockStateProperties; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.Mirror; import net.minecraft.util.Rotation; import net.minecraft.util.math.BlockPos; @@ -24,29 +24,29 @@ public class BlockConveyor extends GUIBlock { public BlockConveyor(Material material, float hardness) { super(Block.Properties.create(material).hardnessAndResistance(hardness) .sound(SoundType.METAL).variableOpacity()); - setDefaultState(getStateContainer().getBaseState().with(FACING, EnumFacing.NORTH)); + setDefaultState(getStateContainer().getBaseState().with(FACING, Direction.NORTH)); } @Override public TileEntity createNewTileEntity(IBlockReader world) { return new TileEntityConveyor(); } - @Override public IBlockState rotate(IBlockState state, Rotation rotation) { return state.with(FACING, rotation.rotate(state.get(FACING))); } - @Override public IBlockState mirror(IBlockState state, Mirror mirror) { return state.rotate(mirror.toRotation(state.get(FACING))); } - @Override public IBlockState getStateForPlacement(BlockItemUseContext context) { + @Override public BlockState rotate(BlockState state, Rotation rotation) { return state.with(FACING, rotation.rotate(state.get(FACING))); } + @Override public BlockState mirror(BlockState state, Mirror mirror) { return state.rotate(mirror.toRotation(state.get(FACING))); } + @Override public BlockState getStateForPlacement(BlockItemUseContext context) { return getDefaultState().with(FACING, context.getFace().getOpposite()); } - @Override protected void fillStateContainer(StateContainer.Builder builder) { builder.add(FACING); } - @Override public boolean isOpaqueCube(IBlockState state) { return false; } - @Override public boolean isFullCube(IBlockState state) { return false; } - @Override public boolean hasComparatorInputOverride(IBlockState state) { return true; } + @Override protected void fillStateContainer(StateContainer.Builder builder) { builder.add(FACING); } + @Override public boolean isOpaqueCube(BlockState state) { return false; } + public boolean isFullCube(BlockState state) { return false; } + @Override public boolean hasComparatorInputOverride(BlockState state) { return true; } @Override - public int getComparatorInputOverride(IBlockState state, World world, BlockPos pos) { + public int getComparatorInputOverride(BlockState state, World world, BlockPos pos) { TileEntity tile = world.getTileEntity(pos); return tile instanceof TileEntityConveyor && !((TileEntityConveyor) tile).getStackInSlot(0).isEmpty() ? 7 : 0; } - public IBlockState getStateFromMeta(int meta) { - EnumFacing facing = EnumFacing.byIndex(meta); - return getDefaultState().with(FACING, facing == null ? EnumFacing.NORTH : facing); + public BlockState getStateFromMeta(int meta) { + Direction facing = Direction.byIndex(meta); + return getDefaultState().with(FACING, facing == null ? Direction.NORTH : facing); } - public int getMetaFromState(IBlockState state) { return state.get(FACING).getIndex(); } + public int getMetaFromState(BlockState state) { return state.get(FACING).getIndex(); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyorFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyorFilter.java index 6f3c8a3..47c7cb6 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyorFilter.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyorFilter.java @@ -2,15 +2,15 @@ import cyano.poweradvantage.api.GUIBlock; import net.minecraft.block.Block; -import net.minecraft.block.BlockHorizontal; +import net.minecraft.block.HorizontalBlock; import net.minecraft.block.SoundType; import net.minecraft.block.material.Material; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.item.BlockItemUseContext; import net.minecraft.state.DirectionProperty; import net.minecraft.state.StateContainer; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.Mirror; import net.minecraft.util.Rotation; import net.minecraft.util.math.BlockPos; @@ -20,7 +20,7 @@ /** Horizontal conveyor filter block retaining its legacy metadata codec. */ public class BlockConveyorFilter extends GUIBlock { - public static final DirectionProperty FACING = BlockHorizontal.HORIZONTAL_FACING; + public static final DirectionProperty FACING = HorizontalBlock.HORIZONTAL_FACING; private final Class tileEntityClass; public BlockConveyorFilter(Material material, float hardness, @@ -28,7 +28,7 @@ public BlockConveyorFilter(Material material, float hardness, super(Block.Properties.create(material).hardnessAndResistance(hardness) .sound(SoundType.METAL).variableOpacity()); this.tileEntityClass = tileEntityClass; - setDefaultState(getStateContainer().getBaseState().with(FACING, EnumFacing.NORTH)); + setDefaultState(getStateContainer().getBaseState().with(FACING, Direction.NORTH)); } @Override @@ -41,18 +41,18 @@ public TileEntity createNewTileEntity(IBlockReader world) { } } - @Override public IBlockState rotate(IBlockState state, Rotation rotation) { return state.with(FACING, rotation.rotate(state.get(FACING))); } - @Override public IBlockState mirror(IBlockState state, Mirror mirror) { return state.rotate(mirror.toRotation(state.get(FACING))); } - @Override public IBlockState getStateForPlacement(BlockItemUseContext context) { + @Override public BlockState rotate(BlockState state, Rotation rotation) { return state.with(FACING, rotation.rotate(state.get(FACING))); } + @Override public BlockState mirror(BlockState state, Mirror mirror) { return state.rotate(mirror.toRotation(state.get(FACING))); } + @Override public BlockState getStateForPlacement(BlockItemUseContext context) { return getDefaultState().with(FACING, context.getPlacementHorizontalFacing()); } - @Override protected void fillStateContainer(StateContainer.Builder builder) { builder.add(FACING); } - @Override public boolean isOpaqueCube(IBlockState state) { return false; } - @Override public boolean isFullCube(IBlockState state) { return false; } - @Override public boolean hasComparatorInputOverride(IBlockState state) { return true; } + @Override protected void fillStateContainer(StateContainer.Builder builder) { builder.add(FACING); } + @Override public boolean isOpaqueCube(BlockState state) { return false; } + public boolean isFullCube(BlockState state) { return false; } + @Override public boolean hasComparatorInputOverride(BlockState state) { return true; } @Override - public int getComparatorInputOverride(IBlockState state, World world, BlockPos pos) { + public int getComparatorInputOverride(BlockState state, World world, BlockPos pos) { TileEntity tile = world.getTileEntity(pos); if (!(tile instanceof TileEntityConveyorFilter)) return 0; TileEntityConveyorFilter filter = (TileEntityConveyorFilter) tile; @@ -60,9 +60,9 @@ public int getComparatorInputOverride(IBlockState state, World world, BlockPos p return filter.matchesFilter(filter.getStackInSlot(0)) ? 15 : 7; } - public IBlockState getStateFromMeta(int meta) { - EnumFacing facing = EnumFacing.byIndex(meta); - return getDefaultState().with(FACING, facing.getAxis().isHorizontal() ? facing : EnumFacing.NORTH); + public BlockState getStateFromMeta(int meta) { + Direction facing = Direction.byIndex(meta); + return getDefaultState().with(FACING, facing.getAxis().isHorizontal() ? facing : Direction.NORTH); } - public int getMetaFromState(IBlockState state) { return state.get(FACING).getIndex(); } + public int getMetaFromState(BlockState state) { return state.get(FACING).getIndex(); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityBlockFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityBlockFilter.java index 6df0795..addda74 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityBlockFilter.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityBlockFilter.java @@ -1,6 +1,6 @@ package zone.moddev.mc.poweradvantage.machines.conveyors; -import net.minecraft.item.ItemBlock; +import net.minecraft.item.BlockItem; import net.minecraft.item.ItemStack; import zone.moddev.mc.poweradvantage.init.Entities; @@ -10,7 +10,7 @@ public class TileEntityBlockFilter extends TileEntityConveyorFilter { @Override public boolean matchesFilter(ItemStack item) { if (item == null) return false; - return item.getItem() instanceof ItemBlock; + return item.getItem() instanceof BlockItem; } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyor.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyor.java index acee3c2..277c9b1 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyor.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyor.java @@ -1,26 +1,26 @@ package zone.moddev.mc.poweradvantage.machines.conveyors; import zone.moddev.mc.poweradvantage.util.InventoryWrapper; -import net.minecraft.block.state.IBlockState; -import net.minecraft.entity.item.EntityItem; -import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.block.BlockState; +import net.minecraft.entity.item.ItemEntity; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.inventory.IInventory; import net.minecraft.inventory.ISidedInventory; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.ListNBT; import net.minecraft.tileentity.TileEntity; -import net.minecraft.tileentity.TileEntityHopper; +import net.minecraft.tileentity.HopperTileEntity; import net.minecraft.tileentity.TileEntityType; -import net.minecraft.util.EnumFacing; -import net.minecraft.util.ITickable; +import net.minecraft.util.Direction; +import net.minecraft.tileentity.ITickableTileEntity; import net.minecraft.util.math.AxisAlignedBB; import net.minecraft.util.math.BlockPos; -import net.minecraft.util.text.TextComponentBase; -import net.minecraft.util.text.TextComponentString; -import net.minecraft.util.text.TextComponentTranslation; +import net.minecraft.util.text.TextComponent; +import net.minecraft.util.text.StringTextComponent; +import net.minecraft.util.text.TranslationTextComponent; import net.minecraft.util.text.ITextComponent; -import net.minecraft.world.ILockableContainer; +import net.minecraft.inventory.container.INamedContainerProvider; import net.minecraft.world.World; import zone.moddev.mc.poweradvantage.PowerAdvantage; @@ -29,7 +29,25 @@ import java.util.Random; import zone.moddev.mc.poweradvantage.init.Entities; -public class TileEntityConveyor extends TileEntity implements ITickable, ISidedInventory { +public class TileEntityConveyor extends TileEntity implements ITickableTileEntity, ISidedInventory, net.minecraft.util.INameable { + private final zone.moddev.mc.poweradvantage.util.MachineCapabilities machineCapabilities = + new zone.moddev.mc.poweradvantage.util.MachineCapabilities(this); + + @Override public net.minecraftforge.common.util.LazyOptional getCapability( + net.minecraftforge.common.capabilities.Capability capability, Direction face) { + net.minecraftforge.common.util.LazyOptional local = machineCapabilities.get(capability, face); + return local.isPresent() ? local : super.getCapability(capability, face); + } + + @Override protected void invalidateCaps() { + super.invalidateCaps(); + machineCapabilities.invalidate(); + } + + @Override public void validate() { + super.validate(); + reviveCaps(); + } public static String unlocalizedName = TileEntityConveyorFilter.class.getName(); @@ -58,16 +76,16 @@ public void tick() { if(transferCooldown > 0) transferCooldown--; if(transferCooldown == 0){ // do tick update - EnumFacing dir =this.getFacing(); + Direction dir =this.getFacing(); // deposit item - EnumFacing myDir = dir; - EnumFacing theirDir = dir.getOpposite(); + Direction myDir = dir; + Direction theirDir = dir.getOpposite(); BlockPos targetPos = getPos().offset(myDir); TileEntity target = w.getTileEntity(targetPos); if(target != null) { if( target instanceof IInventory){ - ISidedInventory them = InventoryWrapper.wrap(TileEntityHopper.getInventoryAtPosition(getWorld(),targetPos.getX(), targetPos.getY(), targetPos.getZ())); + ISidedInventory them = InventoryWrapper.wrap(HopperTileEntity.getInventoryAtPosition(getWorld(),targetPos.getX(), targetPos.getY(), targetPos.getZ())); if(transferItem(this,myDir,them,theirDir)){ transferCooldown = transferInvterval; @@ -82,11 +100,11 @@ public void tick() { BlockPos upstreamBlock = getPos().offset(myDir); if(w.isAirBlock(upstreamBlock)){ // grab items sitting on the ground behind the conveyor - List list = w.getEntitiesWithinAABB(EntityItem.class, new AxisAlignedBB( + List list = w.getEntitiesWithinAABB(ItemEntity.class, new AxisAlignedBB( upstreamBlock.getX(), upstreamBlock.getY(), upstreamBlock.getZ(), upstreamBlock.getX()+1, upstreamBlock.getY()+1, upstreamBlock.getZ()+1)); if(!list.isEmpty()){ - EntityItem e = list.get(0); + ItemEntity e = list.get(0); if(!e.isAirBorne){ ItemStack entityStack = e.getItem(); if(canInsertItemInto(entityStack,this,myDir)){ @@ -111,7 +129,7 @@ public void tick() { target = w.getTileEntity(upstreamBlock); if(target != null){ if( target instanceof IInventory){ - ISidedInventory them = InventoryWrapper.wrap(TileEntityHopper.getInventoryAtPosition(getWorld(),upstreamBlock.getX(), upstreamBlock.getY(), upstreamBlock.getZ())); + ISidedInventory them = InventoryWrapper.wrap(HopperTileEntity.getInventoryAtPosition(getWorld(),upstreamBlock.getX(), upstreamBlock.getY(), upstreamBlock.getZ())); if(transferItem(them,theirDir,this,myDir)){ this.markDirty(); } @@ -123,11 +141,11 @@ public void tick() { } protected static boolean isLocked(Object o){ - return o instanceof ILockableContainer && ((ILockableContainer)o).isLocked(); + return zone.moddev.mc.poweradvantage.util.InventoryLocks.isLocked(o); } - protected static boolean canInsertItemInto(ItemStack item, ISidedInventory dest, EnumFacing destFace){ + protected static boolean canInsertItemInto(ItemStack item, ISidedInventory dest, Direction destFace){ if(item == null || item.isEmpty() || item.getItem() == null || isLocked(dest)){ return false; } @@ -146,7 +164,7 @@ protected static boolean canInsertItemInto(ItemStack item, ISidedInventory dest, return false; } - protected static boolean transferItem(ISidedInventory src, EnumFacing srcFace, ISidedInventory dest, EnumFacing destFace){ + protected static boolean transferItem(ISidedInventory src, Direction srcFace, ISidedInventory dest, Direction destFace){ if(isLocked(src) || isLocked(dest)){ return false; } @@ -191,13 +209,13 @@ protected static boolean transferItem(ISidedInventory src, EnumFacing srcFace, I * Gets the direction that the blockstate is facing * @return an EnumfFacing */ - public EnumFacing getFacing(){ - IBlockState state = getWorld().getBlockState(getPos()); + public Direction getFacing(){ + BlockState state = getWorld().getBlockState(getPos()); if(state.has(BlockConveyor.FACING)){ return state.get(BlockConveyor.FACING); }else{ PowerAdvantage.LOGGER.warn("{}.getFacing(): blockstate at {} does not have BlockConveyor.FACING", this.getClass().getName(), getPos()); - return EnumFacing.NORTH; + return Direction.NORTH; } } @@ -213,14 +231,14 @@ protected ItemStack[] getInventory(){ * @param tagRoot The root of the NBT */ @Override - public void read(final NBTTagCompound tagRoot) { + public void read(final CompoundNBT tagRoot) { super.read(tagRoot); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ Arrays.fill(inventory, null); - final NBTTagList nbttaglist = tagRoot.getList("Items", 10); + final ListNBT nbttaglist = tagRoot.getList("Items", 10); for (int i = 0; i < nbttaglist.size() && i < inventory.length; ++i) { - final NBTTagCompound nbttagcompound1 = nbttaglist .getCompound(i); + final CompoundNBT nbttagcompound1 = nbttaglist .getCompound(i); final byte n = nbttagcompound1.getByte("Slot"); if (n >= 0 && n < inventory.length) { ItemStack stack = ItemStack.read(nbttagcompound1); @@ -240,23 +258,23 @@ public void read(final NBTTagCompound tagRoot) { * @param tagRoot The root of the NBT */ @Override - public NBTTagCompound write(final NBTTagCompound tagRoot) { + public CompoundNBT write(final CompoundNBT tagRoot) { super .write(tagRoot); ItemStack[] inventory = this.getInventory(); if(inventory != null ){ - final NBTTagList nbttaglist = new NBTTagList(); + final ListNBT nbttaglist = new ListNBT(); for (int i = 0; i < inventory.length; ++i) { if (inventory[i] != null && !inventory[i].isEmpty()) { - final NBTTagCompound nbttagcompound1 = new NBTTagCompound(); - nbttagcompound1.setByte("Slot", (byte)i); + final CompoundNBT nbttagcompound1 = new CompoundNBT(); + nbttagcompound1.putByte("Slot", (byte)i); inventory[i] .write(nbttagcompound1); nbttaglist .add(nbttagcompound1); } } - tagRoot.setTag("Items", nbttaglist); + tagRoot.put("Items", nbttaglist); } if (this.hasCustomName()) { - tagRoot.setString("CustomName", this.customName); + tagRoot.putString("CustomName", this.customName); } return tagRoot; } @@ -269,7 +287,7 @@ public NBTTagCompound write(final NBTTagCompound tagRoot) { * @param newSate State after change * @return true if this TileEntity should be invalidated (deleted and recreated), false otherwise */ - public boolean shouldRefresh(World world, BlockPos pos, IBlockState oldState, IBlockState newSate) { + public boolean shouldRefresh(World world, BlockPos pos, BlockState oldState, BlockState newSate) { return oldState.getBlock() != newSate.getBlock(); } @@ -288,7 +306,7 @@ public boolean isEmpty() { } @Override - public void closeInventory(EntityPlayer p) { + public void closeInventory(PlayerEntity p) { // do nothing } @@ -309,12 +327,10 @@ public ItemStack decrStackSize(int slot, int decrement) { return itemstack; } - @Override public int getField(int n) { return 0; } - @Override public int getFieldCount() { return 0; } @@ -350,7 +366,7 @@ public boolean isItemValidForSlot(final int slot, final ItemStack item) { } @Override - public boolean isUsableByPlayer(final EntityPlayer p_isUseableByPlayer_1_) { + public boolean isUsableByPlayer(final PlayerEntity p_isUseableByPlayer_1_) { return this.world.getTileEntity(this.pos) == this && p_isUseableByPlayer_1_.getDistanceSq( (double)this.pos.getX() + 0.5, @@ -359,11 +375,10 @@ public boolean isUsableByPlayer(final EntityPlayer p_isUseableByPlayer_1_) { } @Override - public void openInventory(EntityPlayer arg0) { + public void openInventory(PlayerEntity arg0) { // do nothing } - @Override public void setField(int arg0, int arg1) { // do nothing } @@ -383,8 +398,8 @@ public void setInventorySlotContents(int slot, ItemStack item) { } @Override - public TextComponentBase getDisplayName() { - return (TextComponentBase) getName(); + public TextComponent getDisplayName() { + return (TextComponent) getName(); } private String customName = null; @@ -395,13 +410,13 @@ public TextComponentBase getDisplayName() { */ @Override public ITextComponent getName() { - return this.hasCustomName() ? new TextComponentString(this.customName) - : new TextComponentTranslation(this.getUnlocalizedName()); + return this.hasCustomName() ? new StringTextComponent(this.customName) + : new TranslationTextComponent(this.getUnlocalizedName()); } @Override public ITextComponent getCustomName() { - return this.hasCustomName() ? new TextComponentString(this.customName) : null; + return this.hasCustomName() ? new StringTextComponent(this.customName) : null; } /** @@ -431,7 +446,7 @@ public boolean hasCustomName() { * @return true if the item is allowed to be pulled, false otherwise */ @Override - public boolean canExtractItem(final int slot, final ItemStack targetItem, final EnumFacing side) { + public boolean canExtractItem(final int slot, final ItemStack targetItem, final Direction side) { return slot == 0; } /** @@ -443,7 +458,7 @@ public boolean canExtractItem(final int slot, final ItemStack targetItem, final * @return true if the item is allowed to be inserted, false otherwise */ @Override - public boolean canInsertItem(final int slot, final ItemStack srcItem, final EnumFacing side) { + public boolean canInsertItem(final int slot, final ItemStack srcItem, final Direction side) { return slot == 0 && this.isItemValidForSlot(slot, srcItem); } @@ -463,7 +478,7 @@ public boolean canInsertItem(final int slot, final ItemStack srcItem, final Enum * through the given side of this block */ @Override - public int[] getSlotsForFace(EnumFacing side) { + public int[] getSlotsForFace(Direction side) { return slotAccessCache; } ////////// ////////// diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyorFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyorFilter.java index fe0065a..9499fb6 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyorFilter.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyorFilter.java @@ -1,15 +1,15 @@ package zone.moddev.mc.poweradvantage.machines.conveyors; import zone.moddev.mc.poweradvantage.util.InventoryWrapper; -import net.minecraft.block.state.IBlockState; -import net.minecraft.entity.item.EntityItem; +import net.minecraft.block.BlockState; +import net.minecraft.entity.item.ItemEntity; import net.minecraft.inventory.IInventory; import net.minecraft.inventory.ISidedInventory; import net.minecraft.item.ItemStack; import net.minecraft.tileentity.TileEntity; -import net.minecraft.tileentity.TileEntityHopper; +import net.minecraft.tileentity.HopperTileEntity; import net.minecraft.tileentity.TileEntityType; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; import zone.moddev.mc.poweradvantage.PowerAdvantage; @@ -34,34 +34,34 @@ public void tick() { if (transferCooldown > 0) transferCooldown--; if (transferCooldown == 0) { // do tick update - EnumFacing dir = this.getFacing(); + Direction dir = this.getFacing(); if (getInventory()[0] == null) { // not holding item, get item - EnumFacing myDir = dir.getOpposite(); - EnumFacing theirDir = dir; + Direction myDir = dir.getOpposite(); + Direction theirDir = dir; BlockPos targetPos = getPos().offset(myDir); TileEntity target = w.getTileEntity(targetPos); if (target != null) { if (target instanceof IInventory) { - ISidedInventory them = InventoryWrapper.wrap(TileEntityHopper.getInventoryAtPosition(getWorld(), targetPos.getX(), targetPos.getY(), targetPos.getZ())); + ISidedInventory them = InventoryWrapper.wrap(HopperTileEntity.getInventoryAtPosition(getWorld(), targetPos.getX(), targetPos.getY(), targetPos.getZ())); if (transferItem(them, theirDir, this, myDir)) { this.markDirty(); } } } } else { - EnumFacing myDir = dir; - EnumFacing theirDir = dir.getOpposite(); + Direction myDir = dir; + Direction theirDir = dir.getOpposite(); BlockPos targetPos = getPos().offset(myDir); TileEntity target = w.getTileEntity(targetPos); - TileEntity dropTarget = w.getTileEntity(getPos().offset(EnumFacing.DOWN)); + TileEntity dropTarget = w.getTileEntity(getPos().offset(Direction.DOWN)); if (matchesFilter(this.getInventory()[0])) { - if (isValidItemFor(getInventory()[0], dropTarget, EnumFacing.UP)) { + if (isValidItemFor(getInventory()[0], dropTarget, Direction.UP)) { dropItem(dropTarget); return; } } else if (target instanceof IInventory) { - ISidedInventory them = InventoryWrapper.wrap(TileEntityHopper.getInventoryAtPosition(getWorld(), targetPos.getX(), targetPos.getY(), targetPos.getZ())); + ISidedInventory them = InventoryWrapper.wrap(HopperTileEntity.getInventoryAtPosition(getWorld(), targetPos.getX(), targetPos.getY(), targetPos.getZ())); if (transferItem(this, myDir, them, theirDir)) { this.markDirty(); } @@ -72,11 +72,11 @@ public void tick() { } } - protected boolean isValidItemFor(ItemStack item, TileEntity target, EnumFacing side) { + protected boolean isValidItemFor(ItemStack item, TileEntity target, Direction side) { if (item == null) return false; if (target instanceof IInventory) { BlockPos targetPos = target.getPos(); - ISidedInventory dt = InventoryWrapper.wrap(TileEntityHopper.getInventoryAtPosition(getWorld(), targetPos.getX(), targetPos.getY(), targetPos.getZ())); + ISidedInventory dt = InventoryWrapper.wrap(HopperTileEntity.getInventoryAtPosition(getWorld(), targetPos.getX(), targetPos.getY(), targetPos.getZ())); int[] slots = dt.getSlotsForFace(side); for (int i = 0; i < slots.length; i++) { int slot = slots[i]; @@ -92,23 +92,21 @@ protected boolean isValidItemFor(ItemStack item, TileEntity target, EnumFacing s private void dropItem(TileEntity target) { World w = getWorld(); - IBlockState bs = w.getBlockState(getPos().down()); + BlockState bs = w.getBlockState(getPos().down()); if (target == null && !(bs.getMaterial().blocksMovement())) { // drop item in the air - EntityItem ie = new EntityItem(w, getPos().getX() + 0.5, getPos().getY() - 0.5, getPos().getZ() + 0.5, getInventory()[0]); - ie.motionX = 0; - ie.motionZ = 0; - ie.motionY = 0; - w.spawnEntity(ie); + ItemEntity ie = new ItemEntity(w, getPos().getX() + 0.5, getPos().getY() - 0.5, getPos().getZ() + 0.5, getInventory()[0]); + ie.setMotion(0, 0, 0); + w.addEntity(ie); getInventory()[0] = null; transferCooldown = transferInvterval; this.markDirty(); } else if (target instanceof IInventory) { // add item to inventory - EnumFacing myDir = EnumFacing.DOWN; - EnumFacing theirDir = EnumFacing.UP; + Direction myDir = Direction.DOWN; + Direction theirDir = Direction.UP; BlockPos targetPos = target.getPos(); - ISidedInventory targetInv = InventoryWrapper.wrap(TileEntityHopper.getInventoryAtPosition(getWorld(), targetPos.getX(), targetPos.getY(), targetPos.getZ())); + ISidedInventory targetInv = InventoryWrapper.wrap(HopperTileEntity.getInventoryAtPosition(getWorld(), targetPos.getX(), targetPos.getY(), targetPos.getZ())); if (transferItem(this, myDir, targetInv, theirDir)) { transferCooldown = transferInvterval; this.markDirty(); @@ -122,13 +120,13 @@ private void dropItem(TileEntity target) { * @return an EnumfFacing */ @Override - public EnumFacing getFacing() { - IBlockState state = getWorld().getBlockState(getPos()); + public Direction getFacing() { + BlockState state = getWorld().getBlockState(getPos()); if (state.has(BlockConveyorFilter.FACING)) { return state.get(BlockConveyorFilter.FACING); } else { PowerAdvantage.LOGGER.warn("{}.getFacing(): blockstate at {} does not have BlockConveyorFilter.FACING", this.getClass().getName(), getPos()); - return EnumFacing.NORTH; + return Direction.NORTH; } } @@ -142,10 +140,10 @@ public EnumFacing getFacing() { * @return true if the item is allowed to be pulled, false otherwise */ @Override - public boolean canExtractItem(final int slot, final ItemStack targetItem, final EnumFacing side) { + public boolean canExtractItem(final int slot, final ItemStack targetItem, final Direction side) { boolean filterMatch = matchesFilter(targetItem); if (slot != 0) return false; - if (side == EnumFacing.DOWN) { + if (side == Direction.DOWN) { return filterMatch; } else { return (!filterMatch) && super.canExtractItem(slot, targetItem, side); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFoodFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFoodFilter.java index 9c084af..2b0b539 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFoodFilter.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFoodFilter.java @@ -1,6 +1,5 @@ package zone.moddev.mc.poweradvantage.machines.conveyors; -import net.minecraft.item.ItemFood; import net.minecraft.item.ItemStack; import zone.moddev.mc.poweradvantage.init.Entities; @@ -10,7 +9,7 @@ public class TileEntityFoodFilter extends TileEntityConveyorFilter { @Override public boolean matchesFilter(ItemStack item) { if (item == null) return false; - return item.getItem() instanceof ItemFood; + return !item.isEmpty() && item.getItem().isFood(); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFuelFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFuelFilter.java index 44732b7..821382e 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFuelFilter.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFuelFilter.java @@ -2,7 +2,7 @@ import net.minecraft.item.ItemStack; -import net.minecraft.tileentity.TileEntityFurnace; +import net.minecraft.tileentity.AbstractFurnaceTileEntity; import zone.moddev.mc.poweradvantage.init.Entities; public class TileEntityFuelFilter extends TileEntityConveyorFilter { @@ -11,7 +11,7 @@ public class TileEntityFuelFilter extends TileEntityConveyorFilter { @Override public boolean matchesFilter(ItemStack item) { if (item == null) return false; - return TileEntityFurnace.isItemFuel(item); + return AbstractFurnaceTileEntity.isFuel(item); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOverflowFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOverflowFilter.java index 6e5757c..1cffecc 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOverflowFilter.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOverflowFilter.java @@ -5,7 +5,7 @@ import net.minecraft.inventory.ISidedInventory; import net.minecraft.item.ItemStack; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import zone.moddev.mc.poweradvantage.init.Entities; public class TileEntityOverflowFilter extends TileEntityConveyorFilter { @@ -14,7 +14,7 @@ public class TileEntityOverflowFilter extends TileEntityConveyorFilter { @Override public boolean matchesFilter(ItemStack item) { if (item == null || item.getItem() == null) return false; - boolean canDrop = canInsert(item, EnumFacing.DOWN); + boolean canDrop = canInsert(item, Direction.DOWN); boolean canPass = canInsert(item, this.getFacing()); if (canPass) { return canDrop; @@ -23,10 +23,10 @@ public boolean matchesFilter(ItemStack item) { } } - private boolean canInsert(ItemStack item, EnumFacing f) { + private boolean canInsert(ItemStack item, Direction f) { TileEntity target = getWorld().getTileEntity(getPos().offset(f)); if (target instanceof IInventory) { - EnumFacing side = f.getOpposite(); + Direction side = f.getOpposite(); ISidedInventory dt = InventoryWrapper.wrap((IInventory) target); int[] slots = dt.getSlotsForFace(side); for (int i = 0; i < slots.length; i++) { @@ -49,7 +49,7 @@ private boolean canInsert(ItemStack item, EnumFacing f) { } @Override - protected boolean isValidItemFor(ItemStack item, TileEntity target, EnumFacing side) { + protected boolean isValidItemFor(ItemStack item, TileEntity target, Direction side) { // do all of the logic in the matchesFilter method return true; } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityPlantFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityPlantFilter.java index 1c9e3f6..74fe985 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityPlantFilter.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityPlantFilter.java @@ -2,7 +2,7 @@ import net.minecraft.block.Block; import net.minecraft.block.IGrowable; -import net.minecraft.item.ItemBlock; +import net.minecraft.item.BlockItem; import net.minecraft.item.ItemStack; import net.minecraftforge.common.IPlantable; import zone.moddev.mc.poweradvantage.init.Entities; @@ -14,8 +14,8 @@ public class TileEntityPlantFilter extends TileEntityConveyorFilter { public boolean matchesFilter(ItemStack item) { if (item == null) return false; if (item.getItem() instanceof IPlantable) return true; - if (item.getItem() instanceof ItemBlock) { - Block b = ((ItemBlock) item.getItem()).getBlock(); + if (item.getItem() instanceof BlockItem) { + Block b = ((BlockItem) item.getItem()).getBlock(); if (b instanceof IPlantable) return true; if (b instanceof IGrowable) return true; diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntitySmeltableFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntitySmeltableFilter.java index 01464de..d40a436 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntitySmeltableFilter.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntitySmeltableFilter.java @@ -1,9 +1,9 @@ package zone.moddev.mc.poweradvantage.machines.conveyors; -import net.minecraft.inventory.InventoryBasic; +import net.minecraft.inventory.Inventory; import net.minecraft.item.ItemStack; -import net.minecraft.util.text.TextComponentString; -import net.minecraftforge.common.crafting.VanillaRecipeTypes; +import net.minecraft.util.text.StringTextComponent; +import net.minecraft.item.crafting.IRecipeType; import zone.moddev.mc.poweradvantage.init.Entities; public class TileEntitySmeltableFilter extends TileEntityConveyorFilter { @@ -12,9 +12,9 @@ public class TileEntitySmeltableFilter extends TileEntityConveyorFilter { @Override public boolean matchesFilter(ItemStack item) { if (item == null || item.isEmpty() || world == null) return false; - InventoryBasic probe = new InventoryBasic(new TextComponentString("smelting probe"), 1); + Inventory probe = new Inventory(1); probe.setInventorySlotContents(0, item.copy()); - return world.getRecipeManager().getRecipe(probe, world, VanillaRecipeTypes.SMELTING) != null; + return world.getRecipeManager().getRecipe(IRecipeType.SMELTING, probe, world).isPresent(); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyBlock.java index feea40d..e47de76 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyBlock.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyBlock.java @@ -4,7 +4,7 @@ import cyano.poweradvantage.api.PoweredEntity; import cyano.poweradvantage.api.simple.BlockSimplePowerMachine; import net.minecraft.block.material.Material; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; import net.minecraft.world.IBlockReader; @@ -21,12 +21,12 @@ public PoweredEntity createNewTileEntity(IBlockReader world) { } @Override - public boolean hasComparatorInputOverride(IBlockState bs) { + public boolean hasComparatorInputOverride(BlockState bs) { return false; } @Override - public int getComparatorInputOverride(IBlockState bs, World world, BlockPos coord) { + public int getComparatorInputOverride(BlockState bs, World world, BlockPos coord) { return 0; } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyTileEntity.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyTileEntity.java index 89b48ca..6052706 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyTileEntity.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyTileEntity.java @@ -3,7 +3,7 @@ import cyano.poweradvantage.api.ConduitType; import cyano.poweradvantage.api.simple.TileEntitySimplePowerMachine; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.CompoundNBT; import zone.moddev.mc.poweradvantage.init.Entities; public class InfiniteEnergyTileEntity extends TileEntitySimplePowerMachine { @@ -89,14 +89,14 @@ public void powerUpdate() { } @Override - public NBTTagCompound write(final NBTTagCompound tagRoot) { + public CompoundNBT write(final CompoundNBT tagRoot) { super .write(tagRoot); - tagRoot.setString("InfType", this.type[0].toString()); + tagRoot.putString("InfType", this.type[0].toString()); return tagRoot; } @Override - public void read(final NBTTagCompound tagRoot) { + public void read(final CompoundNBT tagRoot) { super.read(tagRoot); if (tagRoot.contains("InfType", 8)) { this.type[0] = new ConduitType(tagRoot.getString("InfType")); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeBlock.java index c7b2dd7..38fa938 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeBlock.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeBlock.java @@ -5,7 +5,7 @@ import cyano.poweradvantage.api.simple.BlockSimpleFluidMachine; import cyano.poweradvantage.api.simple.TileEntitySimplePowerMachine; import net.minecraft.block.material.Material; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.inventory.IInventory; import net.minecraft.inventory.InventoryHelper; import net.minecraft.tileentity.TileEntity; @@ -29,7 +29,7 @@ public PoweredEntity createNewTileEntity(IBlockReader w) { * breaks. */ @Override - public void onReplaced(IBlockState state, World world, BlockPos coord, IBlockState newState, boolean isMoving) { + public void onReplaced(BlockState state, World world, BlockPos coord, BlockState newState, boolean isMoving) { if (state.getBlock() != newState.getBlock()) { final TileEntity tileEntity = world.getTileEntity(coord); if (tileEntity instanceof TileEntitySimplePowerMachine) { @@ -41,12 +41,12 @@ public void onReplaced(IBlockState state, World world, BlockPos coord, IBlockSta } @Override - public boolean hasComparatorInputOverride(IBlockState state) { + public boolean hasComparatorInputOverride(BlockState state) { return true; } @Override - public int getComparatorInputOverride(IBlockState state, World world, BlockPos coord) { + public int getComparatorInputOverride(BlockState state, World world, BlockPos coord) { TileEntity te = world.getTileEntity(coord); if (te != null && te instanceof FluidDischargeTileEntity) { return ((FluidDischargeTileEntity) te).getRedstoneOutput(); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeTileEntity.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeTileEntity.java index 6184efd..444317a 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeTileEntity.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeTileEntity.java @@ -8,10 +8,10 @@ import net.minecraft.fluid.Fluid; import net.minecraft.fluid.IFluidState; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.CompoundNBT; import net.minecraft.network.NetworkManager; -import net.minecraft.network.play.server.SPacketUpdateTileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.network.play.server.SUpdateTileEntityPacket; +import net.minecraft.util.Direction; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; import zone.moddev.mc.poweradvantage.init.Entities; @@ -28,9 +28,9 @@ public FluidDischargeTileEntity() { @Override public void powerUpdate() { BlockPos below = getPos().down(); - IAdvantageFluidHandler other = FluidHandlerHelper.getHandler(getWorld().getTileEntity(below), EnumFacing.UP); + IAdvantageFluidHandler other = FluidHandlerHelper.getHandler(getWorld().getTileEntity(below), Direction.UP); if (getTank().getFluidAmount() > 0 && other != null) { - int accepted = other.fill(EnumFacing.UP, getTank().getFluid(), true); + int accepted = other.fill(Direction.UP, getTank().getFluid(), true); if (accepted > 0) getTank().drain(accepted, true); } else if (getTank().getFluidAmount() >= FluidTank.BUCKET_VOLUME) { Fluid fluid = getTank().getFluid().getFluid(); @@ -50,12 +50,12 @@ public void powerUpdate() { private static BlockPos findPlacement(World world, BlockPos initial, Fluid fluid, int range) { if (world.isAirBlock(initial)) return initial; BlockPos position = initial; - EnumFacing[] directions = { EnumFacing.DOWN, EnumFacing.NORTH, EnumFacing.WEST, EnumFacing.SOUTH, EnumFacing.EAST }; + Direction[] directions = { Direction.DOWN, Direction.NORTH, Direction.WEST, Direction.SOUTH, Direction.EAST }; while (range-- > 0) { IFluidState state = world.getFluidState(position); if (state.isEmpty()) return position; java.util.List candidates = new java.util.ArrayList<>(); - for (EnumFacing direction : directions) { + for (Direction direction : directions) { BlockPos candidate = position.offset(direction); IFluidState candidateState = world.getFluidState(candidate); if (world.isAirBlock(candidate) || (!candidateState.isEmpty() @@ -80,16 +80,16 @@ private static BlockPos findPlacement(World world, BlockPos initial, Fluid fluid @Override public boolean isFluidSource() { return false; } @Override public boolean isFluidSink() { return true; } - @Override public NBTTagCompound getUpdateTag() { - NBTTagCompound tag = new NBTTagCompound(); + @Override public CompoundNBT getUpdateTag() { + CompoundNBT tag = new CompoundNBT(); getTank().writeToNBT(tag); return tag; } - @Override public void handleUpdateTag(NBTTagCompound tag) { getTank().readFromNBT(tag); } - @Override public SPacketUpdateTileEntity getUpdatePacket() { - return new SPacketUpdateTileEntity(getPos(), 0, getUpdateTag()); + @Override public void handleUpdateTag(CompoundNBT tag) { getTank().readFromNBT(tag); } + @Override public SUpdateTileEntityPacket getUpdatePacket() { + return new SUpdateTileEntityPacket(getPos(), 0, getUpdateTag()); } - @Override public void onDataPacket(NetworkManager net, SPacketUpdateTileEntity packet) { + @Override public void onDataPacket(NetworkManager net, SUpdateTileEntityPacket packet) { handleUpdateTag(packet.getNbtCompound()); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainBlock.java index 18b1c81..05bd1e2 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainBlock.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainBlock.java @@ -4,7 +4,7 @@ import cyano.poweradvantage.api.PoweredEntity; import cyano.poweradvantage.api.simple.BlockSimpleFluidMachine; import net.minecraft.block.material.Material; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.math.BlockPos; import net.minecraft.world.IBlockReader; @@ -23,12 +23,12 @@ public PoweredEntity createNewTileEntity(IBlockReader w) { } @Override - public boolean hasComparatorInputOverride(IBlockState state) { + public boolean hasComparatorInputOverride(BlockState state) { return true; } @Override - public int getComparatorInputOverride(IBlockState state, World world, BlockPos coord) { + public int getComparatorInputOverride(BlockState state, World world, BlockPos coord) { TileEntity te = world.getTileEntity(coord); if (te != null && te instanceof FluidDrainTileEntity) { return ((FluidDrainTileEntity) te).getRedstoneOutput(); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainTileEntity.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainTileEntity.java index 304a7e3..1d26e44 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainTileEntity.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainTileEntity.java @@ -8,10 +8,10 @@ import cyano.poweradvantage.api.simple.TileEntitySimpleFluidMachine; import net.minecraft.fluid.Fluid; import net.minecraft.fluid.IFluidState; -import net.minecraft.init.Blocks; +import net.minecraft.block.Blocks; import net.minecraft.item.ItemStack; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; import zone.moddev.mc.poweradvantage.init.Entities; @@ -25,9 +25,9 @@ public FluidDrainTileEntity() { @Override public void powerUpdate() { - if (getTank().getFluidAmount() > 0) tryPushFluid(getPos().down(), EnumFacing.UP); + if (getTank().getFluidAmount() > 0) tryPushFluid(getPos().down(), Direction.UP); - for (EnumFacing side : EnumFacing.values()) { + for (Direction side : Direction.values()) { BlockPos adjacent = getPos().offset(side); IAdvantageFluidHandler other = FluidHandlerHelper.getHandler( getWorld().getTileEntity(adjacent), side.getOpposite()); @@ -59,7 +59,7 @@ public void powerUpdate() { super.powerUpdate(); } - private void tryPushFluid(BlockPos position, EnumFacing face) { + private void tryPushFluid(BlockPos position, Direction face) { IAdvantageFluidHandler handler = FluidHandlerHelper.getHandler(getWorld().getTileEntity(position), face); if (handler == null || getTank().getFluid() == null) return; int accepted = handler.fill(face, getTank().getFluid(), true); @@ -82,12 +82,12 @@ private void tryPushFluid(BlockPos position, EnumFacing face) { public static BlockPos scanFluidSpaceForSourceBlock(World world, BlockPos initial, int range) { BlockPos position = initial; - EnumFacing[] directions = { EnumFacing.UP, EnumFacing.NORTH, EnumFacing.WEST, EnumFacing.SOUTH, EnumFacing.EAST }; + Direction[] directions = { Direction.UP, Direction.NORTH, Direction.WEST, Direction.SOUTH, Direction.EAST }; while (range-- > 0) { IFluidState state = world.getFluidState(position); if (!state.isEmpty() && state.isSource()) return position; java.util.List candidates = new java.util.ArrayList<>(); - for (EnumFacing direction : directions) { + for (Direction direction : directions) { BlockPos candidate = position.offset(direction); IFluidState candidateState = world.getFluidState(candidate); if (!candidateState.isEmpty() && (state.isEmpty() diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidPipeBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidPipeBlock.java index 8401b59..2ca24d5 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidPipeBlock.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidPipeBlock.java @@ -5,11 +5,11 @@ import zone.moddev.mc.poweradvantage.util.FluidHandlerHelper; import net.minecraft.block.Block; import net.minecraft.block.material.Material; -import net.minecraft.block.state.IBlockState; -import net.minecraft.entity.EntityLivingBase; +import net.minecraft.block.BlockState; +import net.minecraft.entity.LivingEntity; import net.minecraft.item.ItemStack; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; @@ -25,14 +25,14 @@ public FluidPipeBlock() { * Called when a neighboring block changes. */ @Override - public void neighborChanged(IBlockState state, World worldIn, BlockPos pos, Block blockIn, BlockPos fromPos) { + public void neighborChanged(BlockState state, World worldIn, BlockPos pos, Block blockIn, BlockPos fromPos, boolean isMoving) { pipeCheck(worldIn, pos); } /** * Called by ItemBlocks after a block is set in the world, to allow post-place logic */ - public void onBlockPlacedBy(World worldIn, BlockPos pos, IBlockState state, EntityLivingBase placer, ItemStack stack) { + public void onBlockPlacedBy(World worldIn, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) { super.onBlockPlacedBy(worldIn, pos, state, placer, stack); pipeCheck(worldIn, pos); } @@ -48,7 +48,7 @@ private void pipeCheck(World w, BlockPos pos) { protected static int numberOfAdjacentFluidHandlers(World w, BlockPos pos) { int sum = 0; - for (EnumFacing f : EnumFacing.values()) { + for (Direction f : Direction.values()) { TileEntity e = w.getTileEntity(pos.offset(f)); if (FluidHandlerHelper.hasHandler(e, f.getOpposite()) && !(e instanceof cyano.poweradvantage.api.PoweredEntity)) { sum++; diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankBlock.java index 44a93d7..0f28deb 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankBlock.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankBlock.java @@ -4,7 +4,7 @@ import cyano.poweradvantage.api.PoweredEntity; import cyano.poweradvantage.api.simple.BlockSimpleFluidMachine; import net.minecraft.block.material.Material; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; @@ -22,13 +22,13 @@ public PoweredEntity createNewTileEntity(IBlockReader world) { } @Override - public boolean hasComparatorInputOverride(IBlockState bs) { + public boolean hasComparatorInputOverride(BlockState bs) { return true; } @Override - public int getComparatorInputOverride(IBlockState bs, World world, BlockPos coord) { + public int getComparatorInputOverride(BlockState bs, World world, BlockPos coord) { TileEntity te = world.getTileEntity(coord); if (te != null && te instanceof MetalTankTileEntity) { return ((MetalTankTileEntity) te).getRedstoneOutput(); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillBlock.java index 9a8a8e8..68450ad 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillBlock.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillBlock.java @@ -6,9 +6,9 @@ import net.minecraft.block.material.Material; import net.minecraft.state.BooleanProperty; import net.minecraft.state.StateContainer; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.math.BlockPos; import net.minecraft.world.IBlockReader; import net.minecraft.world.IWorldReader; @@ -31,7 +31,7 @@ public StillBlock() { * Creates a blockstate */ @Override - protected void fillStateContainer(StateContainer.Builder builder) { + protected void fillStateContainer(StateContainer.Builder builder) { super.fillStateContainer(builder); builder.add(ACTIVE); } @@ -40,10 +40,10 @@ protected void fillStateContainer(StateContainer.Builder 0) { - NBTTagCompound tag = new NBTTagCompound(); - net.minecraft.util.ResourceLocation id = IRegistry.field_212619_h + CompoundNBT tag = new CompoundNBT(); + net.minecraft.util.ResourceLocation id = net.minecraftforge.registries.ForgeRegistries.FLUIDS .getKey(tile.getTank().getFluid().getFluid()); - if (id != null) tag.setString("FluidID", id.toString()); - tag.setInt("Volume", tile.getTank().getFluidAmount()); + if (id != null) tag.putString("FluidID", id.toString()); + tag.putInt("Volume", tile.getTank().getFluidAmount()); stack.setTag(tag); } return stack; @@ -78,13 +69,13 @@ public ItemStack createItem(StorageTankTileEntity tile) { @Override public void addInformation(ItemStack stack, @Nullable IBlockReader world, List tooltip, ITooltipFlag flag) { - NBTTagCompound tag = stack.getTag(); + CompoundNBT tag = stack.getTag(); if (tag == null || !tag.contains("FluidID", 8) || !tag.contains("Volume", 99)) { - tooltip.add(new TextComponentTranslation("tooltip.poweradvantage.empty")); + tooltip.add(new TranslationTextComponent("tooltip.poweradvantage.empty")); return; } float buckets = tag.getInt("Volume") / (float) FluidTank.BUCKET_VOLUME; - tooltip.add(new TextComponentString(buckets + " buckets of " + tag.getString("FluidID"))); + tooltip.add(new StringTextComponent(buckets + " buckets of " + tag.getString("FluidID"))); } /** Retained for callers from the 1.12 implementation. */ diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java index ae7ef63..9713187 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java @@ -6,11 +6,11 @@ import zone.moddev.mc.poweradvantage.util.FluidHandlerHelper; import net.minecraft.block.Block; import net.minecraft.block.ITileEntityProvider; -import net.minecraft.block.state.IBlockState; -import net.minecraft.entity.EntityLivingBase; +import net.minecraft.block.BlockState; +import net.minecraft.entity.LivingEntity; import net.minecraft.item.ItemStack; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.math.BlockPos; import net.minecraft.world.IBlockReader; import net.minecraft.world.World; @@ -38,14 +38,14 @@ public boolean isPowerSource(ConduitType powerType) { * Called when a neighboring block changes. */ @Override - public void neighborChanged(IBlockState state, World worldIn, BlockPos pos, Block blockIn, BlockPos fromPos) { + public void neighborChanged(BlockState state, World worldIn, BlockPos pos, Block blockIn, BlockPos fromPos, boolean isMoving) { pipeCheck(worldIn, pos); } /** * Called by ItemBlocks after a block is set in the world, to allow post-place logic */ - public void onBlockPlacedBy(World worldIn, BlockPos pos, IBlockState state, EntityLivingBase placer, ItemStack stack) { + public void onBlockPlacedBy(World worldIn, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) { super.onBlockPlacedBy(worldIn, pos, state, placer, stack); pipeCheck(worldIn, pos); } @@ -73,7 +73,7 @@ private void pipeCheck(World w, BlockPos pos) { * method. */ @Override - protected boolean canConnectTo(IBlockReader w, BlockPos thisBlock, IBlockState bs, EnumFacing face) { + protected boolean canConnectTo(IBlockReader w, BlockPos thisBlock, BlockState bs, Direction face) { BlockPos otherBlock = thisBlock.offset(face); return super.canConnectTo(w, thisBlock, bs, face) || FluidHandlerHelper.hasHandler(w.getTileEntity(otherBlock), face.getOpposite()); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java index 8041177..6957a87 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java @@ -11,7 +11,7 @@ import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; import net.minecraft.fluid.Fluid; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry; import zone.moddev.mc.poweradvantage.init.Entities; import zone.moddev.mc.poweradvantage.init.Fluids; @@ -19,7 +19,7 @@ /** Bridges adjacent Advantage fluid handlers onto the conduit request network. */ public class TerminalFluidPipeTileEntity extends PoweredEntity { - private final EnumFacing[] faces = EnumFacing.values(); + private final Direction[] faces = Direction.values(); private final IAdvantageFluidHandler[] neighbors = new IAdvantageFluidHandler[faces.length]; public TerminalFluidPipeTileEntity() { super(Entities.PIPE_TERMINAL); } @@ -56,7 +56,7 @@ public class TerminalFluidPipeTileEntity extends PoweredEntity { for (int index = 0; index < neighbors.length; index++) { IAdvantageFluidHandler neighbor = neighbors[index]; if (neighbor == null) continue; - EnumFacing face = faces[index].getOpposite(); + Direction face = faces[index].getOpposite(); FluidStack available = neighbor.drain(face, Integer.MAX_VALUE, false); if (available == null || available.amount <= 0) continue; int moved = transmitFluidToConsumers(available, PowerRequest.LOW_PRIORITY); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/math/BlockPos4D.java b/src/main/java/zone/moddev/mc/poweradvantage/math/BlockPos4D.java index 9e63f0c..9fe9bfe 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/math/BlockPos4D.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/math/BlockPos4D.java @@ -1,8 +1,8 @@ package zone.moddev.mc.poweradvantage.math; -import net.minecraft.nbt.INBTBase; -import net.minecraft.nbt.NBTTagIntArray; -import net.minecraft.util.EnumFacing; +import net.minecraft.nbt.INBT; +import net.minecraft.nbt.IntArrayNBT; +import net.minecraft.util.Direction; import net.minecraft.util.math.BlockPos; /** @@ -110,7 +110,7 @@ public boolean equals(Object other) { * @param distance The direction to move the coordinate * @return A new BlockPos4D representing the new position */ - public BlockPos4D offset(final EnumFacing direction, final int distance) { + public BlockPos4D offset(final Direction direction, final int distance) { return new BlockPos4D(this.dimension, this.getX() + direction.getXOffset() * distance, this.getY() + direction.getYOffset() * distance, @@ -123,7 +123,7 @@ public BlockPos4D offset(final EnumFacing direction, final int distance) { * @param direction The direction to move the coordinate * @return A new BlockPos4D representing the new position */ - public BlockPos4D offset(final EnumFacing direction) { + public BlockPos4D offset(final Direction direction) { return offset(direction, 1); } @@ -264,24 +264,24 @@ public BlockPos4D toDimension(int newDimension) { } public BlockPos4D[] getNeighbors() { - BlockPos4D[] n = new BlockPos4D[EnumFacing.values().length]; + BlockPos4D[] n = new BlockPos4D[Direction.values().length]; for (int i = 0; i < n.length; i++) { - n[i] = this.offset(EnumFacing.values()[i]); + n[i] = this.offset(Direction.values()[i]); } return n; } - public INBTBase toNBT() { + public INBT toNBT() { int[] data = new int[4]; data[0] = dimension; data[1] = pos.getX(); data[2] = pos.getY(); data[3] = pos.getZ(); - return new NBTTagIntArray(data); + return new IntArrayNBT(data); } - public static BlockPos4D fromNBT(INBTBase tag) { - int[] data = ((NBTTagIntArray) tag).getIntArray(); + public static BlockPos4D fromNBT(INBT tag) { + int[] data = ((IntArrayNBT) tag).getIntArray(); return new BlockPos4D(data[0], data[1], data[2], data[3]); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrations.java b/src/main/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrations.java index 9d14b67..63bec5a 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrations.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrations.java @@ -3,8 +3,8 @@ import cyano.poweradvantage.api.migration.LegacyMigrationApi; import cyano.poweradvantage.api.simple.BlockSimplePowerMachine; import net.minecraft.block.Block; -import net.minecraft.block.BlockFlowingFluid; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.FlowingFluidBlock; +import net.minecraft.block.BlockState; import net.minecraft.util.ResourceLocation; import zone.moddev.mc.poweradvantage.PowerAdvantage; import zone.moddev.mc.poweradvantage.blocks.BlockPowerSwitch; @@ -72,7 +72,7 @@ private static void registerTile(String path) { LegacyMigrationApi.registerTileEntityAlias("minecraft:" + PowerAdvantage.MODID + "." + path, current); } - private static IBlockState decodeState(Block block, int metadata) { + private static BlockState decodeState(Block block, int metadata) { if (block instanceof StillBlock) return ((StillBlock) block).getStateFromMeta(metadata); if (block instanceof BlockConveyor) return ((BlockConveyor) block).getStateFromMeta(metadata); if (block instanceof BlockConveyorFilter) return ((BlockConveyorFilter) block).getStateFromMeta(metadata); @@ -80,8 +80,8 @@ private static IBlockState decodeState(Block block, int metadata) { if (block instanceof BlockSimplePowerMachine) { return ((BlockSimplePowerMachine) block).getStateFromMeta(metadata); } - if (block instanceof BlockFlowingFluid) { - return block.getDefaultState().with(BlockFlowingFluid.LEVEL, Integer.valueOf(metadata & 15)); + if (block instanceof FlowingFluidBlock) { + return block.getDefaultState().with(FlowingFluidBlock.LEVEL, Integer.valueOf(metadata & 15)); } return block.getDefaultState(); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook.java b/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook.java index a8fc504..8f865d3 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook.java @@ -28,11 +28,11 @@ import cpw.mods.modlauncher.api.INameMappingService; import cyano.poweradvantage.api.migration.LegacyMigrationApi; import net.minecraft.block.Block; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.INBTBase; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.INBT; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.ListNBT; import net.minecraft.nbt.NBTUtil; import net.minecraft.util.ResourceLocation; import net.minecraft.util.datafix.fixes.BlockStateFlatteningMap; @@ -52,7 +52,7 @@ public final class LegacyWorldDataHook implements WorldPersistenceHooks.WorldPersistenceHook { private static final LegacyWorldDataHook INSTANCE = new LegacyWorldDataHook(); private static final ResourceLocation BLOCK_REGISTRY = new ResourceLocation("minecraft", "blocks"); - private static final Map LEGACY_WORLD_DATA = new ConcurrentHashMap<>(); + private static final Map LEGACY_WORLD_DATA = new ConcurrentHashMap<>(); private static final BitSet LEGACY_BLOCK_IDS = new BitSet(); private static final Map LEGACY_ENCHANTMENTS = new LinkedHashMap<>(); private static final Set LEGACY_CHUNKS = Collections.newSetFromMap(new ConcurrentHashMap<>()); @@ -79,10 +79,11 @@ public static synchronized void prepareLegacyWorld(File levelDat) { LEGACY_ENCHANTMENTS.clear(); LEGACY_CHUNKS.clear(); LEGACY_BLOCK_COUNTS.clear(); + removeRetiredProfessionSnapshot(levelDat); File source = legacyRegistrySource(levelDat); if (source == null) return; try (FileInputStream input = new FileInputStream(source)) { - NBTTagCompound root = CompressedStreamTools.readCompressed(input); + CompoundNBT root = CompressedStreamTools.readCompressed(input); if (!root.contains("FML", 10)) return; prepareLegacyData(levelDat.getParentFile(), root.getCompound("FML")); migrateLoosePlayerData(levelDat, root); @@ -91,61 +92,100 @@ public static synchronized void prepareLegacyWorld(File levelDat) { } } + private static void removeRetiredProfessionSnapshot(File levelDat) { + if (!levelDat.isFile()) return; + try { + CompoundNBT root; + try (FileInputStream input = new FileInputStream(levelDat)) { + root = CompressedStreamTools.readCompressed(input); + } + if (removeRetiredVanillaProfessionRegistry(root)) writeWithBackup(levelDat, root); + } catch (IOException exception) { + throw new IllegalStateException("Could not prepare the old villager registry in " + levelDat, exception); + } + } + + static boolean removeRetiredVanillaProfessionRegistry(CompoundNBT root) { + boolean changed = false; + for (String namespace : new String[] {"fml", "FML"}) { + CompoundNBT registries = root.getCompound(namespace).getCompound("Registries"); + String key = "minecraft:villagerprofessions"; + if (!registries.contains(key, 10)) continue; + CompoundNBT snapshot = registries.getCompound(key); + if (!snapshot.contains("ids", 9)) continue; + boolean vanillaOnly = true; + for (INBT entry : (ListNBT) snapshot.get("ids")) { + if (!(entry instanceof CompoundNBT) || !Arrays.asList("minecraft:farmer", "minecraft:librarian", + "minecraft:priest", "minecraft:smith", "minecraft:butcher", "minecraft:nitwit") + .contains(((CompoundNBT) entry).getString("K"))) vanillaOnly = false; + } + for (String list : new String[] {"aliases", "overrides", "dummied"}) { + if (snapshot.contains(list, 9) && !((ListNBT) snapshot.get(list)).isEmpty()) vanillaOnly = false; + } + if (snapshot.getIntArray("blocked").length > 0 || !vanillaOnly) continue; + // Vanilla fixes the villagers and offers. Remove only the retired vanilla ID index. + registries.remove(key); + changed = true; + } + return changed; + } + /** Called by the coremod immediately after a legacy chunk NBT is read. */ - public static synchronized void prepareLegacyChunk(NBTTagCompound root) { + public static synchronized void prepareLegacyChunk(CompoundNBT root) { if (root == null) return; migrateLegacyNbt(root); migrateLegacyEntities(root); if (!legacyWorldActive || !root.contains("Level", 10)) return; - NBTTagCompound level = root.getCompound("Level"); + CompoundNBT level = root.getCompound("Level"); int legacyBlocks = countLegacyBlocks(level); if (legacyBlocks > 0) { LEGACY_BLOCK_COUNTS.put(Long.valueOf(chunkKey(level.getInt("xPos"), level.getInt("zPos"))), Integer.valueOf(legacyBlocks)); } - level.setBoolean("TerrainPopulated", true); - level.setBoolean("LightPopulated", true); - level.setBoolean(PRESERVE_CHUNK_MARKER, true); + level.putBoolean("TerrainPopulated", true); + level.putBoolean("LightPopulated", true); + level.putBoolean(PRESERVE_CHUNK_MARKER, true); } - private static void migrateLegacyEntities(NBTTagCompound root) { + private static void migrateLegacyEntities(CompoundNBT root) { int version = root.contains("DataVersion", 99) ? root.getInt("DataVersion") : -1; if (version >= LAST_PRE_FLATTENING_VERSION || !root.contains("Level", 10)) return; DataFixer fixer = DataFixesManager.getDataFixer(); Set vanillaIds = fixer.getSchema(DataFixUtils.makeKey(version)) .findChoiceType(TypeReferences.ENTITY).types().keySet(); - NBTTagList entities = root.getCompound("Level").getList("Entities", 10); + ListNBT entities = root.getCompound("Level").getList("Entities", 10); for (int index = 0; index < entities.size(); index++) { entities.set(index, migrateVanillaEntity(entities.getCompound(index), version, fixer, vanillaIds)); } } - private static NBTTagCompound migrateVanillaEntity(NBTTagCompound entity, int version, + private static CompoundNBT migrateVanillaEntity(CompoundNBT entity, int version, DataFixer fixer, Set vanillaIds) { boolean hasPassengers = entity.contains("Passengers", 9); - NBTTagList passengers = entity.getList("Passengers", 10); + ListNBT passengers = entity.getList("Passengers", 10); // An unknown mod choice can stop a mixed chunk's early entity-ID fixes. // Fix each recognised vanilla entity independently, without its passengers. if (vanillaIds.contains(entity.getString("id"))) { - NBTTagCompound isolated = entity.copy(); - isolated.removeTag("Passengers"); - entity = NBTUtil.update(fixer, TypeReferences.ENTITY_TYPE, isolated, - version, LAST_PRE_FLATTENING_VERSION); + CompoundNBT isolated = entity.copy(); + isolated.remove("Passengers"); + entity = (CompoundNBT) fixer.update(TypeReferences.ENTITY_TYPE, + new Dynamic<>(net.minecraft.nbt.NBTDynamicOps.INSTANCE, isolated), + version, LAST_PRE_FLATTENING_VERSION).getValue(); } for (int index = 0; index < passengers.size(); index++) { passengers.set(index, migrateVanillaEntity(passengers.getCompound(index), version, fixer, vanillaIds)); } - if (hasPassengers) entity.setTag("Passengers", passengers); + if (hasPassengers) entity.put("Passengers", passengers); return entity; } /** Called after vanilla datafixing, before a legacy chunk is returned. */ - public static NBTTagCompound finalizeLegacyChunk(NBTTagCompound root) { + public static CompoundNBT finalizeLegacyChunk(CompoundNBT root) { if (root != null && root.contains("Level", 10)) { - NBTTagCompound level = root.getCompound("Level"); + CompoundNBT level = root.getCompound("Level"); if (level.getBoolean(PRESERVE_CHUNK_MARKER)) { - level.setString("Status", "postprocessed"); - level.removeTag(PRESERVE_CHUNK_MARKER); + level.putString("Status", "full"); + level.remove(PRESERVE_CHUNK_MARKER); } } return root; @@ -159,19 +199,19 @@ public static boolean shouldBlockWorldgenWrite(net.minecraft.util.math.BlockPos @Override public String getModId() { return "FML"; } @Override - public NBTTagCompound getDataForWriting(SaveHandler handler, WorldInfo info) { - NBTTagCompound legacy = LEGACY_WORLD_DATA.get(worldKey(handler.getWorldDirectory())); - return legacy == null ? new NBTTagCompound() : legacy.copy(); + public CompoundNBT getDataForWriting(SaveHandler handler, WorldInfo info) { + CompoundNBT legacy = LEGACY_WORLD_DATA.get(worldKey(handler.getWorldDirectory())); + return legacy == null ? new CompoundNBT() : legacy.copy(); } @Override - public void readData(SaveHandler handler, WorldInfo info, NBTTagCompound tag) { + public void readData(SaveHandler handler, WorldInfo info, CompoundNBT tag) { prepareLegacyData(handler.getWorldDirectory(), tag); } - private static synchronized void prepareLegacyData(File worldDirectory, NBTTagCompound tag) { + private static synchronized void prepareLegacyData(File worldDirectory, CompoundNBT tag) { if (!tag.contains("Registries", 10)) return; - NBTTagCompound registries = tag.getCompound("Registries"); + CompoundNBT registries = tag.getCompound("Registries"); if (!registries.contains(BLOCK_REGISTRY.toString(), 10)) return; installLegacyEnchantments(registries); String key = worldKey(worldDirectory); @@ -187,13 +227,13 @@ private static synchronized void prepareLegacyData(File worldDirectory, NBTTagCo } } - private static int installLegacyBlockStates(NBTTagCompound snapshot) { + private static int installLegacyBlockStates(CompoundNBT snapshot) { LEGACY_BLOCK_IDS.clear(); Map ids = new LinkedHashMap<>(); - NBTTagList savedIds = snapshot.getList("ids", 10); + ListNBT savedIds = snapshot.getList("ids", 10); int highestState = 0; for (int index = 0; index < savedIds.size(); index++) { - NBTTagCompound entry = savedIds.getCompound(index); + CompoundNBT entry = savedIds.getCompound(index); ResourceLocation id = safeId(normalizeLegacyRegistryName(entry.getString("K"))); if (id == null || !LegacyMigrationApi.handlesNamespace(id.getNamespace())) continue; int numeric = entry.getInt("V"); @@ -213,7 +253,7 @@ private static int installLegacyBlockStates(NBTTagCompound snapshot) { + " (expected " + target + ")"); } for (int metadata = 0; metadata < 16; metadata++) { - IBlockState state = LegacyMigrationApi.decodeBlockState(entry.getKey(), block, metadata); + BlockState state = LegacyMigrationApi.decodeBlockState(entry.getKey(), block, metadata); table[(entry.getValue().intValue() << 4) | metadata] = BlockStateFlatteningMap.makeDynamic(NBTUtil.writeBlockState(state).toString()); mapped++; @@ -222,56 +262,56 @@ private static int installLegacyBlockStates(NBTTagCompound snapshot) { return mapped; } - private static void installLegacyEnchantments(NBTTagCompound registries) { + private static void installLegacyEnchantments(CompoundNBT registries) { LEGACY_ENCHANTMENTS.clear(); - NBTTagList entries = registries.getCompound("minecraft:enchantments").getList("ids", 10); + ListNBT entries = registries.getCompound("minecraft:enchantments").getList("ids", 10); for (int index = 0; index < entries.size(); index++) { - NBTTagCompound entry = entries.getCompound(index); + CompoundNBT entry = entries.getCompound(index); ResourceLocation name = safeId(entry.getString("K")); if (name != null) LEGACY_ENCHANTMENTS.put(Integer.valueOf(entry.getInt("V")), name.toString()); } } /** Resolve saved IDs, not today's registry order or vanilla's conflicting numeric IDs. */ - static boolean migrateLegacyEnchantments(NBTTagCompound compound, Map savedIds) { + static boolean migrateLegacyEnchantments(CompoundNBT compound, Map savedIds) { boolean changed = migrateEnchantmentList(compound, "StoredEnchantments", savedIds); changed |= migrateEnchantmentList(compound, "Enchantments", savedIds); if (compound.contains("ench", 9)) { - NBTTagList enchantments = compound.getList("ench", 10); + ListNBT enchantments = compound.getList("ench", 10); if (!enchantments.isEmpty() && !savedIds.isEmpty()) { // A partial conversion would leave an unreadable mixed old/new list. for (int index = 0; index < enchantments.size(); index++) { - NBTTagCompound enchantment = enchantments.getCompound(index); + CompoundNBT enchantment = enchantments.getCompound(index); if (enchantment.contains("id", 99) && !savedIds.containsKey(Integer.valueOf(enchantment.getInt("id")))) { throw new IllegalStateException("Missing saved enchantment mapping for " + enchantment.getInt("id")); } } migrateEnchantmentList(compound, "ench", savedIds); - compound.setTag("Enchantments", enchantments); - compound.removeTag("ench"); + compound.put("Enchantments", enchantments); + compound.remove("ench"); changed = true; } } return changed; } - private static boolean migrateEnchantmentList(NBTTagCompound compound, String key, Map savedIds) { + private static boolean migrateEnchantmentList(CompoundNBT compound, String key, Map savedIds) { boolean changed = false; - NBTTagList list = compound.getList(key, 10); + ListNBT list = compound.getList(key, 10); for (int index = 0; index < list.size(); index++) { - NBTTagCompound enchantment = list.getCompound(index); + CompoundNBT enchantment = list.getCompound(index); if (!enchantment.contains("id", 99)) continue; String name = savedIds.get(Integer.valueOf(enchantment.getInt("id"))); if (name != null) { - enchantment.setString("id", name); + enchantment.putString("id", name); changed = true; } } return changed; } - private static void migrateLoosePlayerData(File levelDat, NBTTagCompound root) throws IOException { + private static void migrateLoosePlayerData(File levelDat, CompoundNBT root) throws IOException { boolean changed = migrateLegacyNbt(root) > 0; if (changed && levelDat.isFile()) writeWithBackup(levelDat, root); File playerData = new File(levelDat.getParentFile(), "playerdata"); @@ -280,20 +320,20 @@ private static void migrateLoosePlayerData(File levelDat, NBTTagCompound root) t Arrays.sort(files); for (File file : files) { try (FileInputStream input = new FileInputStream(file)) { - NBTTagCompound player = CompressedStreamTools.readCompressed(input); + CompoundNBT player = CompressedStreamTools.readCompressed(input); if (migrateLegacyNbt(player) > 0) writeWithBackup(file, player); } } } - private static int migrateLegacyNbt(INBTBase value) { + private static int migrateLegacyNbt(INBT value) { int changed = 0; - if (value instanceof NBTTagCompound) { - NBTTagCompound compound = (NBTTagCompound) value; + if (value instanceof CompoundNBT) { + CompoundNBT compound = (CompoundNBT) value; String id = compound.getString("id"); String vanillaBlockEntity = VANILLA_BLOCK_ENTITY_IDS.get(id); if (vanillaBlockEntity != null) { - compound.setString("id", vanillaBlockEntity); + compound.putString("id", vanillaBlockEntity); id = vanillaBlockEntity; changed++; } @@ -301,19 +341,19 @@ private static int migrateLegacyNbt(INBTBase value) { String tileTarget = LegacyMigrationApi.tileTarget(id); String entityTarget = LegacyMigrationApi.entityTarget(tileTarget); if (!entityTarget.equals(normalizeIdentity(id))) { - compound.setString("id", entityTarget); + compound.putString("id", entityTarget); id = entityTarget; changed++; } if ("forge:bucketfilled".equalsIgnoreCase(id) && compound.contains("tag", 10)) { - NBTTagCompound tag = compound.getCompound("tag"); + CompoundNBT tag = compound.getCompound("tag"); ResourceLocation bucket = LegacyMigrationApi.bucketTarget(tag.getString("FluidName")); if (bucket != null) { - compound.setString("id", bucket.toString()); - tag.removeTag("FluidName"); - tag.removeTag("Amount"); - if (tag.isEmpty()) compound.removeTag("tag"); + compound.putString("id", bucket.toString()); + tag.remove("FluidName"); + tag.remove("Amount"); + if (tag.isEmpty()) compound.remove("tag"); changed++; } } else if (compound.contains("Count", 99)) { @@ -321,16 +361,16 @@ private static int migrateLegacyNbt(INBTBase value) { if (oldId != null && LegacyMigrationApi.handlesNamespace(oldId.getNamespace())) { ResourceLocation target = LegacyMigrationApi.itemTarget(oldId); if (!target.equals(oldId)) { - compound.setString("id", target.toString()); + compound.putString("id", target.toString()); changed++; } if (LegacyMigrationApi.isDurableItem(oldId) && compound.contains("Damage", 99) && compound.getInt("Damage") > 0) { - NBTTagCompound tag = compound.contains("tag", 10) - ? compound.getCompound("tag") : new NBTTagCompound(); + CompoundNBT tag = compound.contains("tag", 10) + ? compound.getCompound("tag") : new CompoundNBT(); if (!tag.contains("Damage", 99)) { - tag.setInt("Damage", compound.getInt("Damage")); - compound.setTag("tag", tag); + tag.putInt("Damage", compound.getInt("Damage")); + compound.put("tag", tag); changed++; } } @@ -342,22 +382,22 @@ private static int migrateLegacyNbt(INBTBase value) { if (migrateLegacyEnchantments(compound, LEGACY_ENCHANTMENTS)) changed++; if (LegacyMigrationApi.migrateRegisteredNbt(compound)) changed++; for (String key : new ArrayList<>(compound.keySet())) { - INBTBase child = compound.getTag(key); + INBT child = compound.get(key); if (child != null) changed += migrateLegacyNbt(child); } - } else if (value instanceof NBTTagList) { - NBTTagList list = (NBTTagList) value; + } else if (value instanceof ListNBT) { + ListNBT list = (ListNBT) value; for (int index = 0; index < list.size(); index++) changed += migrateLegacyNbt(list.get(index)); } return changed; } - private static int migrateFluidName(NBTTagCompound compound, String key) { + private static int migrateFluidName(CompoundNBT compound, String key) { if (!compound.contains(key, 8)) return 0; String oldName = compound.getString(key); String target = LegacyMigrationApi.fluidTarget(oldName); if (target.equals(normalizeIdentity(oldName))) return 0; - compound.setString(key, target); + compound.putString(key, target); return 1; } @@ -365,15 +405,15 @@ private static File legacyRegistrySource(File levelDat) { for (File candidate : new File[] { levelDat, new File(levelDat.getParentFile(), "level.dat_old") }) { if (!candidate.isFile()) continue; try (FileInputStream input = new FileInputStream(candidate)) { - NBTTagCompound root = CompressedStreamTools.readCompressed(input); + CompoundNBT root = CompressedStreamTools.readCompressed(input); if (root.contains("FML", 10) && containsLegacyRegistryEntry(root)) return candidate; } catch (IOException ignored) {} } return null; } - private static boolean containsLegacyRegistryEntry(NBTTagCompound root) { - NBTTagList ids = root.getCompound("FML").getCompound("Registries") + private static boolean containsLegacyRegistryEntry(CompoundNBT root) { + ListNBT ids = root.getCompound("FML").getCompound("Registries") .getCompound(BLOCK_REGISTRY.toString()).getList("ids", 10); for (int index = 0; index < ids.size(); index++) { ResourceLocation id = safeId(normalizeLegacyRegistryName(ids.getCompound(index).getString("K"))); @@ -382,11 +422,11 @@ private static boolean containsLegacyRegistryEntry(NBTTagCompound root) { return false; } - private static int countLegacyBlocks(NBTTagCompound level) { + private static int countLegacyBlocks(CompoundNBT level) { int found = 0; - NBTTagList sections = level.getList("Sections", 10); + ListNBT sections = level.getList("Sections", 10); for (int sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++) { - NBTTagCompound section = sections.getCompound(sectionIndex); + CompoundNBT section = sections.getCompound(sectionIndex); byte[] blocks = section.getByteArray("Blocks"); if (blocks.length != 4096) continue; byte[] add = section.getByteArray("Add"); @@ -454,7 +494,7 @@ private static Dynamic[] expandFlatteningTable(int requiredLength) { } } - private static void writeWithBackup(File file, NBTTagCompound data) throws IOException { + private static void writeWithBackup(File file, CompoundNBT data) throws IOException { File backup = new File(file.getParentFile(), file.getName() + ".poweradvantage-legacy-backup"); if (!backup.exists()) Files.copy(file.toPath(), backup.toPath(), StandardCopyOption.COPY_ATTRIBUTES); File temporary = new File(file.getParentFile(), file.getName() + ".poweradvantage.tmp"); @@ -473,7 +513,7 @@ private static void writeMigrationReport(File worldDirectory, int states, int ch File report = new File(worldDirectory, "poweradvantage-migration-report.txt"); if (report.exists()) return; List lines = Arrays.asList( - "Advantage 1.13 legacy migration", + "Advantage 1.14 legacy migration", "Prepared block states: " + states, "Protected existing chunks: " + chunks, "Namespaces: " + String.join(", ", LegacyMigrationApi.namespaces()), diff --git a/src/main/java/zone/moddev/mc/poweradvantage/recipe/MaterialTags.java b/src/main/java/zone/moddev/mc/poweradvantage/recipe/MaterialTags.java index 07d33c4..0b68956 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/recipe/MaterialTags.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/recipe/MaterialTags.java @@ -14,29 +14,46 @@ public final class MaterialTags { private MaterialTags() {} + public static void onTagsUpdated(net.minecraftforge.event.TagsUpdatedEvent event) { + net.minecraft.server.MinecraftServer server = net.minecraftforge.fml.server.ServerLifecycleHooks.getCurrentServer(); + // A remote client's local config must never change tags supplied by its server. + if (server != null && server.getNetworkTagManager() == event.getTagManager()) finishReload(); + } + public static void finishReload() { // DedicatedServer constructs an initial vanilla resource manager before FML loads mods. if (ModList.get() == null || Items.bioplastic_ingot == null) return; TagCollection tags = ItemTags.getCollection(); - if (PowerAdvantage.plasticIsAlsoRubber) { - append(tags, "forge:rubber", java.util.Collections.singleton(Items.bioplastic_ingot)); - append(tags, "forge:ingots/rubber", java.util.Collections.singleton(Items.bioplastic_ingot)); + applyPolicy(tags, Items.bioplastic_ingot, PowerAdvantage.plasticIsAlsoRubber, + PowerAdvantage.recipeMode == RecipeMode.NORMAL && ModList.get().isLoaded("electricadvantage")); + ItemTags.setCollection(tags); + } + + static void applyPolicy(TagCollection tags, Item plastic, boolean rubber, boolean solder) { + if (!rubber && !solder) return; + // Resolved 1.14 collections are immutable. Rebuild through their native registration API. + java.util.Map> builders = new java.util.LinkedHashMap<>(); + tags.getTagMap().forEach((id, tag) -> { + Tag.Builder builder = Tag.Builder.create(); + tag.getAllElements().forEach(builder::add); + builders.put(id, builder); + }); + if (rubber) { + append(builders, "forge:rubber", java.util.Collections.singleton(plastic)); + append(builders, "forge:ingots/rubber", java.util.Collections.singleton(plastic)); } - if (PowerAdvantage.recipeMode == RecipeMode.NORMAL && ModList.get().isLoaded("electricadvantage")) { + if (solder) { for (String metal : new String[] {"lead", "tin", "silver"}) { - append(tags, "forge:solder", tags.getOrCreate(new ResourceLocation("forge", "ingots/" + metal)).getAllElements()); + append(builders, "forge:solder", tags.getOrCreate(new ResourceLocation("forge", "ingots/" + metal)).getAllElements()); } } - // Refresh wrappers after replacing immutable tags. The same collection is - // also serialized by NetworkTagManager, so clients receive the server policy. - ItemTags.setCollection(tags); + tags.registerAll(builders); } - private static void append(TagCollection tags, String id, java.util.Collection additions) { + private static void append(java.util.Map> builders, + String id, java.util.Collection additions) { ResourceLocation name = new ResourceLocation(id); - Tag.Builder builder = Tag.Builder.create(); - for (Item item : tags.getOrCreate(name).getAllElements()) builder.add(item); + Tag.Builder builder = builders.computeIfAbsent(name, key -> Tag.Builder.create()); for (Item item : additions) if (item != null) builder.add(item); - tags.getTagMap().put(name, builder.build(name)); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/recipe/PartsLoot.java b/src/main/java/zone/moddev/mc/poweradvantage/recipe/PartsLoot.java index 1172c1a..97a9e24 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/recipe/PartsLoot.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/recipe/PartsLoot.java @@ -1,61 +1,41 @@ package zone.moddev.mc.poweradvantage.recipe; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import net.minecraft.util.ResourceLocation; -import net.minecraft.world.storage.loot.LootTable; -import net.minecraft.world.storage.loot.LootTableManager; -import net.minecraftforge.common.ForgeHooks; +import net.minecraft.world.storage.loot.ConstantRange; +import net.minecraft.world.storage.loot.LootPool; +import net.minecraft.world.storage.loot.TableLootEntry; +import net.minecraftforge.event.LootTableLoadEvent; import net.minecraftforge.fml.ModList; import zone.moddev.mc.poweradvantage.PowerAdvantage; -/** Native legacy parts, confined to original vanilla chest data without ALT. */ +/** Forge emits this event for bundled tables, leaving user data-pack tables untouched. */ public final class PartsLoot { private static final Set CHESTS = new HashSet<>(Arrays.asList( - "chests/abandoned_mineshaft", "chests/simple_dungeon", "chests/village_blacksmith", + "chests/abandoned_mineshaft", "chests/simple_dungeon", "chests/village/village_toolsmith", "chests/stronghold_corridor", "chests/stronghold_crossing", "chests/end_city_treasure")); private PartsLoot() {} - /** Forge 25 treats the Default pack as custom and skips LootTableLoadEvent. */ - public static LootTable loadLootTable(Gson gson, ResourceLocation name, String data, - boolean defaultPack, LootTableManager manager) { - String path = name.getPath(); - if (path.startsWith("loot_tables/") && path.endsWith(".json")) - path = path.substring("loot_tables/".length(), path.length() - ".json".length()); - if (defaultPack && name.getNamespace().equals("minecraft") && CHESTS.contains(path) - && ModList.get() != null && ModList.get().isLoaded("poweradvantage") - && PowerAdvantage.chestLootFactor > 0 && !hasAdditionalLootTables()) { - JsonObject table = new JsonParser().parse(data).getAsJsonObject(); - JsonArray pools = table.getAsJsonArray("pools"); - if (pools == null) { pools = new JsonArray(); table.add("pools", pools); } - for (String mod : new String[] {"poweradvantage", "steamadvantage", "electricadvantage"}) { - if (!ModList.get().isLoaded(mod) || (mod.equals("poweradvantage") && path.equals("chests/end_city_treasure"))) continue; - String poolName = mod + ":legacy_parts"; - boolean exists = false; - for (com.google.gson.JsonElement pool : pools) - if (pool.getAsJsonObject().has("name") && poolName.equals(pool.getAsJsonObject().get("name").getAsString())) exists = true; - if (exists) continue; - JsonObject pool = new JsonObject(); - pool.addProperty("name", poolName); - pool.addProperty("rolls", 1); - JsonObject entry = new JsonObject(); - entry.addProperty("type", "loot_table"); - entry.addProperty("name", mod + ":chests/advantage_parts"); - entry.addProperty("weight", 1); - JsonArray entries = new JsonArray(); - entries.add(entry); - pool.add("entries", entries); - pools.add(pool); - } - data = table.toString(); + public static void onLoad(LootTableLoadEvent event) { + addPools(event.getTable(), event.getName(), PowerAdvantage.chestLootFactor > 0 && !hasAdditionalLootTables(), + mod -> ModList.get().isLoaded(mod)); + } + + static void addPools(net.minecraft.world.storage.loot.LootTable table, ResourceLocation name, + boolean enabled, java.util.function.Predicate loaded) { + if (!"minecraft".equals(name.getNamespace()) || !CHESTS.contains(name.getPath()) + || !enabled) return; + for (String mod : new String[] {"poweradvantage", "steamadvantage", "electricadvantage"}) { + if (!loaded.test(mod) + || (mod.equals("poweradvantage") && name.getPath().equals("chests/end_city_treasure"))) continue; + String poolName = mod + ":legacy_parts"; + if (table.getPool(poolName) != null) continue; + table.addPool(LootPool.builder().name(poolName).rolls(ConstantRange.of(1)) + .addEntry(TableLootEntry.builder(new ResourceLocation(mod, "chests/advantage_parts"))).build()); } - return ForgeHooks.loadLootTable(gson, name, data, defaultPack, manager); } private static boolean hasAdditionalLootTables() { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeConditions.java b/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeConditions.java index 65109ce..7e849f5 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeConditions.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeConditions.java @@ -1,9 +1,13 @@ package zone.moddev.mc.poweradvantage.recipe; +import com.google.gson.JsonObject; +import java.util.Locale; import net.minecraft.tags.ItemTags; -import net.minecraft.util.JsonUtils; +import net.minecraft.util.JSONUtils; import net.minecraft.util.ResourceLocation; import net.minecraftforge.common.crafting.CraftingHelper; +import net.minecraftforge.common.crafting.conditions.ICondition; +import net.minecraftforge.common.crafting.conditions.IConditionSerializer; import zone.moddev.mc.poweradvantage.PowerAdvantage; import zone.moddev.mc.poweradvantage.RecipeMode; @@ -15,22 +19,45 @@ private RecipeConditions() {} public static synchronized void register() { if (registered) return; - CraftingHelper.register(new ResourceLocation(PowerAdvantage.MODID, "recipe_mode"), json -> { - RecipeMode expected = RecipeMode.valueOf(JsonUtils.getString(json, "mode").toUpperCase()); - return () -> PowerAdvantage.recipeMode == expected; - }); - CraftingHelper.register(new ResourceLocation(PowerAdvantage.MODID, "not_recipe_mode"), json -> { - RecipeMode excluded = RecipeMode.valueOf(JsonUtils.getString(json, "mode").toUpperCase()); - return () -> PowerAdvantage.recipeMode != excluded; - }); - CraftingHelper.register(new ResourceLocation(PowerAdvantage.MODID, "tag_empty"), json -> { - ResourceLocation tagId = new ResourceLocation(JsonUtils.getString(json, "tag")); - return () -> ItemTags.getCollection().getOrCreate(tagId).getAllElements().isEmpty(); - }); - CraftingHelper.register(new ResourceLocation(PowerAdvantage.MODID, "tag_not_empty"), json -> { - ResourceLocation tagId = new ResourceLocation(JsonUtils.getString(json, "tag")); - return () -> !ItemTags.getCollection().getOrCreate(tagId).getAllElements().isEmpty(); - }); + for (String name : new String[] {"recipe_mode", "not_recipe_mode", "tag_empty", "tag_not_empty"}) { + CraftingHelper.register(new Serializer(name)); + } registered = true; } + + private static final class Condition implements ICondition { + private final ResourceLocation id; + private final String value; + + private Condition(ResourceLocation id, String value) { + this.id = id; + this.value = value; + } + + @Override public ResourceLocation getID() { return id; } + + @Override public boolean test() { + switch (id.getPath()) { + case "recipe_mode": return PowerAdvantage.recipeMode == RecipeMode.valueOf(value.toUpperCase(Locale.ROOT)); + case "not_recipe_mode": return PowerAdvantage.recipeMode != RecipeMode.valueOf(value.toUpperCase(Locale.ROOT)); + case "tag_empty": return ItemTags.getCollection().getOrCreate(new ResourceLocation(value)).getAllElements().isEmpty(); + case "tag_not_empty": return !ItemTags.getCollection().getOrCreate(new ResourceLocation(value)).getAllElements().isEmpty(); + default: throw new IllegalStateException("Unknown Advantage condition " + id); + } + } + } + + private static final class Serializer implements IConditionSerializer { + private final ResourceLocation id; + private final String key; + + private Serializer(String name) { + id = new ResourceLocation(PowerAdvantage.MODID, name); + key = name.contains("recipe_mode") ? "mode" : "tag"; + } + + @Override public ResourceLocation getID() { return id; } + @Override public Condition read(JsonObject json) { return new Condition(id, JSONUtils.getString(json, key)); } + @Override public void write(JsonObject json, Condition condition) { json.addProperty(key, condition.value); } + } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeDiscovery.java b/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeDiscovery.java index 8be8b53..7a8a0d6 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeDiscovery.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeDiscovery.java @@ -5,10 +5,10 @@ import java.util.HashSet; import java.util.Set; import net.minecraft.advancements.Advancement; -import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.entity.player.ServerPlayerEntity; import net.minecraft.item.crafting.IRecipe; import net.minecraft.util.ResourceLocation; -import net.minecraftforge.fml.common.gameevent.PlayerEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; /** Retains earned discoveries when recipe modes or material providers change. */ public final class RecipeDiscovery { @@ -18,8 +18,8 @@ public final class RecipeDiscovery { private RecipeDiscovery() {} public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { - if (!(event.getPlayer() instanceof EntityPlayerMP)) return; - EntityPlayerMP player = (EntityPlayerMP) event.getPlayer(); + if (!(event.getPlayer() instanceof ServerPlayerEntity)) return; + ServerPlayerEntity player = (ServerPlayerEntity) event.getPlayer(); // Disabled recipes have no reward to grant. Replay only already-earned // discoveries when that exact recipe becomes available on a later login. for (IRecipe recipe : player.getServer().getRecipeManager().getRecipes()) { diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/FuelRegistry.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/FuelRegistry.java index 2a82b95..d26606a 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/FuelRegistry.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/FuelRegistry.java @@ -3,7 +3,7 @@ import net.minecraft.block.Block; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; -import net.minecraft.tileentity.TileEntityFurnace; +import net.minecraft.tileentity.AbstractFurnaceTileEntity; import net.minecraftforge.event.ForgeEventFactory; import java.util.*; @@ -142,7 +142,7 @@ public static short getActualBurntimeForItem(ItemStack fuel){ if(fuel == null || fuel.isEmpty()) return 0; int custom = getInstance().getBurnTime(fuel); if (custom > 0) return (short) Math.min(Short.MAX_VALUE, custom); - Integer vanilla = TileEntityFurnace.getBurnTimes().get(fuel.getItem()); + Integer vanilla = AbstractFurnaceTileEntity.getBurnTimes().get(fuel.getItem()); int burnTime = ForgeEventFactory.getItemBurnTime(fuel, vanilla == null ? 0 : vanilla.intValue()); return (short) Math.min(Short.MAX_VALUE, Math.max(0, burnTime)); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/ITileEntityGUI.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/ITileEntityGUI.java index 7cc8800..b850b37 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/ITileEntityGUI.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/ITileEntityGUI.java @@ -1,6 +1,6 @@ package zone.moddev.mc.poweradvantage.registry; -import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.tileentity.TileEntity; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @@ -13,23 +13,23 @@ */ public interface ITileEntityGUI { /** - * Gets an instance of net.minecraft.inventory.Container for the + * Gets an instance of net.minecraft.inventory.container.Container for the * server-side portion of a GUI interaction. * * @param e The TileEntity that is presenting this GUI * @param player The player that is going to use the GUI - * @return An instance of net.minecraft.inventory.Container + * @return An instance of net.minecraft.inventory.container.Container */ - public abstract net.minecraft.inventory.Container getContainer(TileEntity e, EntityPlayer player); + public abstract net.minecraft.inventory.container.Container getContainer(TileEntity e, PlayerEntity player); /** - * Gets an instance of net.minecraft.client.gui.inventory.GuiContainer for + * Gets an instance of net.minecraft.client.gui.screen.inventory.ContainerScreen for * the client-side portion of a GUI interaction. * * @param e The TileEntity that is presenting this GUI * @param player The player that is going to use the GUI - * @return An instance of net.minecraft.client.gui.inventory.GuiContainer + * @return An instance of net.minecraft.client.gui.screen.inventory.ContainerScreen */ @OnlyIn(Dist.CLIENT) - public abstract net.minecraft.client.gui.inventory.GuiContainer getContainerGUI(TileEntity e, EntityPlayer player); + public abstract net.minecraft.client.gui.screen.inventory.ContainerScreen getContainerGUI(TileEntity e, PlayerEntity player); } \ No newline at end of file diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/MachineGUIRegistry.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/MachineGUIRegistry.java index 0fe16d2..77bb614 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/MachineGUIRegistry.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/MachineGUIRegistry.java @@ -1,165 +1,115 @@ package zone.moddev.mc.poweradvantage.registry; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.gui.inventory.GuiContainer; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.entity.player.InventoryPlayer; -import net.minecraft.inventory.Container; +import cyano.poweradvantage.api.GUIBlock; +import cyano.poweradvantage.api.simple.SimpleMachineGUI; +import net.minecraft.block.Block; +import net.minecraft.client.gui.ScreenManager; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.container.Container; +import net.minecraft.inventory.container.ContainerType; +import net.minecraft.inventory.container.INamedContainerProvider; import net.minecraft.network.PacketBuffer; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.math.BlockPos; import net.minecraft.util.text.ITextComponent; -import net.minecraft.util.text.TextComponentString; -import net.minecraft.world.IInteractionObject; +import net.minecraft.util.text.TranslationTextComponent; import net.minecraft.world.World; -import net.minecraftforge.fml.network.FMLPlayMessages; -import net.minecraftforge.fml.network.NetworkHooks; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.common.extensions.IForgeContainerType; +import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.network.NetworkHooks; +import zone.moddev.mc.poweradvantage.PowerAdvantage; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -/** - * The MachineGUIRegistry registry provides a simple way to add GUIs for your - * machines. Just implement the ITileEntityGUI (usually by extending the - * SimpleMachineGUI class) and pass it to the addGUI(...) method. - * This method returns a number which is the unique ID for that GUI. Blocks that - * are supposed to show that GUI will call - * player.openGui(PowerAdvantage.getInstance(), guiID, world, x, y, z); - * in their override of the onBlockActivated(...) method (guiID is - * the number returned by MachineGUIRegistry.addGUI(...)). - * @author DrCyano - * - */ -public class MachineGUIRegistry { - private static final AtomicInteger guiIDCounter = new AtomicInteger(1); - private static final Map guiTable = new HashMap(); +/** Legacy GUI definitions backed by Forge's registered container and screen factories. */ +@Mod.EventBusSubscriber(modid = PowerAdvantage.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) +public final class MachineGUIRegistry { + private static final MachineGUIRegistry INSTANCE = new MachineGUIRegistry(); + private static final AtomicInteger NEXT_ID = new AtomicInteger(1); + private static final Map DEFINITIONS = new ConcurrentHashMap<>(); + public static ContainerType TYPE; - private MachineGUIRegistry() { - // using singleton instantiation - } + private MachineGUIRegistry() {} - private static final Lock initLock = new ReentrantLock(); - private static MachineGUIRegistry instance = null; + public static MachineGUIRegistry getInstance() { return INSTANCE; } - /** - * Gets a singleton instance of MachineGUIRegistry - * - * @return A global instance of MachineGUIRegistry - */ - public static MachineGUIRegistry getInstance() { - if (instance == null) { - initLock.lock(); - try { - if (instance == null) { - // thread-safe singleton instantiation - instance = new MachineGUIRegistry(); - } - } finally { - initLock.unlock(); - } - } - return instance; - } + public static int addGUI(ITileEntityGUI gui) { + int id = NEXT_ID.getAndIncrement(); + DEFINITIONS.put(id, gui); + return id; + } - /** - * Adds a new GUI to the registry and returns its GUI ID number. The blocks - * that are intended to show this GUI will do so by calling - * player.openGui(PowerAdvantage.getInstance(), guiID, world, x, y, z); - * where guiID is the GUI ID number returned by this method. - * - * @param gui An implementation of ITileEntityGUI (typically an extension of - * the SimpleMachineGUI class). - * @return The gui id that the gui was registered to. This number is needed - * to show the GUI to the player. - */ - public static int addGUI(ITileEntityGUI gui) { - int id = guiIDCounter.getAndIncrement(); - guiTable.put(id, gui); - return id; - } + public void removeGUI(int id) { DEFINITIONS.remove(id); } - /** - * Removes a GUI by its ID that was returned by the addGUI(...) method. - * - * @param guiID The ID of the GUI to remove - */ - public void removeGUI(int guiID) { - guiTable.remove(guiID); - } + @SubscribeEvent + public static void registerTypes(RegistryEvent.Register> event) { + TYPE = IForgeContainerType.create(MachineGUIRegistry::createClientContainer); + TYPE.setRegistryName(PowerAdvantage.MODID, "machine"); + event.getRegistry().register(TYPE); + } - /** - * Implementation of net.minecraftforge.fml.common.network.IGuiHandler - */ - @OnlyIn(Dist.CLIENT) - public Object getClientGuiElement( - int id, EntityPlayer player, World world, - int x, int y, int z - ) { - TileEntity tileEntity = world.getTileEntity(new BlockPos(x, y, z)); - if (guiTable.containsKey(id)) { - return guiTable.get(id).getContainerGUI(tileEntity, player); - } - return null; - } + private static SimpleMachineGUI.Container createClientContainer( + int windowId, PlayerInventory inventory, PacketBuffer data) { + BlockPos pos = data.readBlockPos(); + World world = inventory.player.world; + TileEntity tile = world.getTileEntity(pos); + Block block = world.getBlockState(pos).getBlock(); + if (!(block instanceof GUIBlock) || !(tile instanceof IInventory)) { + throw new IllegalStateException("Missing machine for container at " + pos); + } + // Resolve the local block's definition; numeric registration order is not a network identity. + return definition(((GUIBlock) block).getGuiID()).createContainer(windowId, inventory, (IInventory) tile); + } - /** - * Implementation of net.minecraftforge.fml.common.network.IGuiHandler - */ - public Object getServerGuiElement( - int id, EntityPlayer player, World world, - int x, int y, int z - ) { - TileEntity tileEntity = world.getTileEntity(new BlockPos(x, y, z)); - if (guiTable.containsKey(id)) { - return guiTable.get(id).getContainer(tileEntity, player); - } - return null; - } + private static SimpleMachineGUI definition(int id) { + ITileEntityGUI gui = DEFINITIONS.get(id); + if (!(gui instanceof SimpleMachineGUI)) { + throw new IllegalStateException("No native machine GUI registered for " + id); + } + return (SimpleMachineGUI) gui; + } - public static void open(EntityPlayerMP player, World world, BlockPos pos, int guiId) { - ITileEntityGUI definition = guiTable.get(guiId); - if (definition == null) return; - TileEntity tile = world.getTileEntity(pos); - if (tile == null) return; - NetworkHooks.openGui(player, new IInteractionObject() { - @Override - public Container createContainer(InventoryPlayer inventory, EntityPlayer entityPlayer) { - return definition.getContainer(tile, entityPlayer); - } + @OnlyIn(Dist.CLIENT) + public static void registerScreens() { + ScreenManager.registerFactory(TYPE, + (container, inventory, title) -> container.createScreen(inventory, title)); + } - @Override - public String getGuiID() { - return "poweradvantage:machine"; - } + public static void open(ServerPlayerEntity player, World world, BlockPos pos, int id) { + TileEntity tile = world.getTileEntity(pos); + if (world.isRemote || player.world != world || !(tile instanceof IInventory)) return; + SimpleMachineGUI gui = definition(id); + NetworkHooks.openGui(player, new INamedContainerProvider() { + @Override public Container createMenu(int windowId, PlayerInventory inventory, PlayerEntity owner) { + return gui.createContainer(windowId, inventory, (IInventory) tile); + } + @Override public ITextComponent getDisplayName() { + return tile instanceof net.minecraft.util.INameable + ? ((net.minecraft.util.INameable) tile).getDisplayName() + : new TranslationTextComponent(world.getBlockState(pos).getBlock().getTranslationKey()); + } + }, pos); + } - @Override public ITextComponent getName() { return new TextComponentString("Power Advantage"); } - @Override public boolean hasCustomName() { return false; } - @Override public ITextComponent getCustomName() { return null; } - }, buffer -> { - buffer.writeBlockPos(pos); - buffer.writeVarInt(guiId); - }); - } + public Object getServerGuiElement(int id, PlayerEntity player, World world, int x, int y, int z) { + ITileEntityGUI gui = DEFINITIONS.get(id); + TileEntity tile = world.getTileEntity(new BlockPos(x, y, z)); + return gui == null || tile == null ? null : gui.getContainer(tile, player); + } - @OnlyIn(Dist.CLIENT) - public static GuiScreen createClientGui(FMLPlayMessages.OpenContainer message) { - PacketBuffer data = message.getAdditionalData(); - BlockPos pos = data.readBlockPos(); - int guiId = data.readVarInt(); - Minecraft minecraft = Minecraft.getInstance(); - if (minecraft.world == null || minecraft.player == null) return null; - TileEntity tile = minecraft.world.getTileEntity(pos); - ITileEntityGUI definition = guiTable.get(guiId); - if (tile == null || definition == null) return null; - GuiContainer screen = definition.getContainerGUI(tile, minecraft.player); - screen.inventorySlots.windowId = message.getWindowId(); - return screen; - } + @OnlyIn(Dist.CLIENT) + public Object getClientGuiElement(int id, PlayerEntity player, World world, int x, int y, int z) { + ITileEntityGUI gui = DEFINITIONS.get(id); + TileEntity tile = world.getTileEntity(new BlockPos(x, y, z)); + return gui == null || tile == null ? null : gui.getContainerGUI(tile, player); + } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipe.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipe.java index 6ee510a..5762008 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipe.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipe.java @@ -4,7 +4,7 @@ import java.util.LinkedList; import net.minecraft.fluid.Fluid; -import net.minecraft.util.registry.IRegistry; +import net.minecraft.util.registry.Registry; import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.FluidTank; @@ -119,7 +119,7 @@ public boolean isValidOutput(Fluid output) { */ public Collection getValidInputs() { LinkedList l = new LinkedList<>(); - for (Fluid f : IRegistry.field_212619_h) { + for (Fluid f : net.minecraftforge.registries.ForgeRegistries.FLUIDS) { FluidStack stack = new FluidStack(f, FluidTank.BUCKET_VOLUME); if (this.isValidInput(stack)) { l.add(stack); diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipeRegistry.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipeRegistry.java index 6975d66..61b4b6a 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipeRegistry.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipeRegistry.java @@ -11,7 +11,7 @@ import net.minecraft.fluid.Fluid; import cyano.poweradvantage.api.fluid.FluidStack; import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.IRegistry; +import net.minecraft.util.registry.Registry; import zone.moddev.mc.poweradvantage.init.Fluids; /** @@ -137,7 +137,7 @@ public static void addDistillationRecipe(FluidStack input, FluidStack output){ } private static String id(Fluid fluid) { - ResourceLocation id = IRegistry.field_212619_h.getKey(fluid); + ResourceLocation id = net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(fluid); return id == null ? "unknown" : id.toString(); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/ExplicitFluidReference.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/ExplicitFluidReference.java index 5fe583a..d3d6e5e 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/ExplicitFluidReference.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/ExplicitFluidReference.java @@ -2,7 +2,7 @@ import net.minecraft.fluid.Fluid; import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.IRegistry; +import net.minecraft.util.registry.Registry; /** * Implementation of FluidReference that compares specific fluids (fluids of same name but different @@ -44,7 +44,7 @@ public boolean matches(Fluid fluid) { */ @Override public boolean matches(String fluidName) { - ResourceLocation id = IRegistry.field_212619_h.getKey(reference); + ResourceLocation id = net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(reference); return id != null && (id.toString().equals(fluidName) || id.getPath().equals(fluidName)); } @@ -65,7 +65,7 @@ public Fluid getFluid() { */ @Override public int hashCode() { - ResourceLocation id = IRegistry.field_212619_h.getKey(reference); + ResourceLocation id = net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(reference); return id == null ? System.identityHashCode(reference) : id.hashCode(); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/RegistryNameFluidReference.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/RegistryNameFluidReference.java index 159e6fb..447161d 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/RegistryNameFluidReference.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/RegistryNameFluidReference.java @@ -2,7 +2,7 @@ import net.minecraft.fluid.Fluid; import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.IRegistry; +import net.minecraft.util.registry.Registry; import zone.moddev.mc.poweradvantage.init.Fluids; public class RegistryNameFluidReference extends FluidReference { @@ -28,7 +28,7 @@ public RegistryNameFluidReference(String registryName) { */ @Override public boolean matches(Fluid fluid) { - ResourceLocation id = IRegistry.field_212619_h.getKey(fluid); + ResourceLocation id = net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(fluid); return id != null && (reference.equals(id.toString()) || reference.equals(id.getPath())); } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/AdvantageFluidAdapter.java b/src/main/java/zone/moddev/mc/poweradvantage/util/AdvantageFluidAdapter.java new file mode 100644 index 0000000..cfd65bc --- /dev/null +++ b/src/main/java/zone/moddev/mc/poweradvantage/util/AdvantageFluidAdapter.java @@ -0,0 +1,64 @@ +package zone.moddev.mc.poweradvantage.util; + +import cyano.poweradvantage.api.fluid.FluidStack; +import cyano.poweradvantage.api.fluid.FluidTankInfo; +import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; +import net.minecraft.fluid.Fluid; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.Direction; +import net.minecraftforge.fluids.capability.CapabilityFluidHandler; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction; + +/** A side-bound foreign capability; reacquire it after removal or capability invalidation. */ +final class AdvantageFluidAdapter implements IAdvantageFluidHandler { + private final TileEntity tile; + private final Direction side; + + AdvantageFluidAdapter(TileEntity tile, Direction side) { + this.tile = tile; + this.side = side; + } + + private IFluidHandler handler() { + return tile.isRemoved() ? null : tile.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, side).orElse(null); + } + + @Override public FluidTankInfo[] getTankInfo(Direction ignored) { + IFluidHandler handler = handler(); + if (handler == null) return new FluidTankInfo[0]; + FluidTankInfo[] result = new FluidTankInfo[handler.getTanks()]; + for (int i = 0; i < result.length; i++) result[i] = new FluidTankInfo( + ForgeFluidAdapter.fromForge(handler.getFluidInTank(i)), handler.getTankCapacity(i)); + return result; + } + @Override public int fill(Direction ignored, FluidStack resource, boolean execute) { + IFluidHandler handler = handler(); + if (handler == null || resource == null || resource.amount <= 0) return 0; + return Math.max(0, Math.min(resource.amount, handler.fill(ForgeFluidAdapter.toForge(resource), + execute ? FluidAction.EXECUTE : FluidAction.SIMULATE))); + } + @Override public FluidStack drain(Direction ignored, FluidStack resource, boolean execute) { + IFluidHandler handler = handler(); + if (handler == null || resource == null || resource.amount <= 0) return null; + FluidStack result = ForgeFluidAdapter.fromForge(handler.drain(ForgeFluidAdapter.toForge(resource), + execute ? FluidAction.EXECUTE : FluidAction.SIMULATE)); + return result == null || !resource.isFluidEqual(result) ? null : new FluidStack(result, Math.min(resource.amount, result.amount)); + } + @Override public FluidStack drain(Direction ignored, int amount, boolean execute) { + IFluidHandler handler = handler(); + if (handler == null || amount <= 0) return null; + FluidStack result = ForgeFluidAdapter.fromForge(handler.drain(amount, execute ? FluidAction.EXECUTE : FluidAction.SIMULATE)); + return result == null ? null : new FluidStack(result, Math.min(amount, result.amount)); + } + @Override public boolean canFill(Direction ignored, Fluid fluid) { + IFluidHandler handler = handler(); + if (handler == null || fluid == null) return false; + net.minecraftforge.fluids.FluidStack probe = new net.minecraftforge.fluids.FluidStack(fluid, 1); + for (int i = 0; i < handler.getTanks(); i++) if (handler.isFluidValid(i, probe)) return true; + return false; + } + @Override public boolean canDrain(Direction ignored, Fluid fluid) { + return fluid != null && drain(side, new FluidStack(fluid, 1), false) != null; + } +} diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidContainerHelper.java b/src/main/java/zone/moddev/mc/poweradvantage/util/FluidContainerHelper.java index 73f9830..b4a58ca 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidContainerHelper.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/util/FluidContainerHelper.java @@ -3,54 +3,34 @@ import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.fluid.FluidTank; import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; -import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.fluid.Fluid; -import net.minecraft.init.Items; +import net.minecraft.item.Items; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; -import net.minecraft.util.EnumFacing; -import net.minecraft.util.EnumHand; -import net.minecraft.util.registry.IRegistry; +import net.minecraft.util.Direction; +import net.minecraft.util.Hand; +import net.minecraft.util.registry.Registry; -/** Native bucket interaction for Forge 25's transitional fluid implementation. */ +/** Uses Forge's container handling for buckets and capability-based fluid containers. */ public final class FluidContainerHelper { private FluidContainerHelper() {} - public static boolean interact(EntityPlayer player, EnumHand hand, - IAdvantageFluidHandler handler, EnumFacing face) { - ItemStack held = player.getHeldItem(hand); - if (held.isEmpty()) return false; - - if (held.getItem() == Items.BUCKET) { - FluidStack available = handler.drain(face, FluidTank.BUCKET_VOLUME, false); - if (available == null || available.amount < FluidTank.BUCKET_VOLUME) return false; - Item filledBucket = available.getFluid().getFilledBucket(); - if (filledBucket == null || filledBucket == Items.AIR) return false; - handler.drain(face, FluidTank.BUCKET_VOLUME, true); - replaceOne(player, hand, held, new ItemStack(filledBucket)); - return true; - } - - Fluid contained = fluidForBucket(held.getItem()); - if (contained == null) return false; - FluidStack bucket = new FluidStack(contained, FluidTank.BUCKET_VOLUME); - if (handler.fill(face, bucket, false) < FluidTank.BUCKET_VOLUME) return false; - handler.fill(face, bucket, true); - replaceOne(player, hand, held, new ItemStack(Items.BUCKET)); - return true; + public static boolean interact(PlayerEntity player, Hand hand, + IAdvantageFluidHandler handler, Direction face) { + if (player.world.isRemote) return false; + return net.minecraftforge.fluids.FluidUtil.interactWithFluidHandler(player, hand, + new ForgeFluidAdapter(handler, face, () -> { + if (handler instanceof net.minecraft.tileentity.TileEntity) + ((net.minecraft.tileentity.TileEntity) handler).markDirty(); + })); } public static Fluid fluidForBucket(Item bucketItem) { - for (Fluid fluid : IRegistry.field_212619_h) { + for (Fluid fluid : net.minecraftforge.registries.ForgeRegistries.FLUIDS) { if (fluid.getFilledBucket() == bucketItem && bucketItem != Items.BUCKET) return fluid; } return null; } - private static void replaceOne(EntityPlayer player, EnumHand hand, ItemStack held, ItemStack replacement) { - if (player.abilities.isCreativeMode) return; - held.shrink(1); - if (held.isEmpty()) player.setHeldItem(hand, replacement); - else if (!player.inventory.addItemStackToInventory(replacement)) player.dropItem(replacement, false); - } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidHandlerHelper.java b/src/main/java/zone/moddev/mc/poweradvantage/util/FluidHandlerHelper.java index 9208e70..27aeb4c 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidHandlerHelper.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/util/FluidHandlerHelper.java @@ -2,17 +2,20 @@ import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; /** Looks up the stable Advantage fluid handler exposed by local machines. */ public final class FluidHandlerHelper { private FluidHandlerHelper() {} - public static IAdvantageFluidHandler getHandler(TileEntity tileEntity, EnumFacing face) { - return tileEntity instanceof IAdvantageFluidHandler ? (IAdvantageFluidHandler) tileEntity : null; + public static IAdvantageFluidHandler getHandler(TileEntity tileEntity, Direction face) { + if (tileEntity == null || tileEntity.isRemoved()) return null; + if (tileEntity instanceof IAdvantageFluidHandler) return (IAdvantageFluidHandler) tileEntity; + return tileEntity.getCapability(net.minecraftforge.fluids.capability.CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, face).isPresent() + ? new AdvantageFluidAdapter(tileEntity, face) : null; } - public static boolean hasHandler(TileEntity tileEntity, EnumFacing face) { + public static boolean hasHandler(TileEntity tileEntity, Direction face) { return getHandler(tileEntity, face) != null; } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidIdHelper.java b/src/main/java/zone/moddev/mc/poweradvantage/util/FluidIdHelper.java index 8745031..b0429f5 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidIdHelper.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/util/FluidIdHelper.java @@ -2,7 +2,7 @@ import net.minecraft.fluid.Fluid; import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.IRegistry; +import net.minecraft.util.registry.Registry; public final class FluidIdHelper { @@ -12,14 +12,14 @@ private FluidIdHelper() { public static int getFluidId(Fluid fluid) { if (fluid == null) return 0; - ResourceLocation name = IRegistry.field_212619_h.getKey(fluid); + ResourceLocation name = net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(fluid); return name == null ? 0 : name.toString().hashCode(); } public static Fluid getFluid(int id) { if (id == 0) return null; - for (Fluid fluid : IRegistry.field_212619_h) { - ResourceLocation name = IRegistry.field_212619_h.getKey(fluid); + for (Fluid fluid : net.minecraftforge.registries.ForgeRegistries.FLUIDS) { + ResourceLocation name = net.minecraftforge.registries.ForgeRegistries.FLUIDS.getKey(fluid); if (name != null && name.toString().hashCode() == id) return fluid; } return null; diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/ForgeFluidAdapter.java b/src/main/java/zone/moddev/mc/poweradvantage/util/ForgeFluidAdapter.java new file mode 100644 index 0000000..91ecbaa --- /dev/null +++ b/src/main/java/zone/moddev/mc/poweradvantage/util/ForgeFluidAdapter.java @@ -0,0 +1,79 @@ +package zone.moddev.mc.poweradvantage.util; + +import cyano.poweradvantage.api.fluid.FluidStack; +import cyano.poweradvantage.api.fluid.FluidTankInfo; +import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; +import net.minecraft.util.Direction; +import net.minecraftforge.fluids.capability.IFluidHandler; + +/** Exposes the stable Advantage contract through Forge's native fluid capability. */ +public final class ForgeFluidAdapter implements IFluidHandler { + private final IAdvantageFluidHandler handler; + private final Direction face; + private final Runnable changed; + + public ForgeFluidAdapter(IAdvantageFluidHandler handler, Direction face, Runnable changed) { + this.handler = handler; + this.face = face; + this.changed = changed; + } + + public static net.minecraftforge.fluids.FluidStack toForge(FluidStack stack) { + return stack == null || stack.amount <= 0 ? net.minecraftforge.fluids.FluidStack.EMPTY + : new net.minecraftforge.fluids.FluidStack(stack.getFluid(), stack.amount, + stack.tag == null ? null : stack.tag.copy()); + } + + public static FluidStack fromForge(net.minecraftforge.fluids.FluidStack stack) { + if (stack == null || stack.isEmpty()) return null; + FluidStack copy = new FluidStack(stack.getFluid(), stack.getAmount()); + copy.tag = stack.hasTag() ? stack.getTag().copy() : null; + return copy; + } + + private FluidTankInfo[] tanks() { + FluidTankInfo[] tanks = handler.getTankInfo(face); + return tanks == null ? new FluidTankInfo[0] : tanks; + } + + @Override public int getTanks() { return tanks().length; } + @Override public net.minecraftforge.fluids.FluidStack getFluidInTank(int tank) { + FluidTankInfo[] tanks = tanks(); + return tank < 0 || tank >= tanks.length || tanks[tank] == null + ? net.minecraftforge.fluids.FluidStack.EMPTY : toForge(tanks[tank].fluid); + } + @Override public int getTankCapacity(int tank) { + FluidTankInfo[] tanks = tanks(); + return tank < 0 || tank >= tanks.length || tanks[tank] == null ? 0 : Math.max(0, tanks[tank].capacity); + } + @Override public boolean isFluidValid(int tank, net.minecraftforge.fluids.FluidStack resource) { + return resource != null && !resource.isEmpty() && getTankCapacity(tank) > 0 + && handler.canFill(face, resource.getFluid()); + } + @Override public int fill(net.minecraftforge.fluids.FluidStack resource, FluidAction action) { + FluidStack copy = fromForge(resource); + if (copy == null || !handler.canFill(face, copy.getFluid())) return 0; + int accepted = Math.max(0, Math.min(resource.getAmount(), handler.fill(face, copy, action.execute()))); + if (accepted > 0 && action.execute()) changed.run(); + return accepted; + } + @Override public net.minecraftforge.fluids.FluidStack drain(net.minecraftforge.fluids.FluidStack resource, FluidAction action) { + FluidStack copy = fromForge(resource); + if (copy == null || !handler.canDrain(face, copy.getFluid())) return net.minecraftforge.fluids.FluidStack.EMPTY; + FluidStack result = handler.drain(face, copy, action.execute()); + if (result == null || !copy.isFluidEqual(result)) return net.minecraftforge.fluids.FluidStack.EMPTY; + return drained(result, resource.getAmount(), action); + } + @Override public net.minecraftforge.fluids.FluidStack drain(int amount, FluidAction action) { + if (amount <= 0) return net.minecraftforge.fluids.FluidStack.EMPTY; + FluidStack available = handler.drain(face, amount, false); + if (available == null || !handler.canDrain(face, available.getFluid())) return net.minecraftforge.fluids.FluidStack.EMPTY; + return drained(handler.drain(face, new FluidStack(available, Math.min(amount, available.amount)), action.execute()), amount, action); + } + private net.minecraftforge.fluids.FluidStack drained(FluidStack result, int limit, FluidAction action) { + if (result == null || result.amount <= 0) return net.minecraftforge.fluids.FluidStack.EMPTY; + net.minecraftforge.fluids.FluidStack copy = toForge(new FluidStack(result, Math.min(limit, result.amount))); + if (action.execute()) changed.run(); + return copy; + } +} diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/InventoryLocks.java b/src/main/java/zone/moddev/mc/poweradvantage/util/InventoryLocks.java new file mode 100644 index 0000000..de37540 --- /dev/null +++ b/src/main/java/zone/moddev/mc/poweradvantage/util/InventoryLocks.java @@ -0,0 +1,31 @@ +package zone.moddev.mc.poweradvantage.util; + +import java.lang.reflect.Field; +import net.minecraft.item.ItemStack; +import net.minecraft.tileentity.LockableTileEntity; +import net.minecraft.world.LockCode; +import net.minecraftforge.fml.common.ObfuscationReflectionHelper; + +/** Keeps automation from bypassing vanilla container locks. */ +public final class InventoryLocks { + // Forge 1.14 has no lock getter. Resolve its SRG field once, not once per transfer. + private static final Field CODE = ObfuscationReflectionHelper.findField(LockableTileEntity.class, "field_174901_a"); + private static final Field FIRST = ObfuscationReflectionHelper.findField( + net.minecraft.inventory.DoubleSidedInventory.class, "field_70477_b"); + private static final Field SECOND = ObfuscationReflectionHelper.findField( + net.minecraft.inventory.DoubleSidedInventory.class, "field_70478_c"); + + private InventoryLocks() {} + + public static boolean isLocked(Object inventory) { + if (inventory instanceof InventoryWrapper) return ((InventoryWrapper) inventory).isLocked(); + try { + if (inventory instanceof net.minecraft.inventory.DoubleSidedInventory) + return isLocked(FIRST.get(inventory)) || isLocked(SECOND.get(inventory)); + return inventory instanceof LockableTileEntity + && !((LockCode) CODE.get(inventory)).func_219964_a(ItemStack.EMPTY); + } catch (IllegalAccessException failure) { + throw new IllegalStateException("Cannot inspect vanilla inventory lock", failure); + } + } +} diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/InventoryWrapper.java b/src/main/java/zone/moddev/mc/poweradvantage/util/InventoryWrapper.java index abe19da..5ef57a1 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/InventoryWrapper.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/util/InventoryWrapper.java @@ -1,293 +1,60 @@ package zone.moddev.mc.poweradvantage.util; -import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.inventory.IInventory; import net.minecraft.inventory.ISidedInventory; +import net.minecraft.inventory.Inventory; import net.minecraft.item.ItemStack; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; +import net.minecraft.util.INameable; import net.minecraft.util.text.ITextComponent; -import net.minecraft.util.text.TextComponentBase; -import net.minecraft.util.text.TextComponentString; -import net.minecraft.world.ILockableContainer; - -/** - * This class allows you to treat an instance of IInventory as if it were an instance of - * ISidedInventory. - * @author DrCyano - * - */ -public class InventoryWrapper implements ISidedInventory { - - private final IInventory inventory; - private final int[] slots; - private static final int[] NO_SLOTS = new int[0]; - private static final InventoryWrapper NULL_INVENTORY = new InventoryWrapper(); - - /** - * Constructs an ISidedInventory wrapper for the given IInventory instance - * - * @param inv An net.minecraft.inventory.IInventory that you want to treat as a - * net.minecraft.inventory.ISidedInventory - */ - protected InventoryWrapper(IInventory inv) { - this.inventory = inv; - slots = new int[inventory.getSizeInventory()]; - for (int i = 0; i < slots.length; i++) { - slots[i] = i; - } - } - - /** - * Creates an ISidedInventory instance that has no inventory - */ - protected InventoryWrapper() { - slots = NO_SLOTS; - inventory = new IInventory() { - @Override - public int getSizeInventory() { - return 0; - } - - @Override - public boolean isEmpty() { - return true; - } - - @Override - public ItemStack getStackInSlot(int i) { - return null; - } - - @Override - public ItemStack decrStackSize(int i, int i1) { - return null; - } - - @Override - public ItemStack removeStackFromSlot(int i) { - return null; - } - - @Override - public void setInventorySlotContents(int i, ItemStack itemStack) { - - } - - @Override - public int getInventoryStackLimit() { - return 0; - } - - @Override - public void markDirty() { - - } - - @Override - public boolean isUsableByPlayer(EntityPlayer entityPlayer) { - return false; - } - - @Override - public void openInventory(EntityPlayer entityPlayer) { - - } - - @Override - public void closeInventory(EntityPlayer entityPlayer) { - - } - - @Override - public boolean isItemValidForSlot(int i, ItemStack itemStack) { - return false; - } - - @Override - public int getField(int i) { - return 0; - } - - @Override - public void setField(int i, int i1) { - - } - - @Override - public int getFieldCount() { - return 0; - } - - @Override - public void clear() { - - } - - @Override - public ITextComponent getName() { - return new TextComponentString("Inventory"); - } - - @Override - public boolean hasCustomName() { - return false; - } - - @Override - public ITextComponent getCustomName() { - return null; - } - - @Override - public TextComponentBase getDisplayName() { - return new TextComponentString("Inventory"); - } - }; - } - - /** - * Gets whether this is a locked container - * - * @return true if the wrapped inventory is an instance of ILockableContainer and is locked - */ - public boolean isLocked() { - if (inventory instanceof ILockableContainer) { - return ((ILockableContainer) inventory).isLocked(); - } - return false; - } - - /** - * Constructs an ISidedInventory wrapper for the given IInventory instance. If the provided - * instance is already an ISidedInventory, then that instance is simply returned instead of - * making a wrapper. - * - * @param inv An net.minecraft.inventory.IInventory that you want to treat as a - * net.minecraft.inventory.ISidedInventory - * @return An instance of ISidedInventory that forwards all methods to the given IInventory - */ - public static ISidedInventory wrap(IInventory inv) { - if (inv == null) return NULL_INVENTORY; - if (inv instanceof ISidedInventory) return (ISidedInventory) inv; - return new InventoryWrapper(inv); - } - - @Override - public ITextComponent getDisplayName() { - return inventory.getDisplayName(); - } - - @Override - public ITextComponent getName() { - return inventory.getName(); - } - - @Override - public ITextComponent getCustomName() { - return inventory.getCustomName(); - } - - @Override - public boolean hasCustomName() { - return inventory.hasCustomName(); - } - - @Override - public void clear() { - inventory.clear(); - } - - @Override - public boolean isEmpty() { - return inventory.isEmpty(); - } - - @Override - public void closeInventory(EntityPlayer arg0) { - inventory.closeInventory(arg0); - } - - @Override - public ItemStack decrStackSize(int arg0, int arg1) { - return inventory.decrStackSize(arg0, arg1); - } - - @Override - public int getField(int arg0) { - return inventory.getField(arg0); - } - - @Override - public int getFieldCount() { - return inventory.getFieldCount(); - } - - @Override - public int getInventoryStackLimit() { - return inventory.getInventoryStackLimit(); - } - - @Override - public int getSizeInventory() { - return inventory.getSizeInventory(); - } - - @Override - public ItemStack getStackInSlot(int arg0) { - return inventory.getStackInSlot(arg0); - } - - @Override - public ItemStack removeStackFromSlot(int index) { - return inventory.removeStackFromSlot(index); - } - - @Override - public boolean isItemValidForSlot(int arg0, ItemStack arg1) { - return inventory.isItemValidForSlot(arg0, arg1); - } - - @Override - public boolean isUsableByPlayer(EntityPlayer arg0) { - return inventory.isUsableByPlayer(arg0); - } - - @Override - public void markDirty() { - inventory.markDirty(); - } - - @Override - public void openInventory(EntityPlayer arg0) { - inventory.openInventory(arg0); - } - - @Override - public void setField(int arg0, int arg1) { - inventory.setField(arg0, arg1); - } - - @Override - public void setInventorySlotContents(int arg0, ItemStack arg1) { - inventory.setInventorySlotContents(arg0, arg1); - } - - @Override - public boolean canExtractItem(int arg0, ItemStack arg1, EnumFacing arg2) { - if (isLocked()) return false; - return true; - } - - @Override - public boolean canInsertItem(int arg0, ItemStack arg1, EnumFacing arg2) { - if (isLocked()) return false; - return true; - } - - - @Override - public int[] getSlotsForFace(EnumFacing arg0) { - if (isLocked()) return NO_SLOTS; - return slots; - } +import net.minecraft.util.text.StringTextComponent; + +/** Gives ordinary inventories the sided interface used by conveyors. */ +public class InventoryWrapper implements ISidedInventory, INameable { + private static final int[] NO_SLOTS = new int[0]; + private static final InventoryWrapper NULL_INVENTORY = new InventoryWrapper(); + private final IInventory inventory; + private final int[] slots; + + protected InventoryWrapper() { this(new Inventory(0)); } + + protected InventoryWrapper(IInventory inventory) { + this.inventory = inventory; + slots = new int[inventory.getSizeInventory()]; + for (int i = 0; i < slots.length; i++) slots[i] = i; + } + + public static ISidedInventory wrap(IInventory inventory) { + if (inventory == null) return NULL_INVENTORY; + return inventory instanceof ISidedInventory ? (ISidedInventory) inventory : new InventoryWrapper(inventory); + } + + public boolean isLocked() { return InventoryLocks.isLocked(inventory); } + @Override public ITextComponent getName() { + return inventory instanceof INameable ? ((INameable) inventory).getName() : new StringTextComponent("Inventory"); + } + @Override public ITextComponent getCustomName() { + return inventory instanceof INameable ? ((INameable) inventory).getCustomName() : null; + } + @Override public void clear() { inventory.clear(); } + @Override public boolean isEmpty() { return inventory.isEmpty(); } + @Override public int getSizeInventory() { return inventory.getSizeInventory(); } + @Override public int getInventoryStackLimit() { return inventory.getInventoryStackLimit(); } + @Override public ItemStack getStackInSlot(int slot) { return inventory.getStackInSlot(slot); } + @Override public ItemStack decrStackSize(int slot, int amount) { return inventory.decrStackSize(slot, amount); } + @Override public ItemStack removeStackFromSlot(int slot) { return inventory.removeStackFromSlot(slot); } + @Override public void setInventorySlotContents(int slot, ItemStack stack) { inventory.setInventorySlotContents(slot, stack); } + @Override public boolean isItemValidForSlot(int slot, ItemStack stack) { return inventory.isItemValidForSlot(slot, stack); } + @Override public boolean isUsableByPlayer(PlayerEntity player) { + return slots.length != 0 && inventory.isUsableByPlayer(player); + } + @Override public void openInventory(PlayerEntity player) { inventory.openInventory(player); } + @Override public void closeInventory(PlayerEntity player) { inventory.closeInventory(player); } + @Override public void markDirty() { inventory.markDirty(); } + @Override public int[] getSlotsForFace(Direction face) { return isLocked() ? NO_SLOTS : slots; } + @Override public boolean canInsertItem(int slot, ItemStack stack, Direction face) { + return !isLocked() && inventory.isItemValidForSlot(slot, stack); + } + @Override public boolean canExtractItem(int slot, ItemStack stack, Direction face) { return !isLocked(); } } diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/MachineCapabilities.java b/src/main/java/zone/moddev/mc/poweradvantage/util/MachineCapabilities.java new file mode 100644 index 0000000..3e67d1f --- /dev/null +++ b/src/main/java/zone/moddev/mc/poweradvantage/util/MachineCapabilities.java @@ -0,0 +1,44 @@ +package zone.moddev.mc.poweradvantage.util; + +import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; +import java.util.HashMap; +import java.util.Map; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.ISidedInventory; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.Direction; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.fluids.capability.CapabilityFluidHandler; +import net.minecraftforge.items.CapabilityItemHandler; +import net.minecraftforge.items.wrapper.InvWrapper; +import net.minecraftforge.items.wrapper.SidedInvWrapper; + +/** Lazily built sided adapters, discarded when a machine is removed or unloaded. */ +public final class MachineCapabilities { + private final TileEntity owner; + private final Map> fluids = new HashMap<>(); + private final Map> items = new HashMap<>(); + + public MachineCapabilities(TileEntity owner) { this.owner = owner; } + + public LazyOptional get(Capability capability, Direction face) { + if (owner.isRemoved()) return LazyOptional.empty(); + if (capability == CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY && owner instanceof IAdvantageFluidHandler) { + return fluids.computeIfAbsent(face, side -> LazyOptional.of(() -> + new ForgeFluidAdapter((IAdvantageFluidHandler) owner, side, owner::markDirty))).cast(); + } + if (capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY && owner instanceof IInventory) { + return items.computeIfAbsent(face, side -> LazyOptional.of(() -> side != null && owner instanceof ISidedInventory + ? new SidedInvWrapper((ISidedInventory) owner, side) : new InvWrapper((IInventory) owner))).cast(); + } + return LazyOptional.empty(); + } + + public void invalidate() { + fluids.values().forEach(LazyOptional::invalidate); + items.values().forEach(LazyOptional::invalidate); + fluids.clear(); + items.clear(); + } +} diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/PowerHelper.java b/src/main/java/zone/moddev/mc/poweradvantage/util/PowerHelper.java index ca9dc8c..fa6d064 100644 --- a/src/main/java/zone/moddev/mc/poweradvantage/util/PowerHelper.java +++ b/src/main/java/zone/moddev/mc/poweradvantage/util/PowerHelper.java @@ -5,7 +5,7 @@ import cyano.poweradvantage.api.PowerConnectorContext; import net.minecraft.block.ITileEntityProvider; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraftforge.energy.CapabilityEnergy; /** @@ -31,7 +31,7 @@ public static boolean areConnectable(PowerConnectorContext connection) { && PowerAdvantage.forgeEnergyConversionTable.containsKey(connection.powerType)) { TileEntity tile = connection.world.getTileEntity(connection.otherBlockPosition); if (tile == null) return false; - for (EnumFacing side : EnumFacing.values()) { + for (Direction side : Direction.values()) { if (tile.getCapability(CapabilityEnergy.ENERGY, side).isPresent()) return true; } } diff --git a/src/main/resources/META-INF/coremods.json b/src/main/resources/META-INF/coremods.json index e788d50..9de3539 100644 --- a/src/main/resources/META-INF/coremods.json +++ b/src/main/resources/META-INF/coremods.json @@ -1,6 +1,3 @@ { - "poweradvantage_113_migration": "coremods/poweradvantage_113_migration.js", - "poweradvantage_oil_renderer": "coremods/poweradvantage_oil_renderer.js", - "poweradvantage_material_tags": "coremods/poweradvantage_material_tags.js", - "poweradvantage_parts_loot": "coremods/poweradvantage_parts_loot.js" + "poweradvantage_114_migration": "coremods/poweradvantage_114_migration.js" } diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index f2b5cab..f430b5e 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -14,14 +14,14 @@ description='''API and shared machines for pressure, electricity, quantum power, [[dependencies.poweradvantage]] modId="basemetals" mandatory=false -versionRange="[3.0.1.113021,4.0.0)" +versionRange="[3.0.1.114041,4.0.0)" ordering="AFTER" side="BOTH" [[dependencies.poweradvantage]] modId="orespawn" mandatory=true -versionRange="[4.1.0.113021,5.0.0)" +versionRange="[4.1.1.114041,5.0.0)" ordering="AFTER" side="BOTH" diff --git a/src/main/resources/coremods/poweradvantage_113_migration.js b/src/main/resources/coremods/poweradvantage_114_migration.js similarity index 72% rename from src/main/resources/coremods/poweradvantage_113_migration.js rename to src/main/resources/coremods/poweradvantage_114_migration.js index 0d430ad..0c4cdf8 100644 --- a/src/main/resources/coremods/poweradvantage_113_migration.js +++ b/src/main/resources/coremods/poweradvantage_114_migration.js @@ -6,12 +6,13 @@ var LabelNode = Java.type('org.objectweb.asm.tree.LabelNode'); var MethodInsnNode = Java.type('org.objectweb.asm.tree.MethodInsnNode'); var VarInsnNode = Java.type('org.objectweb.asm.tree.VarInsnNode'); +// Only the pre-datafix migration needs bytecode integration on Forge 1.14. var hook = 'zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook'; function initializeCoreMod() { return { 'poweradvantage_legacy_world_info': { - 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.storage.SaveFormatOld' }, + 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.storage.SaveFormat' }, 'transformer': function(classNode) { var patched = false; for (var i = 0; i < classNode.methods.size(); ++i) { @@ -25,44 +26,39 @@ function initializeCoreMod() { method.instructions.insert(prefix); patched = true; } - if (!patched) throw new Error('Power Advantage could not patch the Forge 25 legacy world reader'); + if (!patched) throw new Error('Power Advantage could not attach its legacy world reader'); return classNode; } }, 'poweradvantage_legacy_chunk_status': { - 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.chunk.storage.AnvilChunkLoader' }, + 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.chunk.storage.ChunkLoader' }, 'transformer': function(classNode) { var descriptor = '(Lnet/minecraft/world/dimension/DimensionType;' + - 'Lnet/minecraft/world/storage/WorldSavedDataStorage;II)' + - 'Lnet/minecraft/nbt/NBTTagCompound;'; + 'Ljava/util/function/Supplier;Lnet/minecraft/nbt/CompoundNBT;)' + + 'Lnet/minecraft/nbt/CompoundNBT;'; var patchedRead = false; var patchedReturn = false; for (var i = 0; i < classNode.methods.size(); ++i) { var method = classNode.methods.get(i); if (method.desc !== descriptor) continue; + var prepare = new InsnList(); + prepare.add(new VarInsnNode(Opcodes.ALOAD, 3)); + prepare.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, + 'prepareLegacyChunk', '(Lnet/minecraft/nbt/CompoundNBT;)V', false)); + method.instructions.insert(prepare); + patchedRead = true; for (var instruction = method.instructions.getFirst(); instruction !== null; instruction = instruction.getNext()) { - var previous = instruction.getPrevious(); - if (instruction.getOpcode() === Opcodes.ASTORE && previous !== null - && previous.getOpcode() === Opcodes.INVOKESTATIC - && previous.owner === 'net/minecraft/nbt/CompressedStreamTools' - && previous.desc.endsWith(')Lnet/minecraft/nbt/NBTTagCompound;')) { - var prepare = new InsnList(); - prepare.add(new VarInsnNode(Opcodes.ALOAD, instruction.var)); - prepare.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, - 'prepareLegacyChunk', '(Lnet/minecraft/nbt/NBTTagCompound;)V', false)); - method.instructions.insert(instruction, prepare); - patchedRead = true; - } else if (instruction.getOpcode() === Opcodes.ARETURN) { + if (instruction.getOpcode() === Opcodes.ARETURN) { method.instructions.insertBefore(instruction, new MethodInsnNode(Opcodes.INVOKESTATIC, hook, 'finalizeLegacyChunk', - '(Lnet/minecraft/nbt/NBTTagCompound;)Lnet/minecraft/nbt/NBTTagCompound;', false)); + '(Lnet/minecraft/nbt/CompoundNBT;)Lnet/minecraft/nbt/CompoundNBT;', false)); patchedReturn = true; } } } if (!patchedRead || !patchedReturn) { - throw new Error('Power Advantage could not patch the Forge 25 legacy chunk loader'); + throw new Error('Power Advantage could not attach its legacy chunk converter'); } return classNode; } @@ -71,7 +67,7 @@ function initializeCoreMod() { 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.gen.WorldGenRegion' }, 'transformer': function(classNode) { var descriptor = '(Lnet/minecraft/util/math/BlockPos;' + - 'Lnet/minecraft/block/state/IBlockState;I)Z'; + 'Lnet/minecraft/block/BlockState;I)Z'; var patched = false; for (var i = 0; i < classNode.methods.size(); ++i) { var method = classNode.methods.get(i); @@ -88,7 +84,7 @@ function initializeCoreMod() { method.instructions.insert(prefix); patched = true; } - if (!patched) throw new Error('Power Advantage could not patch the Forge 25 legacy-world write guard'); + if (!patched) throw new Error('Power Advantage could not protect existing legacy terrain'); return classNode; } } diff --git a/src/main/resources/coremods/poweradvantage_material_tags.js b/src/main/resources/coremods/poweradvantage_material_tags.js deleted file mode 100644 index 6923c54..0000000 --- a/src/main/resources/coremods/poweradvantage_material_tags.js +++ /dev/null @@ -1,28 +0,0 @@ -var Opcodes = Java.type('org.objectweb.asm.Opcodes'); -var MethodInsnNode = Java.type('org.objectweb.asm.tree.MethodInsnNode'); - -function initializeCoreMod() { - return { - 'poweradvantage_material_tags': { - 'target': { 'type': 'CLASS', 'name': 'net.minecraft.tags.NetworkTagManager' }, - 'transformer': function(classNode) { - var patched = 0; - for (var i = 0; i < classNode.methods.size(); ++i) { - var method = classNode.methods.get(i); - if (method.desc !== '(Lnet/minecraft/resources/IResourceManager;)V') continue; - for (var instruction = method.instructions.getFirst(); instruction !== null; - instruction = instruction.getNext()) { - if (instruction.getOpcode() !== Opcodes.RETURN) continue; - // Forge 25 has no tags-loaded event. This runs after tag resolution, - // but before the following recipe and advancement reload listeners. - method.instructions.insertBefore(instruction, new MethodInsnNode(Opcodes.INVOKESTATIC, - 'zone/moddev/mc/poweradvantage/recipe/MaterialTags', 'finishReload', '()V', false)); - patched++; - } - } - if (patched !== 1) throw new Error('Power Advantage could not locate the Forge 25 tag reload boundary'); - return classNode; - } - } - }; -} diff --git a/src/main/resources/coremods/poweradvantage_oil_renderer.js b/src/main/resources/coremods/poweradvantage_oil_renderer.js deleted file mode 100644 index 8b7ad02..0000000 --- a/src/main/resources/coremods/poweradvantage_oil_renderer.js +++ /dev/null @@ -1,55 +0,0 @@ -var Opcodes = Java.type('org.objectweb.asm.Opcodes'); -var InsnList = Java.type('org.objectweb.asm.tree.InsnList'); -var MethodInsnNode = Java.type('org.objectweb.asm.tree.MethodInsnNode'); -var VarInsnNode = Java.type('org.objectweb.asm.tree.VarInsnNode'); - -function initializeCoreMod() { - return { - 'poweradvantage_oil_renderer': { - 'target': { 'type': 'CLASS', 'name': 'net.minecraft.client.renderer.BlockFluidRenderer' }, - 'transformer': function(classNode) { - var descriptor = '(Lnet/minecraft/world/IWorldReader;Lnet/minecraft/util/math/BlockPos;' + - 'Lnet/minecraft/client/renderer/BufferBuilder;Lnet/minecraft/fluid/IFluidState;)Z'; - var patched = false; - for (var i = 0; i < classNode.methods.size(); ++i) { - var method = classNode.methods.get(i); - // The descriptor also matches the production SRG-named method. - if (method.desc !== descriptor) continue; - var flag = false, sprites = false, color = false; - for (var instruction = method.instructions.getFirst(); instruction !== null; - instruction = instruction.getNext()) { - var hookName = null, hookDescriptor = null; - if (!flag && instruction.getOpcode() === Opcodes.ISTORE && instruction.var === 5) { - hookName = 'useOilPath'; - hookDescriptor = '(Lnet/minecraft/fluid/IFluidState;Z)Z'; - flag = true; - } else if (!sprites && instruction.getOpcode() === Opcodes.ASTORE && instruction.var === 6) { - hookName = 'overrideSprites'; - hookDescriptor = '(Lnet/minecraft/fluid/IFluidState;[Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;)' + - '[Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;'; - sprites = true; - } else if (!color && instruction.getOpcode() === Opcodes.ISTORE && instruction.var === 7) { - hookName = 'overrideColor'; - hookDescriptor = '(Lnet/minecraft/fluid/IFluidState;I)I'; - color = true; - } - if (hookName === null) continue; - var reference = instruction.var === 6; - var hook = new InsnList(); - hook.add(new VarInsnNode(Opcodes.ALOAD, 4)); - hook.add(new VarInsnNode(reference ? Opcodes.ALOAD : Opcodes.ILOAD, instruction.var)); - hook.add(new MethodInsnNode(Opcodes.INVOKESTATIC, - 'zone/moddev/mc/poweradvantage/client/ClientOilRenderer', - hookName, hookDescriptor, false)); - hook.add(new VarInsnNode(reference ? Opcodes.ASTORE : Opcodes.ISTORE, instruction.var)); - method.instructions.insert(instruction, hook); - } - if (!flag || !sprites || !color) throw new Error('Power Advantage could not patch the Forge 25 fluid renderer'); - patched = true; - } - if (!patched) throw new Error('Power Advantage could not find the Forge 25 fluid renderer'); - return classNode; - } - } - }; -} diff --git a/src/main/resources/coremods/poweradvantage_parts_loot.js b/src/main/resources/coremods/poweradvantage_parts_loot.js deleted file mode 100644 index 3698954..0000000 --- a/src/main/resources/coremods/poweradvantage_parts_loot.js +++ /dev/null @@ -1,25 +0,0 @@ -var MethodInsnNode = Java.type('org.objectweb.asm.tree.MethodInsnNode'); - -function initializeCoreMod() { - return { - 'poweradvantage_parts_loot': { - 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.storage.loot.LootTableManager' }, - 'transformer': function(classNode) { - var patched = 0; - for (var i = 0; i < classNode.methods.size(); ++i) { - var method = classNode.methods.get(i); - for (var instruction = method.instructions.getFirst(); instruction !== null; - instruction = instruction.getNext()) { - if (instruction instanceof MethodInsnNode && instruction.owner === 'net/minecraftforge/common/ForgeHooks' - && instruction.name === 'loadLootTable') { - instruction.owner = 'zone/moddev/mc/poweradvantage/recipe/PartsLoot'; - patched++; - } - } - } - if (patched !== 1) throw new Error('Power Advantage could not locate the Forge 25 loot loading bridge'); - return classNode; - } - } - }; -} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_electricity.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_electricity.json new file mode 100644 index 0000000..841e0a2 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_electricity.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:converter_rf_electricity" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_quantum.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_quantum.json new file mode 100644 index 0000000..231a57a --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_quantum.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:converter_rf_quantum" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_steam.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_steam.json new file mode 100644 index 0000000..0773702 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_rf_steam.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:converter_rf_steam" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_tr_electricity.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_tr_electricity.json new file mode 100644 index 0000000..065fbc4 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/converter_tr_electricity.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:converter_tr_electricity" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/crude_oil.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/crude_oil.json new file mode 100644 index 0000000..7b59ea1 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/crude_oil.json @@ -0,0 +1,4 @@ +{ + "type": "minecraft:block", + "pools": [] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_discharge.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_discharge.json new file mode 100644 index 0000000..298c05f --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_discharge.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:fluid_discharge" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_drain.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_drain.json new file mode 100644 index 0000000..b2ad111 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_drain.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:fluid_drain" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_metal_tank.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_metal_tank.json new file mode 100644 index 0000000..acd5684 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_metal_tank.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:fluid_metal_tank" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_pipe.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_pipe.json new file mode 100644 index 0000000..3d5f6f4 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_pipe.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:fluid_pipe" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_pipe_terminal.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_pipe_terminal.json new file mode 100644 index 0000000..3d5f6f4 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_pipe_terminal.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:fluid_pipe" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_storage_tank.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_storage_tank.json new file mode 100644 index 0000000..f36a05c --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_storage_tank.json @@ -0,0 +1,37 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:fluid_storage_tank", + "functions": [ + { + "function": "minecraft:copy_nbt", + "source": "block_entity", + "ops": [ + { + "source": "Tank.FluidName", + "target": "FluidID", + "op": "replace" + }, + { + "source": "Tank.Amount", + "target": "Volume", + "op": "replace" + } + ] + } + ] + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_switch.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_switch.json new file mode 100644 index 0000000..c7f47ed --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/fluid_switch.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:fluid_switch" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_electricity.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_electricity.json new file mode 100644 index 0000000..42ebe1c --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_electricity.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:infinite_electricity" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_quantum.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_quantum.json new file mode 100644 index 0000000..104c117 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_quantum.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:infinite_quantum" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_steam.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_steam.json new file mode 100644 index 0000000..9192aa7 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/infinite_steam.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:infinite_steam" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_conveyor.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_conveyor.json new file mode 100644 index 0000000..b86de0c --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_conveyor.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:item_conveyor" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_block.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_block.json new file mode 100644 index 0000000..49abd9f --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_block.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:item_filter_block" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_food.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_food.json new file mode 100644 index 0000000..d4af70a --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_food.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:item_filter_food" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_fuel.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_fuel.json new file mode 100644 index 0000000..242a6e9 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_fuel.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:item_filter_fuel" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_inventory.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_inventory.json new file mode 100644 index 0000000..6f6f24d --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_inventory.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:item_filter_inventory" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_ore.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_ore.json new file mode 100644 index 0000000..6d82c6c --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_ore.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:item_filter_ore" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_overflow.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_overflow.json new file mode 100644 index 0000000..5e9446a --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_overflow.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:item_filter_overflow" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_plant.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_plant.json new file mode 100644 index 0000000..e7894ef --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_plant.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:item_filter_plant" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_smelt.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_smelt.json new file mode 100644 index 0000000..9d3df40 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/item_filter_smelt.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:item_filter_smelt" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/refined_oil.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/refined_oil.json new file mode 100644 index 0000000..7b59ea1 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/refined_oil.json @@ -0,0 +1,4 @@ +{ + "type": "minecraft:block", + "pools": [] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/steel_frame.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/steel_frame.json new file mode 100644 index 0000000..9dd9236 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/steel_frame.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:steel_frame" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/main/resources/data/poweradvantage/loot_tables/blocks/still.json b/src/main/resources/data/poweradvantage/loot_tables/blocks/still.json new file mode 100644 index 0000000..e71b507 --- /dev/null +++ b/src/main/resources/data/poweradvantage/loot_tables/blocks/still.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "poweradvantage:still" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/init/FluidLookupTest.java b/src/test/java/zone/moddev/mc/poweradvantage/init/FluidLookupTest.java new file mode 100644 index 0000000..f51976e --- /dev/null +++ b/src/test/java/zone/moddev/mc/poweradvantage/init/FluidLookupTest.java @@ -0,0 +1,37 @@ +package zone.moddev.mc.poweradvantage.init; + +import cyano.poweradvantage.api.fluid.FluidStack; +import net.minecraft.nbt.CompoundNBT; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +public class FluidLookupTest { + @BeforeClass public static void bootstrap() { net.minecraft.util.registry.Bootstrap.register(); } + + @Test public void resolvesLegacyUnqualifiedVanillaNamesInsteadOfTheEmptyDefault() { + assertSame(net.minecraft.fluid.Fluids.WATER, Fluids.byName("water")); + assertSame(net.minecraft.fluid.Fluids.LAVA, Fluids.byName("lava")); + assertSame(net.minecraft.fluid.Fluids.WATER, Fluids.byName("minecraft:water")); + } + + @Test public void unknownNamesAreNotReplacedByAnEmptyFluid() { + assertNull(Fluids.byName("not_registered")); + assertNull(Fluids.byName("missingmod:not_registered")); + assertNull(Fluids.byName(null)); + } + + @Test public void retainsAmountAndNbtWhenReadingLegacySavedWater() { + CompoundNBT data = new CompoundNBT(); + data.putString("FluidName", "water"); + data.putInt("Amount", 700); + CompoundNBT tag = new CompoundNBT(); + tag.putInt("batch", 37); + data.put("Tag", tag); + FluidStack stack = FluidStack.loadFluidStackFromNBT(data); + assertNotNull(stack); + assertSame(net.minecraft.fluid.Fluids.WATER, stack.getFluid()); + assertEquals(700, stack.amount); + assertEquals(tag, stack.tag); + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEnchantmentsTest.java b/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEnchantmentsTest.java index abc9d52..52acfef 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEnchantmentsTest.java +++ b/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEnchantmentsTest.java @@ -2,8 +2,8 @@ import java.util.LinkedHashMap; import java.util.Map; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.ListNBT; import org.junit.Test; import static org.junit.Assert.*; @@ -14,15 +14,15 @@ public class LegacyEnchantmentsTest { saved.put(11, "steamadvantage:powderless"); saved.put(12, "steamadvantage:rapid_reload"); saved.put(13, "steamadvantage:recoil"); - NBTTagCompound tag = new NBTTagCompound(); - NBTTagList list = new NBTTagList(); + CompoundNBT tag = new CompoundNBT(); + ListNBT list = new ListNBT(); for (Integer id : saved.keySet()) { - NBTTagCompound entry = new NBTTagCompound(); - entry.setShort("id", id.shortValue()); - entry.setShort("lvl", (short) 3); + CompoundNBT entry = new CompoundNBT(); + entry.putShort("id", id.shortValue()); + entry.putShort("lvl", (short) 3); list.add(entry); } - tag.setTag("ench", list); + tag.put("ench", list); assertTrue(LegacyWorldDataHook.migrateLegacyEnchantments(tag, saved)); assertFalse(tag.contains("ench", 9)); for (int i = 0; i < 4; i++) { @@ -35,13 +35,13 @@ public class LegacyEnchantmentsTest { @Test public void honorsWorldSpecificIdsAndPreservesStoredEnchantments() { Map saved = new LinkedHashMap<>(); saved.put(173, "steamadvantage:rapid_reload"); - NBTTagCompound book = new NBTTagCompound(); - NBTTagList list = new NBTTagList(); - NBTTagCompound entry = new NBTTagCompound(); - entry.setShort("id", (short) 173); - entry.setShort("lvl", (short) 2); + CompoundNBT book = new CompoundNBT(); + ListNBT list = new ListNBT(); + CompoundNBT entry = new CompoundNBT(); + entry.putShort("id", (short) 173); + entry.putShort("lvl", (short) 2); list.add(entry); - book.setTag("StoredEnchantments", list); + book.put("StoredEnchantments", list); assertTrue(LegacyWorldDataHook.migrateLegacyEnchantments(book, saved)); assertEquals("steamadvantage:rapid_reload", book.getList("StoredEnchantments", 10).getCompound(0).getString("id")); assertEquals(2, book.getList("StoredEnchantments", 10).getCompound(0).getShort("lvl")); @@ -51,12 +51,12 @@ public class LegacyEnchantmentsTest { public void refusesIncompleteOldListInsteadOfSilentlyDroppingAnEnchantment() { Map saved = new LinkedHashMap<>(); saved.put(10, "steamadvantage:high_explosive"); - NBTTagCompound tag = new NBTTagCompound(); - NBTTagList list = new NBTTagList(); - NBTTagCompound entry = new NBTTagCompound(); - entry.setShort("id", (short) 99); + CompoundNBT tag = new CompoundNBT(); + ListNBT list = new ListNBT(); + CompoundNBT entry = new CompoundNBT(); + entry.putShort("id", (short) 99); list.add(entry); - tag.setTag("ench", list); + tag.put("ench", list); LegacyWorldDataHook.migrateLegacyEnchantments(tag, saved); } } diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEntitiesTest.java b/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEntitiesTest.java index 5d1262d..478f9ac 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEntitiesTest.java +++ b/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEntitiesTest.java @@ -1,8 +1,8 @@ package zone.moddev.mc.poweradvantage.migration; -import com.mojang.datafixers.DataFixTypes; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.datafix.DefaultTypeReferences; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.ListNBT; import net.minecraft.nbt.NBTUtil; import net.minecraft.util.datafix.DataFixesManager; import org.junit.Test; @@ -19,24 +19,24 @@ public class LegacyEntitiesTest { } @Test public void fixesVanillaPassengersInsideUnknownModEntities() { - NBTTagCompound vehicle = entity("example:vehicle"); - vehicle.setString("CustomState", "keep-verbatim"); - NBTTagCompound wolf = entity("Wolf"); - wolf.setString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f"); - NBTTagCompound nested = entity("example:seat"); - NBTTagList nestedPassengers = new NBTTagList(); + CompoundNBT vehicle = entity("example:vehicle"); + vehicle.putString("CustomState", "keep-verbatim"); + CompoundNBT wolf = entity("Wolf"); + wolf.putString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f"); + CompoundNBT nested = entity("example:seat"); + ListNBT nestedPassengers = new ListNBT(); nestedPassengers.add(entity("Cow")); - nested.setTag("Passengers", nestedPassengers); - NBTTagList passengers = new NBTTagList(); + nested.put("Passengers", nestedPassengers); + ListNBT passengers = new ListNBT(); passengers.add(wolf); passengers.add(nested); - vehicle.setTag("Passengers", passengers); - NBTTagCompound root = chunk(vehicle, 512); + vehicle.put("Passengers", passengers); + CompoundNBT root = chunk(vehicle, 512); LegacyWorldDataHook.prepareLegacyChunk(root); - NBTTagCompound actual = root.getCompound("Level").getList("Entities", 10).getCompound(0); + CompoundNBT actual = root.getCompound("Level").getList("Entities", 10).getCompound(0); assertEquals("example:vehicle", actual.getString("id")); assertEquals("keep-verbatim", actual.getString("CustomState")); - NBTTagList fixed = actual.getList("Passengers", 10); + ListNBT fixed = actual.getList("Passengers", 10); assertEquals(2, fixed.size()); assertEquals("minecraft:wolf", fixed.getCompound(0).getString("id")); assertEquals(wolf.getString("OwnerUUID"), fixed.getCompound(0).getString("OwnerUUID")); @@ -45,73 +45,73 @@ public class LegacyEntitiesTest { } @Test public void usesVanillaSubtypeFixersRatherThanLowercasingOldIds() { - NBTTagCompound skeleton = entity("Skeleton"); - skeleton.setByte("SkeletonType", (byte) 1); - NBTTagCompound root = chunk(skeleton, 512); + CompoundNBT skeleton = entity("Skeleton"); + skeleton.putByte("SkeletonType", (byte) 1); + CompoundNBT root = chunk(skeleton, 512); LegacyWorldDataHook.prepareLegacyChunk(root); assertEquals("minecraft:wither_skeleton", root.getCompound("Level").getList("Entities", 10).getCompound(0).getString("id")); } @Test public void leavesModernEntityAndUnrelatedCustomNbtUnchangedOnRepeatedPreparation() { - NBTTagCompound wolf = entity("minecraft:wolf"); - wolf.setString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f"); - wolf.setTag("CustomData", entity("Wolf")); - NBTTagCompound root = chunk(wolf, 1343); + CompoundNBT wolf = entity("minecraft:wolf"); + wolf.putString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f"); + wolf.put("CustomData", entity("Wolf")); + CompoundNBT root = chunk(wolf, 1343); LegacyWorldDataHook.prepareLegacyChunk(root); LegacyWorldDataHook.prepareLegacyChunk(root); assertEquals(wolf, root.getCompound("Level").getList("Entities", 10).getCompound(0)); } @Test public void repeatPreparationDoesNotLoseConvertedVanillaState() { - NBTTagCompound wolf = entity("Wolf"); - wolf.setString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f"); - NBTTagCompound root = chunk(wolf, 512); + CompoundNBT wolf = entity("Wolf"); + wolf.putString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f"); + CompoundNBT root = chunk(wolf, 512); LegacyWorldDataHook.prepareLegacyChunk(root); - NBTTagCompound once = root.copy(); + CompoundNBT once = root.copy(); LegacyWorldDataHook.prepareLegacyChunk(root); assertEquals(once, root); } - private static NBTTagCompound entity(String id) { - NBTTagCompound entity = new NBTTagCompound(); - entity.setString("id", id); + private static CompoundNBT entity(String id) { + CompoundNBT entity = new CompoundNBT(); + entity.putString("id", id); return entity; } - private static NBTTagCompound chunk(NBTTagCompound entity, int version) { - NBTTagCompound root = new NBTTagCompound(); - root.setInt("DataVersion", version); - NBTTagCompound level = new NBTTagCompound(); - NBTTagList entities = new NBTTagList(); + private static CompoundNBT chunk(CompoundNBT entity, int version) { + CompoundNBT root = new CompoundNBT(); + root.putInt("DataVersion", version); + CompoundNBT level = new CompoundNBT(); + ListNBT entities = new ListNBT(); entities.add(entity); - level.setTag("Entities", entities); - root.setTag("Level", level); + level.put("Entities", entities); + root.put("Level", level); return root; } private static void assertWolf(boolean customEntity) { - NBTTagCompound root = new NBTTagCompound(); - root.setInt("DataVersion", 512); - NBTTagCompound level = new NBTTagCompound(); - NBTTagList entities = new NBTTagList(); - NBTTagCompound wolf = new NBTTagCompound(); - wolf.setString("id", "Wolf"); - wolf.setString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f"); - wolf.setBoolean("Sitting", true); - wolf.setLong("UUIDMost", 123L); - wolf.setLong("UUIDLeast", 456L); + CompoundNBT root = new CompoundNBT(); + root.putInt("DataVersion", 512); + CompoundNBT level = new CompoundNBT(); + ListNBT entities = new ListNBT(); + CompoundNBT wolf = new CompoundNBT(); + wolf.putString("id", "Wolf"); + wolf.putString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f"); + wolf.putBoolean("Sitting", true); + wolf.putLong("UUIDMost", 123L); + wolf.putLong("UUIDLeast", 456L); entities.add(wolf); if (customEntity) { - NBTTagCompound turbine = new NBTTagCompound(); - turbine.setString("id", "electricadvantage.electricadvantage.Hydroturbine"); - turbine.setIntArray("parentPos", new int[] {164, 81, 8}); + CompoundNBT turbine = new CompoundNBT(); + turbine.putString("id", "electricadvantage.electricadvantage.Hydroturbine"); + turbine.putIntArray("parentPos", new int[] {164, 81, 8}); entities.add(turbine); } - level.setTag("Entities", entities); - root.setTag("Level", level); + level.put("Entities", entities); + root.put("Level", level); LegacyWorldDataHook.prepareLegacyChunk(root); - NBTTagCompound fixed = NBTUtil.update(DataFixesManager.getDataFixer(), DataFixTypes.CHUNK, root, 512, 1343); - NBTTagCompound actual = fixed.getCompound("Level").getList("Entities", 10).getCompound(0); + CompoundNBT fixed = NBTUtil.update(DataFixesManager.getDataFixer(), DefaultTypeReferences.CHUNK, root, 512, 1343); + CompoundNBT actual = fixed.getCompound("Level").getList("Entities", 10).getCompound(0); assertEquals("minecraft:wolf", actual.getString("id")); assertEquals(wolf.getString("OwnerUUID"), actual.getString("OwnerUUID")); assertTrue(actual.getBoolean("Sitting")); diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyTransformerOrderTest.java b/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyTransformerOrderTest.java new file mode 100644 index 0000000..966773d --- /dev/null +++ b/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyTransformerOrderTest.java @@ -0,0 +1,72 @@ +package zone.moddev.mc.poweradvantage.migration; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import org.junit.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import static org.junit.Assert.*; + +/** Applies both real migration scripts to the exact native Forge classes. */ +public class LegacyTransformerOrderTest { + @Test public void advantageThenBaseMetalsPreservesBothHooks() throws Exception { check(false, false); } + @Test public void baseMetalsThenAdvantagePreservesBothHooks() throws Exception { check(true, false); } + @Test public void productionMethodNamesDoNotChangeEitherOrdering() throws Exception { + check(false, true); + check(true, true); + } + + private static void check(boolean baseFirst, boolean productionNames) throws Exception { + String[][] targets = { + {"net/minecraft/world/storage/SaveFormat", "legacy_world_info", "prepareLegacyWorld"}, + {"net/minecraft/world/chunk/storage/ChunkLoader", "legacy_chunk_status", "prepareLegacyChunk"}, + {"net/minecraft/world/gen/WorldGenRegion", "legacy_worldgen_guard", "shouldBlockWorldgenWrite"} + }; + for (String[] target : targets) { + ClassNode node = new ClassNode(); + try (InputStream input = getResource("/" + target[0] + ".class")) { new ClassReader(input).accept(node, 0); } + if (productionNames) for (MethodNode method : node.methods) + if (!method.name.startsWith("<")) method.name = "test_srg_" + method.name; + transform(node, target[1], baseFirst); + transform(node, target[1], !baseFirst); + assertEquals(target[0], 1, calls(node, "poweradvantage", target[2])); + assertEquals(target[0], 1, calls(node, "basemetals", target[2])); + if (target[1].equals("legacy_chunk_status")) { + assertEquals(1, calls(node, "poweradvantage", "finalizeLegacyChunk")); + assertEquals(1, calls(node, "basemetals", "finalizeLegacyChunk")); + } + } + } + + private static void transform(ClassNode node, String target, boolean base) throws Exception { + String mod = base ? "basemetals" : "poweradvantage"; + String path = base ? "/coremods/basemetals_114_compatibility.js" : "/coremods/poweradvantage_114_migration.js"; + ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); + assertNotNull("Transformer tests require the Java 8 test toolchain", engine); + try (InputStreamReader reader = new InputStreamReader(getResource(path), StandardCharsets.UTF_8)) { engine.eval(reader); } + engine.put("targetClass", node); + assertSame(node, engine.eval("initializeCoreMod()." + mod + "_" + target + ".transformer(targetClass)")); + } + + private static InputStream getResource(String name) { + InputStream input = LegacyTransformerOrderTest.class.getResourceAsStream(name); + assertNotNull("Missing pinned native dependency resource " + name, input); + return input; + } + + private static int calls(ClassNode node, String mod, String name) { + int calls = 0; + for (MethodNode method : node.methods) for (AbstractInsnNode instruction : method.instructions.toArray()) { + if (!(instruction instanceof MethodInsnNode)) continue; + MethodInsnNode call = (MethodInsnNode) instruction; + if (call.owner.equals("zone/moddev/mc/" + mod + "/migration/LegacyWorldDataHook") && call.name.equals(name)) calls++; + } + return calls; + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/RetiredProfessionSnapshotTest.java b/src/test/java/zone/moddev/mc/poweradvantage/migration/RetiredProfessionSnapshotTest.java new file mode 100644 index 0000000..179777e --- /dev/null +++ b/src/test/java/zone/moddev/mc/poweradvantage/migration/RetiredProfessionSnapshotTest.java @@ -0,0 +1,45 @@ +package zone.moddev.mc.poweradvantage.migration; + +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.ListNBT; +import org.junit.Test; +import static org.junit.Assert.*; + +public class RetiredProfessionSnapshotTest { + @Test public void removesOnlyRetiredVanillaIndexAndIsIdempotent() { + for (String namespace : new String[] {"FML", "fml"}) { + CompoundNBT root = world(namespace, "minecraft:farmer"); + CompoundNBT player = new CompoundNBT(); + player.putInt("XpLevel", 34); + root.put("Player", player); + assertTrue(LegacyWorldDataHook.removeRetiredVanillaProfessionRegistry(root)); + assertFalse(root.getCompound(namespace).getCompound("Registries").contains("minecraft:villagerprofessions")); + assertEquals(player, root.getCompound("Player")); + assertFalse(LegacyWorldDataHook.removeRetiredVanillaProfessionRegistry(root)); + } + } + + @Test public void preservesCustomProfessionIndexInsteadOfHidingMissingMappings() { + CompoundNBT root = world("fml", "othermod:machinist"); + CompoundNBT before = root.copy(); + assertFalse(LegacyWorldDataHook.removeRetiredVanillaProfessionRegistry(root)); + assertEquals(before, root); + } + + private static CompoundNBT world(String namespace, String profession) { + CompoundNBT entry = new CompoundNBT(); + entry.putString("K", profession); + entry.putInt("V", 1); + ListNBT ids = new ListNBT(); + ids.add(entry); + CompoundNBT snapshot = new CompoundNBT(); + snapshot.put("ids", ids); + CompoundNBT registries = new CompoundNBT(); + registries.put("minecraft:villagerprofessions", snapshot); + CompoundNBT fml = new CompoundNBT(); + fml.put("Registries", registries); + CompoundNBT root = new CompoundNBT(); + root.put(namespace, fml); + return root; + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/recipe/MaterialTagsTest.java b/src/test/java/zone/moddev/mc/poweradvantage/recipe/MaterialTagsTest.java new file mode 100644 index 0000000..36ae3cc --- /dev/null +++ b/src/test/java/zone/moddev/mc/poweradvantage/recipe/MaterialTagsTest.java @@ -0,0 +1,56 @@ +package zone.moddev.mc.poweradvantage.recipe; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import net.minecraft.item.Item; +import net.minecraft.item.Items; +import net.minecraft.tags.Tag; +import net.minecraft.tags.TagCollection; +import net.minecraft.util.ResourceLocation; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +public class MaterialTagsTest { + @BeforeClass public static void bootstrap() { net.minecraft.util.registry.Bootstrap.register(); } + + @Test public void updatesNativeImmutableCollectionWithoutLosingUserEntries() { + TagCollection tags = fixture(); + try { + tags.getTagMap().put(new ResourceLocation("test", "must_be_immutable"), null); + fail("Fixture must reproduce native immutable tag maps"); + } catch (UnsupportedOperationException expected) { } + MaterialTags.applyPolicy(tags, Items.APPLE, true, true); + assertTrue(tag(tags, "forge:rubber").contains(Items.STICK)); + assertTrue(tag(tags, "forge:rubber").contains(Items.APPLE)); + assertTrue(tag(tags, "forge:ingots/rubber").contains(Items.APPLE)); + assertTrue(tag(tags, "forge:solder").contains(Items.IRON_INGOT)); + assertTrue(tag(tags, "forge:solder").contains(Items.GOLD_INGOT)); + assertTrue(tag(tags, "test:unrelated").contains(Items.REDSTONE)); + MaterialTags.applyPolicy(tags, Items.APPLE, true, true); + assertEquals(2, tag(tags, "forge:rubber").getAllElements().size()); + assertEquals(2, tag(tags, "forge:solder").getAllElements().size()); + } + + @Test public void disabledOptionsDoNotAddAliases() { + TagCollection tags = fixture(); + MaterialTags.applyPolicy(tags, Items.APPLE, false, false); + assertFalse(tag(tags, "forge:rubber").contains(Items.APPLE)); + assertTrue(tag(tags, "forge:solder").getAllElements().isEmpty()); + assertTrue(tag(tags, "test:unrelated").contains(Items.REDSTONE)); + } + + private static Tag tag(TagCollection tags, String name) { return tags.getOrCreate(new ResourceLocation(name)); } + + private static TagCollection fixture() { + TagCollection tags = new TagCollection<>(id -> Optional.empty(), "tags/items", false, "item"); + Map> builders = new LinkedHashMap<>(); + builders.put(new ResourceLocation("forge:rubber"), Tag.Builder.create().add(Items.STICK)); + builders.put(new ResourceLocation("forge:ingots/lead"), Tag.Builder.create().add(Items.IRON_INGOT)); + builders.put(new ResourceLocation("forge:ingots/tin"), Tag.Builder.create().add(Items.GOLD_INGOT)); + builders.put(new ResourceLocation("test:unrelated"), Tag.Builder.create().add(Items.REDSTONE)); + tags.registerAll(builders); + return tags; + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/recipe/PartsLootTest.java b/src/test/java/zone/moddev/mc/poweradvantage/recipe/PartsLootTest.java new file mode 100644 index 0000000..9200581 --- /dev/null +++ b/src/test/java/zone/moddev/mc/poweradvantage/recipe/PartsLootTest.java @@ -0,0 +1,40 @@ +package zone.moddev.mc.poweradvantage.recipe; + +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.storage.loot.LootTable; +import org.junit.Test; +import static org.junit.Assert.*; + +public class PartsLootTest { + @Test public void addsOnePoolPerLoadedModWithoutDuplicatingOnRepeat() { + LootTable table = LootTable.builder().build(); + ResourceLocation chest = new ResourceLocation("minecraft", "chests/simple_dungeon"); + PartsLoot.addPools(table, chest, true, mod -> true); + Object original = table.getPool("steamadvantage:legacy_parts"); + assertNotNull(original); + PartsLoot.addPools(table, chest, true, mod -> true); + assertSame(original, table.getPool("steamadvantage:legacy_parts")); + assertNotNull(table.getPool("poweradvantage:legacy_parts")); + assertNotNull(table.getPool("electricadvantage:legacy_parts")); + } + + @Test public void keepsEndCityExceptionAndAbsentMods() { + LootTable table = LootTable.builder().build(); + PartsLoot.addPools(table, new ResourceLocation("minecraft", "chests/end_city_treasure"), true, + mod -> !mod.equals("steamadvantage")); + assertNull(table.getPool("poweradvantage:legacy_parts")); + assertNull(table.getPool("steamadvantage:legacy_parts")); + assertNotNull(table.getPool("electricadvantage:legacy_parts")); + } + + @Test public void leavesUnrelatedTablesAndDisabledLootUnchanged() { + for (String id : new String[] {"minecraft:chests/desert_pyramid", "othermod:chests/simple_dungeon"}) { + LootTable table = LootTable.builder().build(); + PartsLoot.addPools(table, new ResourceLocation(id), true, mod -> true); + assertNull(table.getPool("poweradvantage:legacy_parts")); + } + LootTable table = LootTable.builder().build(); + PartsLoot.addPools(table, new ResourceLocation("minecraft", "chests/simple_dungeon"), false, mod -> true); + assertNull(table.getPool("poweradvantage:legacy_parts")); + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/BuildWorkflowContractTest.java b/src/test/java/zone/moddev/mc/poweradvantage/resources/BuildWorkflowContractTest.java index e6a6c52..923f190 100644 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/BuildWorkflowContractTest.java +++ b/src/test/java/zone/moddev/mc/poweradvantage/resources/BuildWorkflowContractTest.java @@ -16,7 +16,9 @@ public class BuildWorkflowContractTest { int policy = workflow.indexOf("run: git config --global core.autocrlf false"); int checkout = workflow.indexOf("- name: Check out pinned Base Metals"); assertTrue(name, policy >= 0 && checkout > policy); - assertTrue(name, workflow.contains("ref: 1376a20d7d5ba22d3abc77dc34b49a2c88943697")); + assertTrue(name, workflow.contains("ref: 713802f93f2d1f650e1593fa35c487ea43add4a5")); + assertTrue(name, workflow.contains("ref: 9c2488cba02d317e3f31e24441dcc58273d8d0e6")); + assertTrue(name, workflow.contains("master-1.14.4")); } } } diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/ConverterEnergyDirectionTest.java b/src/test/java/zone/moddev/mc/poweradvantage/resources/ConverterEnergyDirectionTest.java new file mode 100644 index 0000000..5739ba8 --- /dev/null +++ b/src/test/java/zone/moddev/mc/poweradvantage/resources/ConverterEnergyDirectionTest.java @@ -0,0 +1,38 @@ +package zone.moddev.mc.poweradvantage.resources; + +import java.io.InputStream; +import org.junit.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import static org.junit.Assert.*; + +/** The live interop fixture additionally checks exact conservation in all four converters. */ +public class ConverterEnergyDirectionTest { + @Test public void extractedExternalEnergyIsAddedWithoutNegatingItAgain() throws Exception { + ClassNode node = new ClassNode(); + try (InputStream input = getClass().getResourceAsStream("/cyano/poweradvantage/api/modsupport/TileEntityConverter.class")) { + assertNotNull(input); + new ClassReader(input).accept(node, 0); + } + int transfers = 0; + for (MethodNode method : node.methods) { + if (!method.name.equals("powerUpdate")) continue; + boolean extracted = false; + for (AbstractInsnNode instruction : method.instructions.toArray()) { + if (extracted) { + assertNotEquals("Positive external extraction must not be negated", Opcodes.FNEG, instruction.getOpcode()); + assertNotEquals("Positive external extraction must not be multiplied by -1", Opcodes.FMUL, instruction.getOpcode()); + } + if (!(instruction instanceof MethodInsnNode)) continue; + String name = ((MethodInsnNode) instruction).name; + if (name.equals("subtractEnergyFromOther")) extracted = true; + if (extracted && name.equals("addEnergy")) { transfers++; extracted = false; } + } + } + assertEquals("Expected exactly one incoming conversion path", 1, transfers); + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/MaterialTagTransformerTest.java b/src/test/java/zone/moddev/mc/poweradvantage/resources/MaterialTagTransformerTest.java deleted file mode 100644 index e816f94..0000000 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/MaterialTagTransformerTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package zone.moddev.mc.poweradvantage.resources; - -import java.io.InputStream; -import java.io.Reader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; -import org.junit.Test; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import static org.junit.Assert.*; - -public class MaterialTagTransformerTest { - @Test public void hooksThePinnedReloadAfterAllTagCollectionsAreInstalled() throws Exception { - check(false); - } - - @Test public void hooksProductionNamesWithoutDependingOnMappings() throws Exception { - check(true); - } - - private static void check(boolean productionName) throws Exception { - ClassNode node = new ClassNode(); - try (InputStream input = MaterialTagTransformerTest.class.getResourceAsStream( - "/net/minecraft/tags/NetworkTagManager.class")) { - assertNotNull(input); - new ClassReader(input).accept(node, 0); - } - MethodNode reload = null; - for (MethodNode method : node.methods) { - if (method.desc.equals("(Lnet/minecraft/resources/IResourceManager;)V")) reload = method; - } - assertNotNull(reload); - if (productionName) reload.name = "func_195410_a"; - ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); - try (Reader script = Files.newBufferedReader(Paths.get("src/main/resources/coremods/poweradvantage_material_tags.js"), - StandardCharsets.UTF_8)) { engine.eval(script); } - engine.put("manager", node); - engine.eval("initializeCoreMod().poweradvantage_material_tags.transformer(manager)"); - int installed = 0, hooks = 0; - for (AbstractInsnNode instruction : reload.instructions.toArray()) { - if (!(instruction instanceof MethodInsnNode)) continue; - MethodInsnNode call = (MethodInsnNode) instruction; - if (call.owner.startsWith("net/minecraft/tags/") && call.desc.endsWith("TagCollection;)V")) installed++; - if (call.owner.equals("zone/moddev/mc/poweradvantage/recipe/MaterialTags")) { - assertEquals(3, installed); - assertEquals("finishReload", call.name); - assertEquals(Opcodes.RETURN, call.getNext().getOpcode()); - hooks++; - } - } - assertEquals(1, hooks); - } -} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/NativeLootContractTest.java b/src/test/java/zone/moddev/mc/poweradvantage/resources/NativeLootContractTest.java new file mode 100644 index 0000000..d69b782 --- /dev/null +++ b/src/test/java/zone/moddev/mc/poweradvantage/resources/NativeLootContractTest.java @@ -0,0 +1,41 @@ +package zone.moddev.mc.poweradvantage.resources; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Test; +import static org.junit.Assert.*; + +public class NativeLootContractTest { + private static final Path ROOT = Paths.get("src/main/resources"); + + @Test public void everyRegisteredBlockHasANativeLootTable() throws Exception { + try (java.util.stream.Stream states = Files.list(ROOT.resolve("assets/poweradvantage/blockstates"))) { + for (Path file : (Iterable) states.filter(p -> p.toString().endsWith(".json"))::iterator) { + String id = file.getFileName().toString(); + JsonObject table = read(ROOT.resolve("data/poweradvantage/loot_tables/blocks").resolve(id)); + assertEquals(id, "minecraft:block", table.get("type").getAsString()); + if (id.equals("crude_oil.json") || id.equals("refined_oil.json")) assertEquals(0, table.getAsJsonArray("pools").size()); + else assertEquals(id, 1, table.getAsJsonArray("pools").size()); + } + } + } + + @Test public void portableTankCopiesTheExistingItemKeysAndTerminalReturnsAPlainPipe() throws Exception { + JsonObject table = read(ROOT.resolve("data/poweradvantage/loot_tables/blocks/fluid_storage_tank.json")); + String serialized = table.toString(); + assertTrue(serialized.contains("\"source\":\"Tank.FluidName\",\"target\":\"FluidID\"")); + assertTrue(serialized.contains("\"source\":\"Tank.Amount\",\"target\":\"Volume\"")); + JsonObject pipe = read(ROOT.resolve("data/poweradvantage/loot_tables/blocks/fluid_pipe_terminal.json")); + assertEquals("poweradvantage:fluid_pipe", pipe.getAsJsonArray("pools").get(0).getAsJsonObject() + .getAsJsonArray("entries").get(0).getAsJsonObject().get("name").getAsString()); + } + + private static JsonObject read(Path file) throws Exception { + assertTrue("Missing native loot table " + file, Files.isRegularFile(file)); + return new JsonParser().parse(new String(Files.readAllBytes(file), StandardCharsets.UTF_8)).getAsJsonObject(); + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/NativePlatformContractTest.java b/src/test/java/zone/moddev/mc/poweradvantage/resources/NativePlatformContractTest.java new file mode 100644 index 0000000..1b5cd72 --- /dev/null +++ b/src/test/java/zone/moddev/mc/poweradvantage/resources/NativePlatformContractTest.java @@ -0,0 +1,71 @@ +package zone.moddev.mc.poweradvantage.resources; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import org.junit.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; +import static org.junit.Assert.*; + +public class NativePlatformContractTest { + @Test public void onlyPreDatafixMigrationStillUsesACoremod() throws Exception { + String json = new String(Files.readAllBytes(Paths.get("src/main/resources/META-INF/coremods.json")), StandardCharsets.UTF_8); + JsonObject coremods = new JsonParser().parse(json).getAsJsonObject(); + assertEquals(1, coremods.size()); + assertEquals("coremods/poweradvantage_114_migration.js", coremods.get("poweradvantage_114_migration").getAsString()); + for (String removed : new String[] {"oil_renderer", "material_tags", "parts_loot"}) + assertFalse(Files.exists(Paths.get("src/main/resources/coremods/poweradvantage_" + removed + ".js"))); + } + + @Test public void pinnedForgeFiresTagsUpdatedAfterInstallingAllFourCollections() throws Exception { + ClassNode node = read("net/minecraft/tags/NetworkTagManager"); + int events = 0; + for (MethodNode method : node.methods) { + int installed = 0; + for (AbstractInsnNode instruction : method.instructions.toArray()) { + if (instruction instanceof MethodInsnNode) { + MethodInsnNode call = (MethodInsnNode) instruction; + if (call.owner.startsWith("net/minecraft/tags/") && call.name.equals("setCollection")) installed++; + } + if (instruction instanceof TypeInsnNode + && ((TypeInsnNode) instruction).desc.equals("net/minecraftforge/event/TagsUpdatedEvent")) { + assertEquals(4, installed); + events++; + } + } + } + assertEquals(1, events); + } + + @Test public void nativeLootLoaderCallsForgeJsonHookAndNotTheRemovedAdvantageBridge() throws Exception { + ClassNode node = read("net/minecraft/world/storage/loot/LootTableManager"); + int calls = 0; + for (MethodNode method : node.methods) for (AbstractInsnNode instruction : method.instructions.toArray()) { + if (!(instruction instanceof MethodInsnNode)) continue; + MethodInsnNode call = (MethodInsnNode) instruction; + assertNotEquals("zone/moddev/mc/poweradvantage/recipe/PartsLoot", call.owner); + if (call.owner.equals("net/minecraftforge/common/ForgeHooks") && call.name.equals("loadLootTable")) { + assertTrue(call.desc.contains("Lcom/google/gson/JsonObject;Z")); + calls++; + } + } + assertEquals(1, calls); + } + + private static ClassNode read(String name) throws Exception { + ClassNode node = new ClassNode(); + try (InputStream input = NativePlatformContractTest.class.getResourceAsStream("/" + name + ".class")) { + assertNotNull(input); + new ClassReader(input).accept(node, 0); + } + return node; + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/OilRendererTransformerTest.java b/src/test/java/zone/moddev/mc/poweradvantage/resources/OilRendererTransformerTest.java deleted file mode 100644 index 9734df9..0000000 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/OilRendererTransformerTest.java +++ /dev/null @@ -1,113 +0,0 @@ -package zone.moddev.mc.poweradvantage.resources; - -import org.junit.Test; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.VarInsnNode; - -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; -import java.io.InputStream; -import java.io.Reader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; - -import static org.junit.Assert.*; - -/** Exercises the shipped JavaScript against the pinned native renderer bytecode. */ -public class OilRendererTransformerTest { - private static final String RENDER = "(Lnet/minecraft/world/IWorldReader;Lnet/minecraft/util/math/BlockPos;" - + "Lnet/minecraft/client/renderer/BufferBuilder;Lnet/minecraft/fluid/IFluidState;)Z"; - private static final String OWNER = "zone/moddev/mc/poweradvantage/client/ClientOilRenderer"; - private static final String PEER = "test/SiblingFluidRenderer"; - - @Test public void patchesMappedRenderer() throws Exception { - ClassNode node = renderer(); - transform(node); - assertHooks(node, OWNER); - } - - @Test public void patchesProductionNamedRenderer() throws Exception { - ClassNode node = renderer(); - render(node).name = "func_209557_a"; - transform(node); - assertHooks(node, OWNER); - } - - @Test public void preservesEarlierSiblingHooks() throws Exception { - ClassNode node = renderer(); - addSiblingHooks(node); - transform(node); - assertHooks(node, OWNER); - assertHooks(node, PEER); - } - - @Test public void preservesLaterSiblingHooks() throws Exception { - ClassNode node = renderer(); - transform(node); - addSiblingHooks(node); - assertHooks(node, OWNER); - assertHooks(node, PEER); - } - - private static ClassNode renderer() throws Exception { - ClassNode node = new ClassNode(); - try (InputStream input = OilRendererTransformerTest.class.getResourceAsStream( - "/net/minecraft/client/renderer/BlockFluidRenderer.class")) { - assertNotNull("Pinned Forge renderer is absent", input); - new ClassReader(input).accept(node, 0); - } - return node; - } - - private static MethodNode render(ClassNode node) { - for (MethodNode method : node.methods) if (RENDER.equals(method.desc)) return method; - throw new AssertionError("Pinned renderer signature changed"); - } - - private static void transform(ClassNode node) throws Exception { - ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); - assertNotNull("Run transformer tests with the Java 8 toolchain", engine); - try (Reader script = Files.newBufferedReader(Paths.get("src/main/resources/coremods/poweradvantage_oil_renderer.js"), - StandardCharsets.UTF_8)) { - engine.eval(script); - } - engine.put("renderer", node); - assertSame(node, engine.eval("initializeCoreMod().poweradvantage_oil_renderer.transformer(renderer)")); - } - - private static void assertHooks(ClassNode node, String owner) { - int count = 0; - for (AbstractInsnNode instruction : render(node).instructions.toArray()) { - if (instruction instanceof MethodInsnNode && owner.equals(((MethodInsnNode) instruction).owner)) count++; - } - assertEquals("Missing or duplicated hooks for " + owner, 3, count); - } - - private static void addSiblingHooks(ClassNode node) { - // Base Metals and Mineralogy hook these same first local stores. Keep both chains intact. - boolean[] patched = new boolean[3]; - for (AbstractInsnNode instruction : render(node).instructions.toArray()) { - if (!(instruction instanceof VarInsnNode)) continue; - int slot = ((VarInsnNode) instruction).var; - if (slot < 5 || slot > 7 || patched[slot - 5]) continue; - boolean reference = slot == 6; - if (instruction.getOpcode() != (reference ? Opcodes.ASTORE : Opcodes.ISTORE)) continue; - String desc = slot == 5 ? "Z" : reference ? "[Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;" : "I"; - InsnList hook = new InsnList(); - hook.add(new VarInsnNode(Opcodes.ALOAD, 4)); - hook.add(new VarInsnNode(reference ? Opcodes.ALOAD : Opcodes.ILOAD, slot)); - hook.add(new MethodInsnNode(Opcodes.INVOKESTATIC, PEER, "override", - "(Lnet/minecraft/fluid/IFluidState;" + desc + ")" + desc, false)); - hook.add(new VarInsnNode(reference ? Opcodes.ASTORE : Opcodes.ISTORE, slot)); - render(node).instructions.insert(instruction, hook); - patched[slot - 5] = true; - } - } -} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/PartsLootTransformerTest.java b/src/test/java/zone/moddev/mc/poweradvantage/resources/PartsLootTransformerTest.java deleted file mode 100644 index 30fbade..0000000 --- a/src/test/java/zone/moddev/mc/poweradvantage/resources/PartsLootTransformerTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package zone.moddev.mc.poweradvantage.resources; - -import java.io.InputStream; -import java.io.Reader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; -import org.junit.Test; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import static org.junit.Assert.*; - -public class PartsLootTransformerTest { - @Test public void redirectsOnlyThePinnedForgeLoaderWithAnIdenticalStackContract() throws Exception { - ClassNode node = new ClassNode(); - try (InputStream input = getClass().getResourceAsStream("/net/minecraft/world/storage/loot/LootTableManager.class")) { - assertNotNull(input); - new ClassReader(input).accept(node, 0); - } - ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); - try (Reader script = Files.newBufferedReader(Paths.get("src/main/resources/coremods/poweradvantage_parts_loot.js"), - StandardCharsets.UTF_8)) { engine.eval(script); } - engine.put("manager", node); - engine.eval("initializeCoreMod().poweradvantage_parts_loot.transformer(manager)"); - int calls = 0; - for (MethodNode method : node.methods) for (AbstractInsnNode instruction : method.instructions.toArray()) { - if (!(instruction instanceof MethodInsnNode)) continue; - MethodInsnNode call = (MethodInsnNode) instruction; - if (call.owner.equals("zone/moddev/mc/poweradvantage/recipe/PartsLoot")) { - calls++; - assertEquals(Opcodes.INVOKESTATIC, call.getOpcode()); - assertEquals("loadLootTable", call.name); - assertEquals("(Lcom/google/gson/Gson;Lnet/minecraft/util/ResourceLocation;Ljava/lang/String;ZLnet/minecraft/world/storage/loot/LootTableManager;)Lnet/minecraft/world/storage/loot/LootTable;", call.desc); - } - assertFalse(call.owner.equals("net/minecraftforge/common/ForgeHooks") && call.name.equals("loadLootTable")); - } - assertEquals(1, calls); - } -} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/PersistenceRegistrationThreadTest.java b/src/test/java/zone/moddev/mc/poweradvantage/resources/PersistenceRegistrationThreadTest.java new file mode 100644 index 0000000..76b2bc6 --- /dev/null +++ b/src/test/java/zone/moddev/mc/poweradvantage/resources/PersistenceRegistrationThreadTest.java @@ -0,0 +1,46 @@ +package zone.moddev.mc.poweradvantage.resources; + +import java.io.InputStream; +import org.junit.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Handle; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import static org.junit.Assert.*; + +public class PersistenceRegistrationThreadTest { + @Test public void sharedForgeHookListIsOnlyUpdatedThroughQueuedSetup() throws Exception { + ClassNode node = new ClassNode(); + try (InputStream input = getClass().getResourceAsStream("/zone/moddev/mc/poweradvantage/PowerAdvantage.class")) { + assertNotNull(input); + new ClassReader(input).accept(node, 0); + } + String hook = "zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook"; + int queuedRegistrations = 0; + for (MethodNode method : node.methods) { + boolean hookRunnable = false; + for (AbstractInsnNode instruction : method.instructions.toArray()) { + if (instruction instanceof MethodInsnNode) { + MethodInsnNode call = (MethodInsnNode) instruction; + assertFalse("Do not mutate Forge's ArrayList during parallel mod construction", + call.owner.equals(hook) && call.name.equals("register")); + if (call.owner.equals("net/minecraftforge/fml/DeferredWorkQueue") && call.name.equals("runLater") && hookRunnable) { + assertEquals("Queue the hook during common setup", "commonSetup", method.name); + queuedRegistrations++; + hookRunnable = false; + } + } + if (instruction instanceof InvokeDynamicInsnNode) { + for (Object argument : ((InvokeDynamicInsnNode) instruction).bsmArgs) { + if (argument instanceof Handle && ((Handle) argument).getOwner().equals(hook) + && ((Handle) argument).getName().equals("register")) hookRunnable = true; + } + } + } + } + assertEquals("Register the shared persistence hook once, after parallel lifecycle work", 1, queuedRegistrations); + } +} diff --git a/src/test/java/zone/moddev/mc/poweradvantage/util/ForgeFluidAdapterTest.java b/src/test/java/zone/moddev/mc/poweradvantage/util/ForgeFluidAdapterTest.java new file mode 100644 index 0000000..e8e2078 --- /dev/null +++ b/src/test/java/zone/moddev/mc/poweradvantage/util/ForgeFluidAdapterTest.java @@ -0,0 +1,98 @@ +package zone.moddev.mc.poweradvantage.util; + +import cyano.poweradvantage.api.fluid.FluidStack; +import cyano.poweradvantage.api.fluid.FluidTank; +import cyano.poweradvantage.api.fluid.FluidTankInfo; +import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; +import net.minecraft.fluid.Fluid; +import net.minecraft.fluid.WaterFluid; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.util.Direction; +import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction; +import org.junit.Test; +import org.junit.BeforeClass; +import static org.junit.Assert.*; + +public class ForgeFluidAdapterTest { + private static Fluid WATER; + + @BeforeClass public static void bootstrapNativeRegistries() { + net.minecraft.util.registry.Bootstrap.register(); + WATER = net.minecraft.fluid.Fluids.WATER; + } + + @Test public void simulatesWithoutChangingTankOrInputThenExecutesExactAmount() { + TankHandler tank = new TankHandler(); + int[] changes = {0}; + ForgeFluidAdapter adapter = new ForgeFluidAdapter(tank, Direction.NORTH, () -> changes[0]++); + net.minecraftforge.fluids.FluidStack offered = new net.minecraftforge.fluids.FluidStack(WATER, 1500); + CompoundNBT tag = new CompoundNBT(); + tag.putString("Owner", "preserve-me"); + offered.setTag(tag); + assertEquals(1000, adapter.fill(offered, FluidAction.SIMULATE)); + assertEquals(0, tank.tank.getFluidAmount()); + assertEquals(0, changes[0]); + assertEquals(1000, adapter.fill(offered, FluidAction.EXECUTE)); + assertEquals(1000, tank.tank.getFluidAmount()); + assertEquals(1500, offered.getAmount()); + assertEquals(tag, offered.getTag()); + assertEquals(1, changes[0]); + tank.tank.getFluid().tag.putString("Owner", "changed-in-tank"); + assertEquals("preserve-me", offered.getTag().getString("Owner")); + } + + @Test public void keepsSidedRestrictionsAndRejectsEmptyOrInvalidRequests() { + TankHandler tank = new TankHandler(); + ForgeFluidAdapter adapter = new ForgeFluidAdapter(tank, Direction.SOUTH, () -> fail("Changed blocked tank")); + assertFalse(adapter.isFluidValid(0, new net.minecraftforge.fluids.FluidStack(WATER, 10))); + assertEquals(0, adapter.fill(new net.minecraftforge.fluids.FluidStack(WATER, 10), FluidAction.EXECUTE)); + assertEquals(0, adapter.fill(net.minecraftforge.fluids.FluidStack.EMPTY, FluidAction.EXECUTE)); + assertTrue(adapter.drain(-1, FluidAction.EXECUTE).isEmpty()); + assertTrue(adapter.drain(net.minecraftforge.fluids.FluidStack.EMPTY, FluidAction.EXECUTE).isEmpty()); + assertEquals(0, adapter.getTankCapacity(-1)); + assertTrue(adapter.getFluidInTank(8).isEmpty()); + } + + @Test public void drainsWithSimulationLimitsAndIndependentSnapshots() { + TankHandler tank = new TankHandler(); + tank.tank.fill(new FluidStack(WATER, 900), true); + int[] changes = {0}; + ForgeFluidAdapter adapter = new ForgeFluidAdapter(tank, Direction.NORTH, () -> changes[0]++); + net.minecraftforge.fluids.FluidStack view = adapter.getFluidInTank(0); + view.setAmount(1); + assertEquals(900, tank.tank.getFluidAmount()); + assertEquals(300, adapter.drain(300, FluidAction.SIMULATE).getAmount()); + assertEquals(900, tank.tank.getFluidAmount()); + assertEquals(0, changes[0]); + net.minecraftforge.fluids.FluidStack request = new net.minecraftforge.fluids.FluidStack(WATER, 250); + assertEquals(250, adapter.drain(request, FluidAction.EXECUTE).getAmount()); + assertEquals(250, request.getAmount()); + assertEquals(650, tank.tank.getFluidAmount()); + assertEquals(650, adapter.drain(1000, FluidAction.EXECUTE).getAmount()); + assertEquals(0, tank.tank.getFluidAmount()); + assertEquals(2, changes[0]); + } + + @Test public void copiesBothDirectionsIncludingNbt() { + FluidStack source = new FluidStack(WATER, 700); + source.tag = new CompoundNBT(); + source.tag.putInt("batch", 7); + net.minecraftforge.fluids.FluidStack nativeStack = ForgeFluidAdapter.toForge(source); + FluidStack copy = ForgeFluidAdapter.fromForge(nativeStack); + copy.tag.putInt("batch", 8); + assertEquals(7, source.tag.getInt("batch")); + assertEquals(7, nativeStack.getTag().getInt("batch")); + assertNull(ForgeFluidAdapter.fromForge(net.minecraftforge.fluids.FluidStack.EMPTY)); + assertTrue(ForgeFluidAdapter.toForge(null).isEmpty()); + } + + private static class TankHandler implements IAdvantageFluidHandler { + final FluidTank tank = new FluidTank(1000); + @Override public FluidTankInfo[] getTankInfo(Direction face) { return new FluidTankInfo[] {tank.getInfo()}; } + @Override public boolean canFill(Direction face, Fluid fluid) { return face == Direction.NORTH; } + @Override public boolean canDrain(Direction face, Fluid fluid) { return face == Direction.NORTH; } + @Override public int fill(Direction face, FluidStack stack, boolean execute) { return tank.fill(stack, execute); } + @Override public FluidStack drain(Direction face, FluidStack stack, boolean execute) { return tank.drain(stack, execute); } + @Override public FluidStack drain(Direction face, int amount, boolean execute) { return tank.drain(amount, execute); } + } +} diff --git a/test-rig/NATIVE114.md b/test-rig/NATIVE114.md new file mode 100644 index 0000000..206b853 --- /dev/null +++ b/test-rig/NATIVE114.md @@ -0,0 +1,170 @@ +# Native 1.14.4 Harness + +This test-only destination targets Forge 28.2.26, snapshot 20190719-1.14.3, +MCP 20190829.143755, FG 7.0.34, Gradle 9.6.1 and Java 8 bytecode. Run Gradle +with Java 17. When projects share a Gradle cache, run their builds one at a time, +including development server tasks. + +```powershell +$env:JAVA_HOME='' +$env:GRADLE_USER_HOME='' +$env:ADVANTAGE_WORKS_JAVA8_HOME='' +$env:ADVANTAGE_WORKS_GRADLE_JAVA_HOME=$env:JAVA_HOME +./gradlew.bat -p test-rig test assemble verifyHarnessIsolation +./gradlew.bat -p test-rig verifyWorksNative114Inputs +``` + +Run those commands from the PA114 Power checkout while no other build is using +the shared cache. Input preflight checks identities and records actual hashes without +creating a runtime or opening a world. `-PworksProfile=dev-core` selects another +native profile. Workspace candidates remain explicitly UNQUALIFIED; a local +hash is provenance, not qualification. Rebuild/package child release jars before +packaged preflight. `powerAdvantageDeobfJar` and `powerAdvantageReleaseJar` can +select explicit native candidate inputs for compilation and isolation checks. + +## Native APIs and Invariants + +All main runtime sources compile against genuine mapped 1.14.4 classes and the +actual Power deobf jar. There are no dependency shims or vanilla shadow classes. +Native state/NBT/player/packet/recipe/loot APIs replace the 1.13 names. Chunk +release removes forced tickets; only real unload/load events count as lifecycle +evidence. `ServerChunkProvider.func_225313_a` is the mapped non-loading lookup +used for observations. No snapshot may force an unloaded witness back in. + +Actor holds use native `canUpdate` and restore the original flag and tick count; +actors are not removed from native tracking indexes. These paths still require +real tick and restart witnesses, not merely matching serialized NBT. See the +[broader qualification record](../docs/1.14/BROADER-QUALIFICATION.md) for their +current live results, including passenger/leash restoration and respawn. + +The native works descriptor changes only the target header. It selects the +immutable 1.13.2 station specification, preserving all 21 station IDs, bounds, +placements, controls, quantities, assertions, bug markers and manual observations. +The definition regression compares every station and district. Existing command +names and PASS/FAIL/EXPECTED_FAIL/UNEXPECTED_PASS/SKIP/MANUAL_REQUIRED meanings +remain unchanged. All original tests remain present. + +## Destination Profiles + +All standard Gradle runner task names now select `profiles/native114/`, never +the retained top-level historical profiles. Profiles create new runtimes under +`test-rig/build/works-native114`, bind to loopback, and retain the world-name and +allow-file interlocks. The default native flat generator uses vanilla defaults, +not a legacy semicolon generator string. + +| Profiles | Port State | Runtime Qualification | +| --- | --- | --- | +| dev-core, dev-core-tech-progression, dev-core-apocalyptic | Native candidates | Pending | +| packaged-current | Native candidates | Bounded connected/lifecycle, movement-aware broad bays and final endurance pass; human observations remain open | +| packaged-decoupled | Native candidates | Broad bays pass without Base Metals; required OreSpawn remains present | +| packaged-optional-power | Native candidates | Native energy/fluid capability contracts pass; real third-party implementations remain manual | +| worldgen-advantage, worldgen-legacy-import | Native candidates; retained assertions | Sampling, import and profile-restart assertions passed | +| packaged-mineralogy, worldgen-mineralogy | Native candidates | Distillation, six oil-transfer lanes and generation assertions passed; wider acceptance pending | + +The original top-level JSON files remain source manifests. Each native profile's +`legacySourceProfile` resolves explicitly into `PA113/PowerAdvantage/test-rig`. +Historical qualification and intermediate failures belong to that source tree; +they are not evidence for the new destination. + +The Mineralogy profiles pin the genuine `6.0.1.114041` jar from its clean +`5bcec041775bed9de562075d177e33c507b2e2e1` checkout. Its SHA-256 is +`75CE58BCDCB845407DE55F476FDB06211E2F8DBE6FCB0AC0F7FBDB5B7841F7BA`. +An explicitly unported profile still fails before runtime creation; the profile +regression retains a blocked-input witness even though all ten current profiles +now have native candidates. + +## Frozen Migration Destination + +`migration/native114.py` is a separate destination adapter, requiring Python +3.11+ and the existing `migration/requirements.txt` dependencies. It does not +call the copied legacy convenience-profile discovery in `matrix.py`. + +First pin actual packaged candidates and an installed native114 server supplied +by the operator. This command opens no world: + +```text +python test-rig/migration/native114.py pin-target --server-seed --output test-rig/build/native114-inputs.json +``` + +Then run conversion only against an explicitly selected disposable fixture: + +```text +python test-rig/migration/native114.py run --source --source-manifest --source-key --source-hashes --source-snapshot migration-source.json --target-manifest test-rig/build/native114-inputs.json --java8 --output test-rig/build/native114-conversion-new +``` + +For a retained 1.13 destination, select its actual existing snapshot filename, +such as `migration-restarted.json`, and its original pinned source key. Relative +source-manifest paths resolve at the original archived manifest, not at PA114. +Source manifests and fixture saves are never rewritten. The runner checks the +approved fingerprint before copying and the complete source runtime afterward. +Output paths must be new, confined to `test-rig/build`, and disjoint from inputs. +No source process is launched. The destination can only snapshot, save, stop and +restart; it cannot rebuild, refill or reset imported equipment. Existing state +legacy comparison rules remain the default, including failure on loss or duplication. +The native destination alone supplies an explicit platform adapter: vanilla 1.14 +omits an empty chest `Lock`, so an omitted key and an empty string both mean +unlocked. Nonempty locks remain strict in both directions; custom machine NBT +is not covered by this exception. Original snapshots are never rewritten. +Runtime errors also fail a conversion even if Minecraft produces a snapshot. + +## Remaining Campaign Gaps + +- Thirteen frozen conversion routes passed with a full destination-process + restart. Five archived connected sources also passed preservation and + 512+512 ticks of operation, with exact stock reconciliation and no rebuilding. + Their source fingerprints remained unchanged. These are bounded checks, not + actor, lifecycle or endurance acceptance. +- The original copied campaign defaults still target 1.13. Use the separate + native adapters below for the qualified 1.14 destination, and the PA113 + checkout with explicit manifests for historical source construction. +- The new destination runner measures frozen state conversion and one full + process restart only. It does not qualify machine operation, links, inventory + transactions, live chunk unload orders, actors or death/respawn. +- The native respawn adapter explicitly binds + `ServerPlayNetHandler.PERFORM_RESPAWN`, with guarded target-name translation + and 72 passing server-side cases. Real client death-screen interaction and + player travel remain manual. +- The native `client-resources/` probe and launcher have passed the combined + model, texture, language, creative-entry, fluid-rendering and tank-gauge checks. + Native recipe packets also pass all three recipe-book modes and the unchanged + button-rendering threshold. Manual in-world graphical acceptance remains a + separate gate. A main-menu probe does not qualify machine GUI interaction. +- Mineralogy distillation and boundary-aware ore/oil generation assertions passed + with the pinned native jar. That does not qualify every boiler, fuel, bucket + or third-party capability interaction. No artifact is substituted to fill a gap. + +Never modify or launch original fixture saves. Preserve failed evidence and +keep source failures, conversion results, restarts and native operation separate. + +## Additional Native Runners + +These wrappers select genuine native inputs explicitly. They do not retarget the +historical campaign defaults or change their stock and state oracles. + +| Runner | Scope | +| --- | --- | +| `migration/native114_connected.py` | Prebuilt source assemblies; initial preservation, 512 operating ticks, full restart, 512 more ticks; no rebuilding or refill | +| `migration/native114_recipes.py` | Packaged ingredient discoveries, native recipe packets, manufacturing progression, recipe modes, reloads and restarts | +| `migration/native114_oil.py` | Native oil-bucket placement, drain/pipe/container transfer, exact refill quantities and two full process restarts; six lanes when Mineralogy is pinned | +| `migration/native114_lifecycle.py` | Unchanged movement, real unload/load, mining and saved-work oracles; passed fresh and copied chained worlds | +| `migration/native114_extended.py`, `native114_partial.py` | Extended drill and partial-network observations; current result remains in the broader qualification record | +| `migration/native114_actors.py`, `native114_dimensions.py`, `native114_player_layout.py`, `native114_respawn.py` | Native destination adapters for archived player/entity/dimension fixtures and real server respawn handling | +| `migration/native114_transformer_order.py` | Live Base/Power migration-hook ordering checks | +| `migration/native114_broad.py` | Current and decoupled walkable bays with initial placement, tagged drill movement and restart assertions | +| `migration/native114_endurance.py` | Two measured thirty-minute environments, native feeding, exact batch conservation and full restart | +| `migration/native114_interop.py` | Native capability simulation and limits, converter conservation, portable tank loot, locked automation and restart | +| `migration/native114_refresh.py` | Refresh all thirteen frozen and five connected source routes against another explicitly pinned candidate | + +Each wrapper has `--help`, requires a pinned `--target-manifest`, writes only new +outputs under `test-rig/build`, and records failures rather than replacing them +with successful reruns. Freeze candidate jars into hash-addressed artifact copies +before another build changes the development outputs; do not rewrite an earlier +manifest to point at replacement bytes. + +The native lifecycle adapter accounts for asynchronous chunk transitions. After +restart it restores only fixture tickets, requires genuine Load events and loaded +witnesses, then selects the first measured tick advance. A queued Unload callback +must also arrive before the unchanged unload oracle runs; the original 45-second +budget is not extended. Immediate and pending observations are retained under +distinct names. Selection never reads progress, inventories or a desired verdict. +The inherited operations and all gameplay/state oracles remain unchanged. diff --git a/test-rig/README.md b/test-rig/README.md index 0ea64d1..a3850c5 100644 --- a/test-rig/README.md +++ b/test-rig/README.md @@ -1,4 +1,23 @@ -# Advantage Works 1.13.2 Test Rig +# Advantage Works 1.14.4 Native Test Rig + +The native target is Minecraft 1.14.4 / Forge 28.2.26, snapshot mappings +20190719-1.14.3, MCP 20190829.143755, Java 8 bytecode and harness +`1.1.0.114041`. Read [the native114 runner and qualification limits](NATIVE114.md) +before using this checkout. Build success is not runtime or migration acceptance. + +Native destination profiles live only in `profiles/native114/`. The original +profiles, station specification and legacy migration source adapters remain +unchanged. New profiles explicitly reference the historical PA113 source root; +the native runner refuses to launch an old profile as a native114 destination. +Both Mineralogy profiles are fail-closed pending a verified native candidate. + +## Retained 1.13.2 Protocol Reference + +The remaining text describes the retained **1.13.2 source qualification**, not +1.14.4 results. Its paths and old artifact versions are historical. Use the +original source checkout at `D:/PowerAdvantage/PA113/PowerAdvantage` for that +qualification and its documentation. Do not execute copied Python defaults +from PA114 or alias `target114` to `target113`. > **Commissioning notice:** This harness builds disposable works. It is not a gameplay mod, registers no content, and must never be shipped inside a PowerAdvantage jar. diff --git a/test-rig/build.gradle b/test-rig/build.gradle index a83dd96..962f3c7 100644 --- a/test-rig/build.gradle +++ b/test-rig/build.gradle @@ -54,11 +54,11 @@ repositories { dependencies { implementation minecraft.dependency( "net.minecraftforge:forge:${project.minecraft_version}-${project.forge_version}") - compileOnly 'org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209' runnerImplementation 'com.google.code.gson:gson:2.8.0' + testImplementation sourceSets.runner.output testImplementation 'junit:junit:4.13.2' compileOnly files(project.findProperty('powerAdvantageDeobfJar') - ?: '../build/libs-dev/PowerAdvantage-2.4.4.113021-deobf.jar') + ?: '../build/libs-dev/PowerAdvantage-2.4.5.114041-deobf.jar') } tasks.named('processResources', ProcessResources) { @@ -114,7 +114,7 @@ def registerWorksRunner = { String taskName, String profileName, boolean automat classpath = sourceSets.runner.runtimeClasspath mainClass = 'zone.moddev.mc.advantageworks.runner.WorksRunner' if (!automated) standardInput = System.in - args '--profile', file("profiles/${profileName}.json").absolutePath + args '--profile', file("profiles/native114/${profileName}.json").absolutePath if (automated) args '--automated' if (project.hasProperty('worksStations')) { args '--stations', project.property('worksStations').toString() @@ -129,7 +129,7 @@ def registerWorksRunner = { String taskName, String profileName, boolean automat } registerWorksRunner('runWorksDev', 'dev-core', false, - 'Builds and launches a disposable interactive 1.13.2 Advantage Works development server.') + 'Builds and launches a disposable interactive 1.14.4 Advantage Works development server.') registerWorksRunner('verifyWorksDev', 'dev-core', true, 'Generates, checks, restarts, and rechecks the disposable development works.') registerWorksRunner('verifyWorksTechProgression', 'dev-core-tech-progression', true, @@ -153,11 +153,22 @@ registerWorksRunner('verifyWorksWorldgenLegacyImport', 'worldgen-legacy-import', registerWorksRunner('runWorksPackagedCurrent', 'packaged-current', false, 'Launches the complete packaged works on localhost for manual inspection.') +tasks.register('verifyWorksNative114Inputs', JavaExec) { + group = 'verification' + description = 'Checks native114 candidate identities and hashes without creating or opening a world.' + dependsOn tasks.named('runnerClasses'), tasks.named('jar'), releaseJar + classpath = sourceSets.runner.runtimeClasspath + mainClass = 'zone.moddev.mc.advantageworks.runner.WorksRunner' + args '--profile', file("profiles/native114/${project.findProperty('worksProfile') ?: 'packaged-current'}.json").absolutePath, + '--validate-only' +} + tasks.register('verifyHarnessIsolation') { group = 'verification' description = 'Confirms the PowerAdvantage release contains no test-harness content.' doLast { - File powerJar = file('../build/libs/PowerAdvantage-2.4.4.113021.jar') + File powerJar = file(project.findProperty('powerAdvantageReleaseJar') + ?: '../build/libs/PowerAdvantage-2.4.5.114041.jar') if (!powerJar.isFile()) { throw new GradleException("Build PowerAdvantage first: ${powerJar}") } diff --git a/test-rig/client-resources/README.md b/test-rig/client-resources/README.md index b76729b..ec34c09 100644 --- a/test-rig/client-resources/README.md +++ b/test-rig/client-resources/README.md @@ -1,18 +1,43 @@ # Native Client Resource Check -This build-only Forge 1.13.2 probe checks PowerAdvantage, or all three Advantage mods in the combined profile, after Minecraft bakes its models and stitches its textures. It registers no gameplay content, opens no world, and shuts down its own disposable client when finished. It is not included in release jars or ordinary Eclipse launches. +This build-only Minecraft 1.14.4 / Forge 28.2.26 probe checks PowerAdvantage, or all three Advantage mods in the combined profile, after Minecraft bakes its models and stitches its textures. It compiles against the actual mapped snapshot `20190719-1.14.3` jar. It registers no gameplay content, opens no world, and shuts down its own disposable client when finished. It is not included in release jars or ordinary Eclipse launches. -For native recipe-book grouping, first complete the packaged `recipe_unlocks.py` -run, then pass its output directory: +For native recipe-book grouping, supply a **new native 1.14.4 server export** +containing `recipes-{normal,tech,apocalyptic}-sweep.bin`, their matching successful +JSON reports with `bookRecipes`, and `AdvantageWorksMigration/level.dat`. +The copied convenience `migration/recipe_unlocks.py` still targets 1.13.2; do +not use its outputs or historical qualification as 1.14.4 evidence. Native +fixture generation is owned by the server harness, outside this directory. +The separate native campaign retains the five unlock/persistence boots and all +18 progression checks. It reads recipes and advancements from the packaged +candidate jars, validates target hashes, creates new loopback runtimes only, +and fingerprints the installed server seed before and after. It needs an +existing Python 3.11+ environment with `migration/requirements.txt`, plus Java 8; +it does not invoke Gradle: ```powershell -./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -RecipeBookFixtures 'run-recipe-unlocks/new-current' + -B test-rig/migration/native114_recipes.py --target-manifest test-rig/build/native114-inputs-20261023-b.json --java8 '/bin/java.exe' --port 25577 --output test-rig/build/native114-recipes-new +``` + +`--campaign unlock` or `--campaign progression` selects a single campaign. +Native vanilla logs saved recipe IDs removed by a recipe-mode change at ERROR +level. The unlock audit checks the exact set against the difference between +the preceding and current actual recipe packets, retains all log events, and +fails on any unrelated error or a mismatched removal set. It never erases a +saved book to avoid that persistence check. + +After native exports are available, pass the campaign's `unlock` subdirectory: + +```powershell +./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -RecipeBookFixtures '' ``` The server exports its actual recipe packets and matching metadata. The client imports the generated world's Forge registry snapshot before decoding those packets, reproducing the item-ID synchronization normally supplied by Forge's -connection handshake. It then rebuilds native `RecipeBookClient`/`RecipeList` +connection handshake. The saved header must identify Minecraft 1.14.4 and +Forge 28.2.26; a legacy fixture is rejected. It then rebuilds native +`ClientRecipeBook`/`RecipeList` cells for Normal, Tech and Apocalyptic modes: each cell must contain exactly one output identity, all loaded variants must remain accessible, and same-output alternatives must not split into extra cells. A framebuffer image, @@ -22,23 +47,35 @@ The probe reads only the disposable fixture's saved registry data; it never opens that world or a user's world in Minecraft. This is a native cell-model and button-rendering check, not manual navigation or notification acceptance. -Run Gradle with Java 17, then supply a Java 8 runtime to the launcher: +Obtain the shared `D:/GradleCaches/minecraft-mods-shared` slot before any Gradle +command. Use the repository's Gradle 9.6.1 / ForgeGradle 7 setup with Java 17 +for Gradle and the configured Java 8 compilation toolchain. Supply Java 8 to +the disposable launcher. Do not launch the actual client until authorized: ```powershell ./gradlew.bat check clientResourceProbeJar verifyEclipseProductionClasspath +./gradlew.bat prepareCombinedEclipseLaunch +./test-rig/client-resources/test-launcher.ps1 +./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -PreflightOnly +# Only after authorization for the graphical run: ./test-rig/client-resources/run.ps1 -JavaHome '' ``` -Each run gets a new directory under `build/client-resource-runs/`. The runner snapshots production classes/resources, uses the audited launch dependencies, and loads only the probe as an additional mod. It retains logs and `client-resources.json`; missing reports, failed assertions, and model/texture errors fail the run. Existing saves, configuration, and prior reports are untouched. +Each run gets a new directory under `build/client-resource-runs/`. The runner snapshots production classes/resources, uses the audited native launch dependencies, and loads only the probe as an additional mod. Helpers launch hidden, with separate stdout/stderr logs and a bounded timeout. It retains logs, images and `client-resources.json`; missing reports, failed assertions, and model/texture errors fail the run. A runner failure gets a separate `runner-failure.json`, without replacing partial native results. Existing saves, configuration, and prior reports are untouched. `-PreflightOnly` creates an input snapshot without starting Java or claiming a graphical pass. -The default `-OutputSource Gradle` merges Gradle's classes and processed resources. To test the actual Eclipse output instead, pass `-OutputSource Eclipse`; this copies `bin/main` verbatim and does not overlay Gradle resources. `client-input.json` records the selected output, launch and metadata hash. Missing/unexpanded metadata fails the preflight before Minecraft opens, and the native result records the versions Forge actually discovers. The [Eclipse metadata regression](../../docs/1.13/ECLIPSE-METADATA.md) explains why this is a separate acceptance gate. +The default `-OutputSource Gradle` merges Gradle's classes and processed resources. To test the actual Eclipse output instead, pass `-OutputSource Eclipse`; this copies `bin/main` verbatim and does not overlay Gradle resources. `production-files.json` hashes every selected class/resource. `client-input.json` records that manifest's hash, the Java binary, probe, launch, production metadata, every dependency archive, expansion candidate hashes and optional fixture hashes. The current Power deobf candidate is recorded separately, defaulting to `build/libs-dev/PowerAdvantage-2.4.5.114041-deobf.jar`; `-PowerCandidate` selects an explicit native candidate. That jar hash is provenance, not a claim that Eclipse/Gradle output equals the jar. Missing/unexpanded metadata or a legacy target fails preflight. The native result records actual discovered mod versions and the exact game/Forge target. Refresh candidate hashes after any main-owned rebuild before final evidence is taken. The [historical Eclipse metadata regression](../../docs/1.13/ECLIPSE-METADATA.md) explains the original failure, not native 1.14 acceptance. -For coordinated startup, first run `gradlew.bat prepareCombinedEclipseLaunch`, then add `-LaunchProfile runClientAllAdvantage.launch -RequireExtensions` to the probe command. The report's `advantage_mods` must list all three mods. With this flag, resource and item-name assertions cover Power, Steam and Electric, and model/blockstate errors from any of their namespaces fail the run. Without it, the original Power-only scope is unchanged. Pin every rebuilt expansion jar in the development and packaged rig profiles before regenerating the combined launch. +For coordinated startup, first run `gradlew.bat prepareCombinedEclipseLaunch`, then add `-LaunchProfile runClientAllAdvantage.launch -RequireExtensions` to the probe command. The report's `advantage_mods` must list all three mods. With this flag, resource and item-name assertions cover Power, Steam and Electric, and model/blockstate errors from any of their namespaces fail the run. Without it, the original Power-only scope is unchanged. Main must pin every rebuilt expansion jar in the native114 profiles before regenerating the combined launch. Recipe grouping requires that combined scope; without fixtures it explicitly reports `NOT_RUN_NO_NATIVE_FIXTURES`. Assertions cover every registered item and valid block state in the selected Advantage namespaces, nonempty item/machine geometry, missing-model and missing-texture sprites, English item names, the shared creative tab and its icon, and agreement between Power's modern block and legacy GUI translation keys. Native fluid block models are intentionally particle-only. This test does not certify machine operation, animated entity rendering, or third-party resource packs. -The six `ClientResourceContractTest` checks first failed against the pre-repair resources. They cover literal model paths, removed legacy blockstate selectors, model parents/textures (including vanilla assets), oil item models, locale aliases, and English block/bucket coverage. +The `ClientResourceContractTest` checks cover literal model paths, removed legacy blockstate selectors, model parents/textures (including vanilla assets), oil item models, locale aliases, and English block/bucket coverage. Their historical 1.13 red/green results do not qualify this destination. + +Tank visual regressions use Minecraft's actual neighbour-face culling beneath both tanks, native Forge fluid capability simulation/execution, full 10,000 mB and empty tank update packets, and a native registered `ContainerType` screen initialized without a player/world. The actual filtered-tank gauge and frame render into an OpenGL framebuffer through the production GUI. The pixel at `(88,39)` must remain blue with the original thresholds (`blue > red + 20`, `blue > green + 10`); `tank-water-gauge.png` is written before assertions. Historical hidden-floor/untinted `ADADAD` observations remain 1.13 evidence only. No test-world loading or fluid-network changes are required. -Tank visual regressions also use the native client. They test Minecraft's actual neighbour-face culling beneath both tanks, synchronize a full 10,000 mB metal tank and then an empty tank, and render the filtered tank's gauge and frame into an OpenGL framebuffer. The water pixel must be blue; `tank-water-gauge.png` retains the rendered indicator. These checks reproduced hidden floor faces and an untinted `ADADAD` water pixel before the rendering repairs. No test-world loading or fluid-network changes are required for this focused check. +Oil regressions verify both buckets through native `BucketItem.getFluid()` and Forge's fluid-item capability, then call the initialized native `BlockRendererDispatcher` / `FluidBlockRenderer` against a read-only synthetic `IEnviromentBlockReader`. Still and flowing crude/refined oil must emit vertices using their own atlas sprites and `FFFFFF` top-face tint. Vanilla water and lava remain controls. Forge 28 reads sprites/tint from `FluidAttributes`; no removed oil transformer or 1.13 hook is assumed. The probe temporarily supplies native vanilla water/lava tags at the main menu and restores them afterward; no production tags or saves are written. `fluid_rendering` retains case results. The historical water-sprite/`3F76E4` failure is not native 1.14 evidence. -Oil regressions verify both native buckets' fluid identities, then run Minecraft's actual `BlockFluidRenderer` against a read-only synthetic neighbourhood. Still and flowing crude/refined oil must emit vertices using their own atlas sprites, without a biome water tint. Vanilla water and lava are control cases. The probe installs vanilla water/lava tag memberships explicitly because no server has synchronized tags at the main menu. It changes no production tags or saves. Before the repair all four oil cases used water sprites and the blue `3F76E4` tint; `fluid_rendering` in the JSON report retains the results. Separate Java 8 transformer tests cover mapped/SRG-style method names and preservation of sibling hook chains in either order; these structural checks do not certify every third-party renderer or resource pack. +The native recipe image still requires exactly thirteen separate cells and more +than 1,000 colored framebuffer pixels. A blank image is retained before failure. +Compilation and input preflight are not graphical acceptance, recipe discovery +acceptance, manual GUI navigation or server-to-client menu-handshake acceptance. diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ClientResourceProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ClientResourceProbe.java index 09195a8..08d1b17 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ClientResourceProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ClientResourceProbe.java @@ -2,19 +2,19 @@ import com.google.gson.JsonObject; import net.minecraft.block.Block; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.model.BakedQuad; import net.minecraft.client.renderer.model.IBakedModel; import net.minecraft.client.resources.I18n; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.NonNullList; import net.minecraft.util.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.event.TickEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.registries.ForgeRegistries; import org.apache.logging.log4j.LogManager; @@ -39,10 +39,16 @@ public final class ClientResourceProbe { @SubscribeEvent public static void tick(TickEvent.ClientTickEvent event) { if (!Boolean.getBoolean("advantageworks.clientResources") || finished || event.phase != TickEvent.Phase.END) return; Minecraft minecraft = Minecraft.getInstance(); - if (++ticks < 5 || (minecraft.getModelManager().getMissingModel() == null && ticks < 1200)) return; + if (++ticks < 5 || ((minecraft.getLoadingGui() != null || minecraft.getModelManager().getMissingModel() == null) && ticks < 1200)) return; finished = true; JsonObject result = new JsonObject(); try { + result.addProperty("minecraft", net.minecraftforge.versions.mcp.MCPVersion.getMCVersion()); + result.addProperty("forge", net.minecraftforge.versions.forge.ForgeVersion.getVersion()); + require("1.14.4".equals(result.get("minecraft").getAsString()) + && "28.2.26".equals(result.get("forge").getAsString()), "Wrong native client target"); + require(minecraft.world == null && minecraft.player == null && !minecraft.isIntegratedServerRunning(), + "Resource probe must not open a world or connect to a server"); com.google.gson.JsonArray mods = new com.google.gson.JsonArray(); JsonObject versions = new JsonObject(); for (String id : new String[]{"poweradvantage", "steamadvantage", "electricadvantage"}) { @@ -59,6 +65,7 @@ public final class ClientResourceProbe { require(versions.has("poweradvantage") && zone.moddev.mc.poweradvantage.PowerAdvantage.VERSION .equals(versions.get("poweradvantage").getAsString()), "Power metadata does not match the compiled version"); require(minecraft.getModelManager().getMissingModel() != null, "Timed out before model baking"); + require(minecraft.getLoadingGui() == null, "Timed out before native resource loading completed"); int items = 0; int states = 0; IBakedModel missing = minecraft.getModelManager().getMissingModel(); @@ -75,9 +82,9 @@ public final class ClientResourceProbe { for (Block block : ForgeRegistries.BLOCKS) { ResourceLocation id = block.getRegistryName(); if (id == null || !included(id.getNamespace())) continue; - for (IBlockState state : block.getStateContainer().getValidStates()) { + for (BlockState state : block.getStateContainer().getValidStates()) { IBakedModel model = minecraft.getModelManager().getBlockModelShapes().getModel(state); - verifyModel(model, missing, state, state.toString(), !(block instanceof net.minecraft.block.BlockFlowingFluid)); + verifyModel(model, missing, state, state.toString(), !(block instanceof net.minecraft.block.FlowingFluidBlock)); states++; } } @@ -97,7 +104,7 @@ public final class ClientResourceProbe { result.addProperty("creative_entries", contents.size()); result.addProperty("screen", minecraft.currentScreen == null ? "none" : minecraft.currentScreen.getClass().getName()); LogManager.getLogger(MODID).info("ADVANTAGE_CLIENT_RESOURCES PASS items={} block_states={} creative_entries={}", items, states, contents.size()); - } catch (RuntimeException | AssertionError failure) { + } catch (RuntimeException | LinkageError | AssertionError failure) { result.addProperty("status", "FAIL"); result.addProperty("error", failure.toString()); LogManager.getLogger(MODID).error("ADVANTAGE_CLIENT_RESOURCES FAIL", failure); @@ -113,12 +120,12 @@ private static boolean included(String namespace) { && ("steamadvantage".equals(namespace) || "electricadvantage".equals(namespace))); } - private static void verifyModel(IBakedModel model, IBakedModel missing, IBlockState state, String id, boolean requireQuads) { + private static void verifyModel(IBakedModel model, IBakedModel missing, BlockState state, String id, boolean requireQuads) { require(model != missing, "Missing baked model: " + id); require(!"missingno".equals(model.getParticleTexture().getName().getPath()), "Missing particle texture: " + id); Random random = new Random(42L); List quads = new ArrayList<>(model.getQuads(state, null, random)); - for (EnumFacing side : EnumFacing.values()) quads.addAll(model.getQuads(state, side, random)); + for (Direction side : Direction.values()) quads.addAll(model.getQuads(state, side, random)); require(!requireQuads || !quads.isEmpty(), "Empty baked model: " + id); for (BakedQuad quad : quads) require(!"missingno".equals(quad.getSprite().getName().getPath()), "Missing face texture: " + id); } diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java index b980ba5..5cb199a 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java @@ -2,25 +2,29 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.BlockFluidRenderer; import net.minecraft.client.renderer.BufferBuilder; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.renderer.vertex.DefaultVertexFormats; import net.minecraft.fluid.Fluid; import net.minecraft.fluid.FlowingFluid; import net.minecraft.fluid.IFluidState; -import net.minecraft.item.ItemBucket; +import net.minecraft.item.BucketItem; +import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.BlockPos; -import net.minecraft.world.IWorldReader; +import net.minecraft.world.IEnviromentBlockReader; +import net.minecraft.world.LightType; +import net.minecraft.world.biome.Biome; +import net.minecraft.tileentity.TileEntity; import zone.moddev.mc.poweradvantage.init.Fluids; import zone.moddev.mc.poweradvantage.init.Items; -import java.lang.reflect.Field; -import java.lang.reflect.Proxy; import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; /** Exercises the native fluid renderer, not just oil's particle-only block model. */ final class OilRenderProbe { @@ -28,13 +32,17 @@ private OilRenderProbe() {} static void verify(Minecraft minecraft, JsonObject result) { // Before a world/server is opened, vanilla's fluid tags have not been synchronized. - net.minecraft.tags.TagCollection tags = new net.minecraft.tags.TagCollection<>(id -> true, - net.minecraft.util.registry.IRegistry.field_212619_h::func_212608_b, "tags/fluids", false, "fluid"); - tags.register(net.minecraft.tags.Tag.Builder.create().add(net.minecraft.init.Fluids.WATER, - net.minecraft.init.Fluids.FLOWING_WATER).build(new ResourceLocation("minecraft", "water"))); - tags.register(net.minecraft.tags.Tag.Builder.create().add(net.minecraft.init.Fluids.LAVA, - net.minecraft.init.Fluids.FLOWING_LAVA).build(new ResourceLocation("minecraft", "lava"))); + net.minecraft.tags.TagCollection previousTags = net.minecraft.tags.FluidTags.getCollection(); + net.minecraft.tags.TagCollection tags = new net.minecraft.tags.TagCollection<>(id -> + Optional.ofNullable(net.minecraftforge.registries.ForgeRegistries.FLUIDS.getValue(id)), "tags/fluids", false, "fluid"); + Map> builders = new HashMap<>(); + builders.put(new ResourceLocation("minecraft", "water"), net.minecraft.tags.Tag.Builder.create() + .add(net.minecraft.fluid.Fluids.WATER, net.minecraft.fluid.Fluids.FLOWING_WATER)); + builders.put(new ResourceLocation("minecraft", "lava"), net.minecraft.tags.Tag.Builder.create() + .add(net.minecraft.fluid.Fluids.LAVA, net.minecraft.fluid.Fluids.FLOWING_LAVA)); + tags.registerAll(builders); net.minecraft.tags.FluidTags.setCollection(tags); + try { requireBucket(Items.crude_oil_bucket, Fluids.crude_oil); requireBucket(Items.refined_oil_bucket, Fluids.refined_oil); result.addProperty("oil_bucket_identities", true); @@ -46,40 +54,36 @@ static void verify(Minecraft minecraft, JsonObject result) { valid &= check(minecraft, Fluids.flowing_crude_oil.getFlowingFluidState(5, false), "poweradvantage:blocks/crude_oil_flow", true, cases); valid &= check(minecraft, Fluids.refined_oil.getDefaultState(), "poweradvantage:blocks/refined_oil_still", false, cases); valid &= check(minecraft, Fluids.flowing_refined_oil.getFlowingFluidState(5, false), "poweradvantage:blocks/refined_oil_flow", true, cases); - valid &= check(minecraft, net.minecraft.init.Fluids.WATER.getDefaultState(), "minecraft:block/water_still", false, cases); - valid &= check(minecraft, net.minecraft.init.Fluids.LAVA.getDefaultState(), "minecraft:block/lava_still", false, cases); + valid &= check(minecraft, net.minecraft.fluid.Fluids.WATER.getDefaultState(), "minecraft:block/water_still", false, cases); + valid &= check(minecraft, net.minecraft.fluid.Fluids.LAVA.getDefaultState(), "minecraft:block/lava_still", false, cases); if (!valid) throw new IllegalStateException("Native fluid renderer used the wrong sprite or tint: " + cases); + } finally { net.minecraft.tags.FluidTags.setCollection(previousTags); } } private static void requireBucket(net.minecraft.item.Item bucket, Fluid expected) { - try { - for (Field field : ItemBucket.class.getDeclaredFields()) { - if (field.getType() != Fluid.class) continue; - field.setAccessible(true); - if (field.get(bucket) == expected && expected.getFilledBucket() == bucket) return; - } - } catch (ReflectiveOperationException failure) { - throw new IllegalStateException("Cannot inspect native bucket fluid", failure); + net.minecraftforge.fluids.FluidStack contents = net.minecraftforge.fluids.FluidUtil.getFluidContained(new ItemStack(bucket)) + .orElseThrow(() -> new IllegalStateException("Bucket has no native fluid capability: " + bucket.getRegistryName())); + if (!(bucket instanceof BucketItem) || ((BucketItem) bucket).getFluid() != expected + || expected.getFilledBucket() != bucket || contents.getFluid() != expected || contents.getAmount() != 1000) { + throw new IllegalStateException("Wrong bucket fluid: " + bucket.getRegistryName()); } - throw new IllegalStateException("Wrong bucket fluid: " + bucket.getRegistryName()); } private static boolean check(Minecraft minecraft, IFluidState state, String texture, boolean flowing, JsonArray cases) { // A tiny read-only neighbourhood supplies a slope for flowing sprites; no save is opened. - IWorldReader world = (IWorldReader) Proxy.newProxyInstance(OilRenderProbe.class.getClassLoader(), - new Class[]{IWorldReader.class}, (proxy, method, args) -> { - BlockPos pos = args != null && args.length > 0 && args[0] instanceof BlockPos ? (BlockPos) args[0] : null; - if (method.getName().equals("getBlockState")) return blockAt(state, flowing, pos); - if (method.getName().equals("getFluidState")) return blockAt(state, flowing, pos).getFluidState(); - if (method.getName().equals("getCombinedLight")) return 0x00F000F0; - if (method.getName().equals("getBiome")) return net.minecraft.init.Biomes.PLAINS; - throw new UnsupportedOperationException(method.toString()); - }); + IEnviromentBlockReader world = new IEnviromentBlockReader() { + @Override public TileEntity getTileEntity(BlockPos pos) { return null; } + @Override public BlockState getBlockState(BlockPos pos) { return blockAt(state, flowing, pos); } + @Override public IFluidState getFluidState(BlockPos pos) { return getBlockState(pos).getFluidState(); } + @Override public int getLightFor(LightType type, BlockPos pos) { return 15; } + @Override public Biome getBiome(BlockPos pos) { return net.minecraft.world.biome.Biomes.PLAINS; } + }; BufferBuilder buffer = new BufferBuilder(4096); - buffer.begin(org.lwjgl.opengl.GL11.GL_QUADS, DefaultVertexFormats.BLOCK); - boolean rendered = new BlockFluidRenderer().render(world, BlockPos.ORIGIN, buffer, state); + buffer.begin(7, DefaultVertexFormats.BLOCK); + boolean rendered = minecraft.getBlockRendererDispatcher().renderFluid(BlockPos.ZERO, world, buffer, state); buffer.finishDrawing(); TextureAtlasSprite sprite = minecraft.getTextureMap().getSprite(new ResourceLocation(texture)); + require(rendered && buffer.getVertexCount() >= 4, "Native fluid renderer emitted no top face for " + state); ByteBuffer data = buffer.getByteBuffer(); float u = data.getFloat(DefaultVertexFormats.BLOCK.getUvOffsetById(0)); float v = data.getFloat(DefaultVertexFormats.BLOCK.getUvOffsetById(0) + 4); @@ -89,7 +93,7 @@ private static boolean check(Minecraft minecraft, IFluidState state, String text && u >= sprite.getMinU() && u <= sprite.getMaxU() && v >= sprite.getMinV() && v <= sprite.getMaxV(); boolean correctTint = !(state.getFluid() instanceof Fluids.PowerFluid) || rgb == 0xFFFFFF; JsonObject entry = new JsonObject(); - entry.addProperty("fluid", net.minecraft.util.registry.IRegistry.field_212619_h.getKey(state.getFluid()).toString()); + entry.addProperty("fluid", state.getFluid().getRegistryName().toString()); entry.addProperty("block", state.getBlockState().getBlock().getRegistryName().toString()); entry.addProperty("expected_sprite", texture); entry.addProperty("correct_sprite", correctSprite); @@ -99,11 +103,15 @@ private static boolean check(Minecraft minecraft, IFluidState state, String text return rendered && buffer.getVertexCount() > 0 && correctSprite && correctTint; } - private static IBlockState blockAt(IFluidState state, boolean flowing, BlockPos pos) { - if (BlockPos.ORIGIN.equals(pos)) return state.getBlockState(); - if (flowing && BlockPos.ORIGIN.east().equals(pos)) { + private static BlockState blockAt(IFluidState state, boolean flowing, BlockPos pos) { + if (BlockPos.ZERO.equals(pos)) return state.getBlockState(); + if (flowing && BlockPos.ZERO.east().equals(pos)) { return ((FlowingFluid) state.getFluid()).getFlowingFluidState(3, false).getBlockState(); } - return (pos.getY() < 0 ? net.minecraft.init.Blocks.STONE : net.minecraft.init.Blocks.AIR).getDefaultState(); + return (pos.getY() < 0 ? net.minecraft.block.Blocks.STONE : net.minecraft.block.Blocks.AIR).getDefaultState(); + } + + private static void require(boolean condition, String message) { + if (!condition) throw new IllegalStateException(message); } } diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/RecipeBookGroupingProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/RecipeBookGroupingProbe.java index 47512b3..5defe47 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/RecipeBookGroupingProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/RecipeBookGroupingProbe.java @@ -22,65 +22,87 @@ import java.util.stream.Stream; import javax.imageio.ImageIO; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.recipebook.GuiButtonRecipe; +import net.minecraft.client.gui.recipebook.RecipeWidget; import net.minecraft.client.gui.recipebook.RecipeList; -import net.minecraft.client.renderer.GlStateManager; +import com.mojang.blaze3d.platform.GlStateManager; import net.minecraft.client.shader.Framebuffer; -import net.minecraft.client.util.RecipeBookClient; +import net.minecraft.client.util.ClientRecipeBook; import net.minecraft.item.crafting.IRecipe; import net.minecraft.item.crafting.RecipeBook; import net.minecraft.item.crafting.RecipeItemHelper; import net.minecraft.item.crafting.RecipeManager; import net.minecraft.network.PacketBuffer; -import net.minecraft.network.play.server.SPacketUpdateRecipes; -import org.lwjgl.BufferUtils; -import org.lwjgl.opengl.GL11; +import net.minecraft.network.play.server.SUpdateRecipesPacket; /** Decodes actual server recipe packets and exercises Minecraft's real cell grouping. */ final class RecipeBookGroupingProbe { + private static final int GL_PROJECTION = 5889, GL_MODELVIEW = 5888; + private static final int GL_COLOR_BUFFER_BIT = 16384, GL_DEPTH_BUFFER_BIT = 256, GL_RGBA = 6408, GL_UNSIGNED_BYTE = 5121; static void verify(Minecraft minecraft, JsonObject report) { Path root = Paths.get("recipe-book-fixtures"); - if (!Files.isDirectory(root)) return; + if (!Files.isDirectory(root)) { + report.addProperty("recipe_book_grouping_status", "NOT_RUN_NO_NATIVE_FIXTURES"); + return; + } JsonArray checks = new JsonArray(); + report.add("recipe_book_grouping", checks); try (Stream files = Files.list(root)) { // A separate main-menu client has not performed Forge's registry handshake. // Import only the generated server fixture's ID snapshot, never a user world. Map registry = net.minecraftforge.registries.RegistryManager.ACTIVE.takeSnapshot(false); - net.minecraft.nbt.NBTTagCompound saved; + net.minecraft.nbt.CompoundNBT saved; try (java.io.InputStream input = Files.newInputStream(root.resolve("server-registries.dat"))) { - saved = net.minecraft.nbt.CompressedStreamTools.readCompressed(input).getCompound("fml").getCompound("Registries"); + net.minecraft.nbt.CompoundNBT level = net.minecraft.nbt.CompressedStreamTools.readCompressed(input); + require("1.14.4".equals(level.getCompound("Data").getCompound("Version").getString("Name")), + "Recipe fixture is not a native 1.14.4 server export"); + net.minecraft.nbt.ListNBT mods = level.getCompound("fml").getList("LoadingModList", 10); + boolean nativeForge = false; + for (int i = 0; i < mods.size(); i++) { + net.minecraft.nbt.CompoundNBT mod = mods.getCompound(i); + if ("forge".equals(mod.getString("ModId"))) nativeForge = "28.2.26".equals(mod.getString("ModVersion")); + } + require(nativeForge, "Recipe fixture was not saved by Forge 28.2.26"); + saved = level.getCompound("fml").getCompound("Registries"); } - require(saved.contains("minecraft:items", 10) && saved.contains("minecraft:blocks", 10), "Missing server registry snapshot"); + String itemRegistry = net.minecraftforge.registries.ForgeRegistries.ITEMS.getRegistryName().toString(); + String blockRegistry = net.minecraftforge.registries.ForgeRegistries.BLOCKS.getRegistryName().toString(); + require(saved.contains(itemRegistry, 10) && saved.contains(blockRegistry, 10) + && !saved.getCompound(itemRegistry).getList("ids", 10).isEmpty() + && !saved.getCompound(blockRegistry).getList("ids", 10).isEmpty(), "Missing native server registry snapshot"); for (String key : saved.keySet()) registry.put(new net.minecraft.util.ResourceLocation(key), net.minecraftforge.registries.ForgeRegistry.Snapshot.read(saved.getCompound(key))); require(net.minecraftforge.registries.GameData.injectSnapshot(registry, false, false).isEmpty(), "Client lacks a server registry entry"); List packets = files.filter(file -> file.toString().endsWith(".bin")).sorted().collect(Collectors.toList()); - require(!packets.isEmpty(), "No recipe-book packets supplied"); + require(packets.size() == 3, "Normal, Tech and Apocalyptic recipe-book packets are required"); for (Path file : packets) { JsonObject manifest = new JsonParser().parse(new String(Files.readAllBytes(Paths.get( file.toString().replace(".bin", ".json"))), StandardCharsets.UTF_8)).getAsJsonObject(); + require("PASS".equals(manifest.get("status").getAsString()), "Server recipe fixture did not pass"); Map expected = new HashMap<>(); - manifest.getAsJsonArray("bookRecipes").forEach(row -> expected.put(row.getAsJsonObject().get("id").getAsString(), row.getAsJsonObject())); - SPacketUpdateRecipes packet = new SPacketUpdateRecipes(); + manifest.getAsJsonArray("bookRecipes").forEach(row -> require(expected.put(row.getAsJsonObject().get("id").getAsString(), + row.getAsJsonObject()) == null, "Duplicate recipe in packet manifest")); + SUpdateRecipesPacket packet = new SUpdateRecipesPacket(); PacketBuffer buffer = new PacketBuffer(Unpooled.wrappedBuffer(Files.readAllBytes(file))); try { packet.readPacketData(buffer); require(!buffer.isReadable(), "Trailing recipe packet bytes"); } finally { buffer.release(); } RecipeManager manager = new RecipeManager(); Set cellKeys = new HashSet<>(); - for (IRecipe recipe : packet.getRecipes()) { + for (IRecipe recipe : packet.getRecipes()) { JsonObject row = expected.get(recipe.getId().toString()); require(row != null, "Unexpected network recipe " + recipe.getId()); String output = recipe.getRecipeOutput().getItem().getRegistryName().toString(); require(output.equals(row.get("output").getAsString()) && recipe.getRecipeOutput().getCount() == row.get("count").getAsInt(), "Client/server item registry mismatch for " + recipe.getId()); require(recipe.getGroup().equals(row.get("group").getAsString()) && recipe.getGroup().equals(output), "Group changed in recipe packet"); - manager.addRecipe(recipe); - cellKeys.add(recipe.getType().getId() + "|" + output); + String type = net.minecraft.util.registry.Registry.RECIPE_TYPE.getKey(recipe.getType()).toString(); + require(type.equals(row.get("type").getAsString()), "Recipe type changed in packet"); + cellKeys.add(type + "|" + output); } + manager.func_223389_a(packet.getRecipes()); require(manager.getRecipes().size() == expected.size(), "Missing network recipes"); - RecipeBookClient book = new RecipeBookClient(manager); + ClientRecipeBook book = new ClientRecipeBook(manager); manager.getRecipes().forEach(book::unlock); book.rebuildTable(); int counted = 0; @@ -103,36 +125,38 @@ static void verify(Minecraft minecraft, JsonObject report) { if (file.getFileName().toString().equals("recipes-normal-sweep.bin")) render(minecraft, book); } } catch (Exception failure) { throw new IllegalStateException("Native recipe-book validation failed", failure); } - report.add("recipe_book_grouping", checks); + report.addProperty("recipe_book_grouping_status", "PASS"); } - private static void render(Minecraft minecraft, RecipeBookClient book) throws Exception { + private static void render(Minecraft minecraft, ClientRecipeBook book) throws Exception { List shown = new ArrayList<>(); Set selected = new HashSet<>(java.util.Arrays.asList("electricadvantage:blank_circuit_board", "electricadvantage:integrated_circuit", "electricadvantage:control_circuit", "electricadvantage:psu", "poweradvantage:item_conveyor")); for (RecipeList cell : book.getRecipes()) { - IRecipe recipe = cell.getRecipes().get(0); + IRecipe recipe = cell.getRecipes().get(0); String id = recipe.getRecipeOutput().getItem().getRegistryName().toString(); if (selected.contains(id) || id.startsWith("poweradvantage:item_filter_")) shown.add(cell); } shown.sort(java.util.Comparator.comparing(cell -> cell.getRecipes().get(0).getRecipeOutput().getItem().getRegistryName().toString())); require(shown.size() == 13, "The thirteen reported circuit/conveyor cells must be separate"); int width = 1000, height = 180; - Framebuffer target = new Framebuffer(width, height, true); + Framebuffer target = new Framebuffer(width, height, true, Minecraft.IS_RUNNING_ON_MAC); target.bindFramebuffer(true); - GlStateManager.matrixMode(GL11.GL_PROJECTION); + GlStateManager.matrixMode(GL_PROJECTION); GlStateManager.pushMatrix(); GlStateManager.loadIdentity(); GlStateManager.ortho(0, width, height, 0, -1000, 1000); - GlStateManager.matrixMode(GL11.GL_MODELVIEW); + GlStateManager.matrixMode(GL_MODELVIEW); GlStateManager.pushMatrix(); GlStateManager.loadIdentity(); GlStateManager.disableDepthTest(); try { - GL11.glClearColor(0.12F, 0.12F, 0.12F, 1F); - GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); + GlStateManager.clearColor(0.12F, 0.12F, 0.12F, 1F); + GlStateManager.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, Minecraft.IS_RUNNING_ON_MAC); for (int i = 0; i < shown.size(); i++) { int x = 10 + (i % 5) * 198, y = 12 + (i / 5) * 55; - GuiButtonRecipe button = new GuiButtonRecipe(); - for (Field field : GuiButtonRecipe.class.getDeclaredFields()) { + RecipeWidget button = new RecipeWidget(); + // Native page initialization needs a connected player. Supply only its two + // cell/book inputs; null means the ordinary crafting filter in RecipeBook. + for (Field field : RecipeWidget.class.getDeclaredFields()) { if (field.getType() == RecipeList.class || field.getType() == RecipeBook.class) { field.setAccessible(true); field.set(button, field.getType() == RecipeList.class ? shown.get(i) : book); @@ -143,8 +167,8 @@ private static void render(Minecraft minecraft, RecipeBookClient book) throws Ex GlStateManager.disableLighting(); minecraft.fontRenderer.drawString(shown.get(i).getRecipes().get(0).getRecipeOutput().getDisplayName().getFormattedText(), x + 30, y + 8, 0xFFFFFF); } - ByteBuffer pixels = BufferUtils.createByteBuffer(width * height * 4); - GL11.glReadPixels(0, 0, width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels); + ByteBuffer pixels = ByteBuffer.allocateDirect(width * height * 4); + GlStateManager.readPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); int colored = 0; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) { @@ -153,11 +177,11 @@ private static void render(Minecraft minecraft, RecipeBookClient book) throws Ex image.setRGB(x, y, rgb); if ((rgb & 255) > 80 || ((rgb >>> 16) & 255) > 80) colored++; } - require(colored > 1000, "Native recipe buttons rendered blank"); ImageIO.write(image, "png", new File("recipe-book-cells.png")); + require(colored > 1000, "Native recipe buttons rendered blank"); } finally { - GlStateManager.popMatrix(); GlStateManager.matrixMode(GL11.GL_PROJECTION); - GlStateManager.popMatrix(); GlStateManager.matrixMode(GL11.GL_MODELVIEW); + GlStateManager.popMatrix(); GlStateManager.matrixMode(GL_PROJECTION); + GlStateManager.popMatrix(); GlStateManager.matrixMode(GL_MODELVIEW); target.deleteFramebuffer(); minecraft.getFramebuffer().bindFramebuffer(true); GlStateManager.enableDepthTest(); } diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java index a01821a..9bdd4a9 100644 --- a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java +++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java @@ -1,24 +1,25 @@ package zone.moddev.mc.advantageworks.client; import com.google.gson.JsonObject; -import cyano.poweradvantage.api.fluid.FluidStack; import cyano.poweradvantage.api.simple.SimpleMachineGUI; import net.minecraft.block.Block; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.GlStateManager; +import com.mojang.blaze3d.platform.GlStateManager; import net.minecraft.client.shader.Framebuffer; -import net.minecraft.entity.player.InventoryPlayer; +import net.minecraft.entity.player.PlayerInventory; import net.minecraft.fluid.IFluidState; import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.BlockPos; import net.minecraft.world.IBlockReader; -import org.lwjgl.BufferUtils; -import org.lwjgl.opengl.GL11; import zone.moddev.mc.poweradvantage.gui.FilteredFluidTankGUI; -import zone.moddev.mc.poweradvantage.gui.FluidTankGUI; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.CapabilityFluidHandler; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction; +import zone.moddev.mc.poweradvantage.registry.MachineGUIRegistry; import zone.moddev.mc.poweradvantage.init.Blocks; import zone.moddev.mc.poweradvantage.machines.fluidmachines.MetalTankTileEntity; @@ -30,6 +31,8 @@ /** Checks native neighbour culling and the actual fluid-bar drawing function. */ final class TankVisualProbe { + private static final int GL_PROJECTION = 5889, GL_MODELVIEW = 5888; + private static final int GL_COLOR_BUFFER_BIT = 16384, GL_RGBA = 6408, GL_UNSIGNED_BYTE = 5121; private TankVisualProbe() {} static void verify(Minecraft minecraft, JsonObject result) { @@ -39,12 +42,17 @@ static void verify(Minecraft minecraft, JsonObject result) { result.addProperty("portable_tank_floor_visible", portableFloor); MetalTankTileEntity serverTank = new MetalTankTileEntity(); - serverTank.getTank().fill(new FluidStack(net.minecraft.init.Fluids.WATER, 10000), true); + IFluidHandler handler = serverTank.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, Direction.UP) + .orElseThrow(() -> new IllegalStateException("Metal tank has no native Forge fluid capability")); + require(handler.fill(new FluidStack(net.minecraft.fluid.Fluids.WATER, 10000), FluidAction.SIMULATE) == 10000 + && serverTank.getTank().getFluidAmount() == 0, "Native simulated fill changed the tank"); + require(handler.fill(new FluidStack(net.minecraft.fluid.Fluids.WATER, 10000), FluidAction.EXECUTE) == 10000, + "Native capability did not fill the tank"); MetalTankTileEntity clientTank = new MetalTankTileEntity(); clientTank.onDataPacket(null, serverTank.getUpdatePacket()); result.addProperty("synced_tank_amount", clientTank.getTank().getFluidAmount()); require(clientTank.getTank().getFluidAmount() == 10000, "Filled tank packet lost water"); - serverTank.getTank().drain(10000, true); + require(handler.drain(10000, FluidAction.EXECUTE).getAmount() == 10000, "Native capability did not drain the tank"); clientTank.onDataPacket(null, serverTank.getUpdatePacket()); require(clientTank.getTank().getFluidAmount() == 0, "Empty tank packet retained old water"); @@ -57,44 +65,49 @@ static void verify(Minecraft minecraft, JsonObject result) { require(blue > red + 20 && blue > green + 10, "Water gauge is not blue: " + String.format("%06X", rgb)); } - private static boolean floorVisible(final IBlockState tank) { + private static boolean floorVisible(final BlockState tank) { IBlockReader world = new IBlockReader() { @Override public TileEntity getTileEntity(BlockPos pos) { return null; } - @Override public IBlockState getBlockState(BlockPos pos) { - return pos.equals(BlockPos.ORIGIN.up()) ? tank : net.minecraft.init.Blocks.GRASS_BLOCK.getDefaultState(); + @Override public BlockState getBlockState(BlockPos pos) { + return pos.equals(BlockPos.ZERO.up()) ? tank : net.minecraft.block.Blocks.GRASS_BLOCK.getDefaultState(); } @Override public IFluidState getFluidState(BlockPos pos) { return getBlockState(pos).getFluidState(); } }; - return Block.shouldSideBeRendered(net.minecraft.init.Blocks.GRASS_BLOCK.getDefaultState(), world, BlockPos.ORIGIN, EnumFacing.UP); + return Block.shouldSideBeRendered(net.minecraft.block.Blocks.GRASS_BLOCK.getDefaultState(), world, BlockPos.ZERO, Direction.UP); } private static int renderWater(Minecraft minecraft, MetalTankTileEntity tile) { FilteredFluidTankGUI definition = new FilteredFluidTankGUI(new ResourceLocation("poweradvantage", "textures/gui/container/fluid_metal_tank_gui.png")); - SimpleMachineGUI.GUIContainer screen = definition.new GUIContainer(new InventoryPlayer(null), tile); - screen.mc = minecraft; - tile.getTank().setFluid(new FluidStack(net.minecraft.init.Fluids.WATER, 10000)); final int width = 128, height = 96; - Framebuffer framebuffer = new Framebuffer(width, height, true); + PlayerInventory inventory = new PlayerInventory(null); + SimpleMachineGUI.Container container = definition.createContainer(1, inventory, tile); + require(container.getType() == MachineGUIRegistry.TYPE && MachineGUIRegistry.TYPE != null, + "Tank GUI is not backed by the registered native container type"); + SimpleMachineGUI.GUIContainer screen = container.createScreen(inventory, new net.minecraft.util.text.StringTextComponent("Tank probe")); + screen.init(minecraft, width, height); + require(screen.getMinecraft() == minecraft, "Native tank screen was not initialized"); + tile.getTank().setFluid(new cyano.poweradvantage.api.fluid.FluidStack(net.minecraft.fluid.Fluids.WATER, 10000)); + Framebuffer framebuffer = new Framebuffer(width, height, true, Minecraft.IS_RUNNING_ON_MAC); framebuffer.bindFramebuffer(true); - GlStateManager.matrixMode(GL11.GL_PROJECTION); + GlStateManager.matrixMode(GL_PROJECTION); GlStateManager.pushMatrix(); GlStateManager.loadIdentity(); GlStateManager.ortho(0.0D, width, height, 0.0D, -1.0D, 1.0D); - GlStateManager.matrixMode(GL11.GL_MODELVIEW); + GlStateManager.matrixMode(GL_MODELVIEW); GlStateManager.pushMatrix(); GlStateManager.loadIdentity(); GlStateManager.disableLighting(); GlStateManager.disableDepthTest(); GlStateManager.enableBlend(); - GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - GlStateManager.enableTexture2D(); + GlStateManager.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); + GlStateManager.enableTexture(); GlStateManager.color4f(1F, 1F, 1F, 1F); try { - GL11.glClearColor(0F, 0F, 0F, 1F); - GL11.glClear(GL11.GL_COLOR_BUFFER_BIT); + GlStateManager.clearColor(0F, 0F, 0F, 1F); + GlStateManager.clear(GL_COLOR_BUFFER_BIT, Minecraft.IS_RUNNING_ON_MAC); definition.drawGUIDecorations(tile, screen, 0, 0, 0F); - ByteBuffer pixels = BufferUtils.createByteBuffer(width * height * 4); - GL11.glReadPixels(0, 0, width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels); + ByteBuffer pixels = ByteBuffer.allocateDirect(width * height * 4); + GlStateManager.readPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { @@ -109,9 +122,9 @@ private static int renderWater(Minecraft minecraft, MetalTankTileEntity tile) { throw new IllegalStateException("Could not retain the native tank gauge image", failure); } finally { GlStateManager.popMatrix(); - GlStateManager.matrixMode(GL11.GL_PROJECTION); + GlStateManager.matrixMode(GL_PROJECTION); GlStateManager.popMatrix(); - GlStateManager.matrixMode(GL11.GL_MODELVIEW); + GlStateManager.matrixMode(GL_MODELVIEW); framebuffer.deleteFramebuffer(); minecraft.getFramebuffer().bindFramebuffer(true); GlStateManager.enableDepthTest(); diff --git a/test-rig/client-resources/resources/META-INF/mods.toml b/test-rig/client-resources/resources/META-INF/mods.toml index 87823e5..421aeef 100644 --- a/test-rig/client-resources/resources/META-INF/mods.toml +++ b/test-rig/client-resources/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[25,)" +loaderVersion="[28,29)" license="GPL-3.0-only" [[mods]] @@ -11,6 +11,20 @@ description='''Build-only model, texture and translation checks. Never distribut [[dependencies.advantageclientresources]] modId="poweradvantage" mandatory=true -versionRange="[2.4.4.113021,)" +versionRange="[2.4.5.114041,2.5.0)" ordering="AFTER" side="CLIENT" + +[[dependencies.advantageclientresources]] +modId="forge" +mandatory=true +versionRange="[28.2.26]" +ordering="NONE" +side="CLIENT" + +[[dependencies.advantageclientresources]] +modId="minecraft" +mandatory=true +versionRange="[1.14.4]" +ordering="NONE" +side="CLIENT" diff --git a/test-rig/client-resources/run.ps1 b/test-rig/client-resources/run.ps1 index 1329166..f1cbe84 100644 --- a/test-rig/client-resources/run.ps1 +++ b/test-rig/client-resources/run.ps1 @@ -2,17 +2,66 @@ param([Parameter(Mandatory = $true)][string]$JavaHome, [ValidateSet('runClient.launch', 'runClientAllAdvantage.launch')][string]$LaunchProfile = 'runClient.launch', [ValidateSet('Gradle', 'Eclipse')][string]$OutputSource = 'Gradle', [switch]$RequireExtensions, - [string]$RecipeBookFixtures) + [string]$RecipeBookFixtures, + [string]$PowerCandidate, + [switch]$PreflightOnly, + [ValidateRange(30, 1800)][int]$TimeoutSeconds = 300) $ErrorActionPreference = 'Stop' $project = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path $java = Join-Path $JavaHome 'bin/java.exe' -if (-not (Test-Path -LiteralPath $java)) { throw "Java 8 runtime not found: $java" } +Add-Type -AssemblyName System.IO.Compression.FileSystem +function Get-Record([string]$Path) { + $file = Get-Item -LiteralPath $Path + [ordered]@{ path = $file.FullName; bytes = $file.Length; sha256 = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash } +} +function Read-JarMetadata([string]$Path) { + $jar = [IO.Compression.ZipFile]::OpenRead($Path) + try { + $entry = $jar.GetEntry('META-INF/mods.toml') + if (-not $entry) { return $null } + $reader = [IO.StreamReader]::new($entry.Open()) + try { $reader.ReadToEnd() } finally { $reader.Dispose() } + } finally { $jar.Dispose() } +} +function Assert-NativeMetadata([string]$Metadata, [string]$Id) { + if (-not $Metadata -or $Metadata -match '\$\{[^}]+\}') { throw "Missing/unexpanded metadata for $Id" } + # Match the known single-mod header, not dependency modId/versionRange fields. + $header = [regex]::Match($Metadata, '(?ms)^\[\[mods\]\]\s*(.*?)(?=^\[|\z)').Groups[1].Value + if ($header -notmatch ('(?m)^modId\s*=\s*"' + [regex]::Escape($Id) + '"[ \t]*(?:#[^\r\n]*)?\r?$') -or + $header -notmatch '(?m)^version\s*=\s*"[^"\r\n]*\.114\d+"[ \t]*(?:#[^\r\n]*)?\r?$' -or + $Metadata -notmatch '(?m)^loaderVersion\s*=\s*"\[28,') { throw "Not a native Forge 28 candidate: $Id" } +} +function Quote-Argument([string]$Value) { + '"' + [regex]::Replace([regex]::Replace($Value, '(\\*)"', '$1$1\"'), '(\\+)$', '$1$1') + '"' +} $probe = Join-Path $project 'build/client-resource-probe/advantageclientresources-deobf.jar' +$runtime = Join-Path $project ('build/client-resource-runs/' + [DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N').Substring(0, 8)) +$inputReport = [ordered]@{ minecraft = '1.14.4'; forge = '28.2.26'; qualification = 'UNQUALIFIED'; output_source = $OutputSource; launch_profile = $LaunchProfile; require_extensions = [bool]$RequireExtensions } +$phase = 'launch-preflight' +New-Item -ItemType Directory -Path $runtime | Out-Null +try { +if (-not (Test-Path -LiteralPath $java)) { throw "Java 8 runtime not found: $java" } +if ((Get-Content -LiteralPath (Join-Path $JavaHome 'release') -Raw) -notmatch '(?m)^JAVA_VERSION="1\.8\.') { throw 'Supply Java 8 for the native client.' } if (-not (Test-Path -LiteralPath $probe)) { throw 'Run gradlew.bat clientResourceProbeJar verifyEclipseProductionClasspath first.' } [xml]$launch = Get-Content -LiteralPath (Join-Path $project $LaunchProfile) -Raw $attributes = @{} foreach ($attribute in $launch.launchConfiguration.stringAttribute) { $attributes[$attribute.key] = $attribute.value } -$runtime = Join-Path $project ('build/client-resource-runs/' + [DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N').Substring(0, 8)) +$inputReport.java = Get-Record $java +$inputReport.launch = Get-Record (Join-Path $project $LaunchProfile) +$inputReport.probe = Get-Record $probe +$probeMetadata = Read-JarMetadata $probe +if ($probeMetadata -notmatch 'loaderVersion="\[28,29\)"' -or $probeMetadata -notmatch 'versionRange="\[1\.14\.4\]"') { throw 'Rebuild the native 1.14 probe; do not substitute the 1.13 probe.' } +Copy-Item -LiteralPath (Join-Path $project $LaunchProfile) -Destination (Join-Path $runtime 'launch.xml') +$environment = @{} +foreach ($map in $launch.launchConfiguration.mapAttribute) { + if ($map.key -eq 'org.eclipse.debug.core.environmentVariables') { + foreach ($entry in $map.mapEntry) { $environment[$entry.key] = $entry.value } + } +} +if ($environment.MC_VERSION -ne '1.14.4' -or $environment.FORGE_VERSION -ne '28.2.26' -or $environment.target -ne 'fmluserdevclient') { throw 'Regenerate the native 1.14.4 / Forge 28.2.26 client launch.' } +if ($attributes['org.eclipse.jdt.launching.PROGRAM_ARGUMENTS'] -notmatch '1\.14\.4-28\.2\.26' -or + $attributes['org.eclipse.jdt.launching.PROGRAM_ARGUMENTS'] -match '1\.13\.2|--(?:server|port|gameDir|world)\b' -or + $attributes['org.eclipse.jdt.launching.MAIN_TYPE'] -ne 'net.minecraftforge.launcher.Main') { throw 'Unexpected native launch arguments; no server connection or existing game directory is allowed.' } $modOutput = Join-Path $runtime 'poweradvantage' New-Item -ItemType Directory -Path $modOutput,(Join-Path $runtime 'mods') -Force | Out-Null if ($OutputSource -eq 'Eclipse') { @@ -22,7 +71,6 @@ if ($OutputSource -eq 'Eclipse') { Copy-Item -Path (Join-Path $project 'build/resources/main/*') -Destination $modOutput -Recurse -Force } $metadataPath = Join-Path $modOutput 'META-INF/mods.toml' -$inputReport = [ordered]@{ output_source = $OutputSource; launch_profile = $LaunchProfile; require_extensions = [bool]$RequireExtensions } if (Test-Path -LiteralPath $metadataPath) { $inputReport.metadata_sha256 = (Get-FileHash -LiteralPath $metadataPath).Hash } $inputReport | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $runtime 'client-input.json') -Encoding utf8 if (-not (Test-Path -LiteralPath $metadataPath) -or (Get-Content -LiteralPath $metadataPath -Raw) -match '\$\{[^}]+\}') { @@ -31,39 +79,94 @@ if (-not (Test-Path -LiteralPath $metadataPath) -or (Get-Content -LiteralPath $m Set-Content -LiteralPath (Join-Path $runtime 'client-resources.json') -Encoding utf8 throw $failure } +Assert-NativeMetadata (Get-Content -LiteralPath $metadataPath -Raw) 'poweradvantage' +$outputFiles = @(Get-ChildItem -LiteralPath $modOutput -Recurse -File | Sort-Object FullName | ForEach-Object { Get-Record $_.FullName }) +if (@($outputFiles | Where-Object { $_.path -like '*.class' }).Count -eq 0) { throw 'Selected output has no compiled production classes.' } +$outputFiles | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $runtime 'production-files.json') -Encoding utf8 +$inputReport.production_files = Get-Record (Join-Path $runtime 'production-files.json') +if (-not $PowerCandidate) { $PowerCandidate = Join-Path $project 'build/libs-dev/PowerAdvantage-2.4.5.114041-deobf.jar' } +$PowerCandidate = (Resolve-Path -LiteralPath $PowerCandidate).Path +Assert-NativeMetadata (Read-JarMetadata $PowerCandidate) 'poweradvantage' +$inputReport.power_candidate = Get-Record $PowerCandidate +$inputReport.candidate_note = 'Jar provenance only; runtime uses the separately hashed selected production classes/resources.' Copy-Item -LiteralPath $probe -Destination (Join-Path $runtime 'mods/advantageclientresources.jar') +if ((Get-FileHash -LiteralPath (Join-Path $runtime 'mods/advantageclientresources.jar')).Hash -ne $inputReport.probe.sha256) { throw 'Probe changed during snapshot.' } if ($RecipeBookFixtures) { + if (-not $RequireExtensions) { throw 'The thirteen recipe-cell rendering assertions require the combined profile.' } $fixtures = (Resolve-Path -LiteralPath $RecipeBookFixtures).Path $destination = Join-Path $runtime 'recipe-book-fixtures' New-Item -ItemType Directory -Path $destination | Out-Null $packets = @(Get-ChildItem -LiteralPath $fixtures -Filter 'recipes-*-sweep.bin') if ($packets.Count -ne 3) { throw 'Recipe grouping requires Normal, Tech and Apocalyptic sweep packets.' } + foreach ($mode in 'normal','tech','apocalyptic') { + if (-not (Test-Path -LiteralPath (Join-Path $fixtures "recipes-$mode-sweep.bin"))) { throw "Missing $mode recipe packet." } + } + $fixtureRecords = @() foreach ($packet in $packets) { + $fixtureRecords += Get-Record $packet.FullName + $fixtureRecords += Get-Record ([IO.Path]::ChangeExtension($packet.FullName, '.json')) Copy-Item -LiteralPath $packet.FullName -Destination $destination Copy-Item -LiteralPath ([IO.Path]::ChangeExtension($packet.FullName, '.json')) -Destination $destination } $registrySource = Join-Path $fixtures 'AdvantageWorksMigration/level.dat' + $fixtureRecords += Get-Record $registrySource + $inputReport.recipe_fixtures = $fixtureRecords Copy-Item -LiteralPath $registrySource -Destination (Join-Path $destination 'server-registries.dat') } -$archives = foreach ($entry in $launch.launchConfiguration.listAttribute.listEntry) { +$classpathNode = @($launch.launchConfiguration.listAttribute | Where-Object { $_.key -eq 'org.eclipse.jdt.launching.CLASSPATH' }) +$archives = @(foreach ($entry in $classpathNode.listEntry) { [xml]$classpathEntry = $entry.value if ($classpathEntry.runtimeClasspathEntry.externalArchive) { $classpathEntry.runtimeClasspathEntry.externalArchive } +}) +$archiveRecords = @() +$runtimeCandidates = @{} +foreach ($archive in $archives) { + if ($archive -match '(?:forge|client-extra)-1\.13\.2|[\\/]PA113[\\/]') { throw "Legacy runtime archive: $archive" } + $record = Get-Record $archive + $archiveRecords += $record + $metadata = if ([IO.Path]::GetExtension($archive) -eq '.jar') { Read-JarMetadata $archive } + $modHeader = if ($metadata) { [regex]::Match($metadata, '(?ms)^\[\[mods\]\]\s*(.*?)(?=^\[|\z)').Groups[1].Value } + foreach ($id in 'poweradvantage','steamadvantage','electricadvantage','basemetals','orespawn') { + if ($modHeader -match ('(?m)^modId\s*=\s*"' + $id + '"[ \t]*(?:#[^\r\n]*)?\r?$')) { + Assert-NativeMetadata $metadata $id + if ($id -eq 'poweradvantage' -or $runtimeCandidates.ContainsKey($id)) { throw "Duplicate runtime candidate: $id" } + $runtimeCandidates[$id] = $record + } + } } +$inputReport.archives = $archiveRecords +$inputReport.runtime_candidates = $runtimeCandidates +if (-not ($archives -match 'forge-1\.14\.4-28\.2\.26-snapshot-20190719-1\.14\.3\.jar$')) { throw 'Missing actual native mapped Forge runtime.' } +if ($RequireExtensions -and (-not $runtimeCandidates.ContainsKey('steamadvantage') -or -not $runtimeCandidates.ContainsKey('electricadvantage'))) { throw 'Combined launch must explicitly contain both native expansion candidates.' } $arguments = @([regex]::Matches($attributes['org.eclipse.jdt.launching.PROGRAM_ARGUMENTS'], '"[^"]*"|\S+') | ForEach-Object { $_.Value.Trim('"') }) $vmArguments = @([regex]::Matches($attributes['org.eclipse.jdt.launching.VM_ARGUMENTS'], '"[^"]*"|\S+') | ForEach-Object { $_.Value.Trim('"') }) if ($RequireExtensions) { $vmArguments += '-Dadvantageworks.requireExtensions=true' } +$environment.MOD_CLASSES = "poweradvantage%%$modOutput;poweradvantage%%$modOutput" +$inputReport.environment = $environment +$inputReport.program_arguments = $arguments +$inputReport.vm_arguments = $vmArguments +$inputReport | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $runtime 'client-input.json') -Encoding utf8 +if ($PreflightOnly) { + $inputReport.qualification = 'PREFLIGHT_ONLY_NOT_CLIENT_EVIDENCE' + Write-Output "Native launch preflight passed; no client was started. Evidence: $runtime" + return +} $previous = @{} +$phase = 'native-client' try { - foreach ($entry in $launch.launchConfiguration.mapAttribute.mapEntry) { - $previous[$entry.key] = [Environment]::GetEnvironmentVariable($entry.key, 'Process') - [Environment]::SetEnvironmentVariable($entry.key, $entry.value, 'Process') + foreach ($key in $environment.Keys) { + $previous[$key] = [Environment]::GetEnvironmentVariable($key, 'Process') + [Environment]::SetEnvironmentVariable($key, $environment[$key], 'Process') } - $env:MOD_CLASSES = "poweradvantage%%$modOutput;poweradvantage%%$modOutput" - Push-Location -LiteralPath $runtime - try { - & $java @vmArguments '-Dadvantageworks.clientResources=true' '-Xmx2G' '-cp' ((@($modOutput) + $archives) -join [IO.Path]::PathSeparator) $attributes['org.eclipse.jdt.launching.MAIN_TYPE'] @arguments - if ($LASTEXITCODE -ne 0) { throw "Client exited with code $LASTEXITCODE; inspect $runtime/logs" } - } finally { Pop-Location } + $command = @($vmArguments) + @('-Dadvantageworks.clientResources=true', '-Xmx2G', '-cp', ((@($modOutput) + $archives) -join [IO.Path]::PathSeparator), $attributes['org.eclipse.jdt.launching.MAIN_TYPE']) + $arguments + $process = Start-Process -FilePath $java -ArgumentList (($command | ForEach-Object { Quote-Argument $_ }) -join ' ') -WorkingDirectory $runtime -WindowStyle Hidden -PassThru -RedirectStandardOutput (Join-Path $runtime 'launcher.stdout.log') -RedirectStandardError (Join-Path $runtime 'launcher.stderr.log') + if (-not $process.WaitForExit($TimeoutSeconds * 1000)) { + & taskkill.exe /PID $process.Id /T /F | Out-Null + throw "Native client timed out after $TimeoutSeconds seconds; evidence retained." + } + $process.WaitForExit() + $process.Refresh() + if ($process.ExitCode -ne 0) { throw "Client exited with code $($process.ExitCode); inspect $runtime/logs" } } finally { foreach ($key in $previous.Keys) { [Environment]::SetEnvironmentVariable($key, $previous[$key], 'Process') } } @@ -71,9 +174,26 @@ $resultPath = Join-Path $runtime 'client-resources.json' if (-not (Test-Path -LiteralPath $resultPath)) { throw "Client did not produce a result; inspect $runtime/logs" } $result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json if ($result.status -ne 'PASS') { throw "Client resource check failed: $($result.error); inspect $runtime" } +if ($result.minecraft -ne '1.14.4' -or $result.forge -ne '28.2.26') { throw 'Result is not native 1.14.4 / Forge 28.2.26 evidence.' } +foreach ($record in $archiveRecords) { + if ((Get-FileHash -LiteralPath $record.path).Hash -ne $record.sha256) { throw "Dependency changed during the run: $($record.path)" } +} $log = Get-Content -LiteralPath (Join-Path $runtime 'logs/latest.log') -Raw $namespaces = if ($RequireExtensions) { '(?:poweradvantage|steamadvantage|electricadvantage)' } else { 'poweradvantage' } if ($log -match "Unable to load model:.*${namespaces}:|Exception loading blockstate definition:.*${namespaces}:|\[FML\.TEXTURE_ERRORS/\]") { throw "Client logged a model or texture error; inspect $runtime/logs/latest.log" } Write-Output "PASS: $($result.items) items, $($result.block_states) block states, $($result.creative_entries) creative entries. Evidence: $runtime" +$inputReport.qualification = 'NATIVE_CLIENT_RESOURCE_PASS' +} catch { + # Preserve native partial results and all images; record launcher failures separately. + [ordered]@{ status = 'FAIL'; phase = $phase; error = $_.Exception.Message } | ConvertTo-Json | + Set-Content -LiteralPath (Join-Path $runtime 'runner-failure.json') -Encoding utf8 + if (-not (Test-Path -LiteralPath (Join-Path $runtime 'client-resources.json'))) { + [ordered]@{ status = 'FAIL'; phase = $phase; error = $_.Exception.Message } | ConvertTo-Json | + Set-Content -LiteralPath (Join-Path $runtime 'client-resources.json') -Encoding utf8 + } + throw "Client resource check failed; inspect $runtime. $($_.Exception.Message)" +} finally { + $inputReport | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $runtime 'client-input.json') -Encoding utf8 +} diff --git a/test-rig/client-resources/test-launcher.ps1 b/test-rig/client-resources/test-launcher.ps1 new file mode 100644 index 0000000..2866c53 --- /dev/null +++ b/test-rig/client-resources/test-launcher.ps1 @@ -0,0 +1,65 @@ +$ErrorActionPreference = 'Stop' +$tokens = $null +$errors = $null +$script = Join-Path $PSScriptRoot 'run.ps1' +$ast = [Management.Automation.Language.Parser]::ParseFile($script, [ref]$tokens, [ref]$errors) +if ($errors.Count) { throw ($errors | Out-String) } +# Load helper definitions only. Never execute the runner or start Java here. +foreach ($definition in $ast.FindAll({ param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] }, $false)) { + . ([scriptblock]::Create($definition.Extent.Text)) +} +Add-Type -AssemblyName System.IO.Compression.FileSystem +$checks = 1 +function Assert-Throws([scriptblock]$Action) { + $threw = $false + try { & $Action } catch { $threw = $true } + if (-not $threw) { throw 'Expected invalid native metadata to be rejected.' } + $script:checks++ +} +$native = @' +modLoader="javafml" +loaderVersion="[28,)" # native Forge +[[mods]] # mandatory header +modId="poweradvantage" # inline comment +version="2.4.5.114041" # native candidate +[[dependencies.poweradvantage]] +modId="minecraft" +mandatory=true +versionRange="[1.14.4]" +'@ +Assert-NativeMetadata $native 'poweradvantage' +$checks++ +Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.5.114041','2.4.4.113021')) 'poweradvantage' } +Assert-Throws { Assert-NativeMetadata ($native.Replace('[28,)','[25,)')) 'poweradvantage' } +Assert-Throws { Assert-NativeMetadata ($native.Replace('2.4.5.114041','${version}')) 'poweradvantage' } +Assert-Throws { Assert-NativeMetadata $native 'electricadvantage' } +Assert-Throws { Assert-NativeMetadata '' 'poweradvantage' } +if ((Quote-Argument 'D:\native path\') -ne '"D:\native path\\"') { throw 'Trailing-backslash command-line quoting changed.' } +$checks++ +if ((Quote-Argument 'text"quoted') -ne '"text\"quoted"') { throw 'Embedded-quote command-line quoting changed.' } +$checks++ +$project = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path +$power = Join-Path $project 'build/libs-dev/PowerAdvantage-2.4.5.114041-deobf.jar' +Assert-NativeMetadata (Read-JarMetadata $power) 'poweradvantage' +$checks++ +[xml]$launch = Get-Content -LiteralPath (Join-Path $project 'runClientAllAdvantage.launch') -Raw +$found = @{} +foreach ($entry in ($launch.launchConfiguration.listAttribute | Where-Object { $_.key -eq 'org.eclipse.jdt.launching.CLASSPATH' }).listEntry) { + [xml]$classpath = $entry.value + $path = $classpath.runtimeClasspathEntry.externalArchive + if (-not $path -or [IO.Path]::GetExtension($path) -ne '.jar') { continue } + $metadata = Read-JarMetadata $path + $header = if ($metadata) { [regex]::Match($metadata, '(?ms)^\[\[mods\]\]\s*(.*?)(?=^\[|\z)').Groups[1].Value } + foreach ($id in 'basemetals','orespawn','steamadvantage','electricadvantage') { + if ($header -match ('(?m)^modId\s*=\s*"' + $id + '"[ \t]*(?:#[^\r\n]*)?\r?$')) { + Assert-NativeMetadata $metadata $id + $found[$id] = $true + $checks++ + } + } +} +if ($found.Count -ne 4) { throw 'The actual combined launch must expose all four native dependency/expansion candidates.' } +$starts = @($ast.FindAll({ param($node) $node -is [Management.Automation.Language.CommandAst] -and $node.GetCommandName() -eq 'Start-Process' }, $true)) +if ($starts.Count -ne 1 -or $starts[0].Extent.Text -notmatch '-WindowStyle Hidden') { throw 'Native helper must launch hidden.' } +$checks++ +Write-Output "PASS: $checks launcher parser, metadata, native input and quoting checks. No Java/client/world was started." diff --git a/test-rig/gradle.properties b/test-rig/gradle.properties index f28952b..bc7ffc6 100644 --- a/test-rig/gradle.properties +++ b/test-rig/gradle.properties @@ -1,8 +1,9 @@ org.gradle.jvmargs=-Xmx2G org.gradle.daemon=false -minecraft_version=1.13.2 -forge_version=25.0.223 +minecraft_version=1.14.4 +forge_version=28.2.26 mapping_channel=snapshot -mapping_version=20180921-1.13 -harness_version=1.1.0.113021 +mapping_version=20190719-1.14.3 +mcp_version=20190829.143755 +harness_version=1.1.0.114041 diff --git a/test-rig/migration/audit_native114_endurance.py b/test-rig/migration/audit_native114_endurance.py new file mode 100644 index 0000000..4ec39df --- /dev/null +++ b/test-rig/migration/audit_native114_endurance.py @@ -0,0 +1,57 @@ +"""Reconstruct every endurance verdict from retained raw station checkpoints.""" + +import argparse +from pathlib import Path +from unittest.mock import patch + +import endurance +from matrix import digest, fingerprint, write_json +from native114_endurance import audit +from native114_lifecycle import read_json + + +def replay(root): + result = audit(root) + report = read_json(root / "native114-endurance-results.json") + if fingerprint(Path(report["source"])) != report["source_fingerprint"]: + raise AssertionError("Protected source changed after the run") + for name, expected in report["helper_sha256"].items(): + if digest(Path(name)) != expected: + raise AssertionError("Recorded protocol source changed: " + name) + definitions_path = next(Path(p) for p in report["helper_sha256"] if p.endswith("works-1.13.2.json")) + definitions = {station["id"]: station for station in read_json(definitions_path)["stations"]} + for name in result["profiles"]: + runtime = root / name + rig = endurance.Rig(name, runtime, None, None, definitions) + setup = endurance.parse_snapshot(read_json(runtime / "advantage-works-results/E-02-checkpoint-setup.json")) + battery_pos = rig.pos("E-02", (3, 1, 12)) + rig.static_batteries = setup[battery_pos]["data"].get("Items", []) + ledger = read_json(runtime / "endurance-ledger.json") + for expected in ledger: + label = expected["label"] + for station in endurance.CHECKED: + document = read_json(runtime / "advantage-works-results" / f"{station}-checkpoint-{label}.json") + rig.documents[station] = document + rig.snapshots[station] = endurance.parse_snapshot(document) + # Suppress the oracle's ledger writer only; never change its checks. + with patch.object(endurance, "write_json"): + actual = rig.batch(label) + if actual != expected: + raise AssertionError(name + "/" + label + ": raw checkpoint verdict differs from ledger") + result["profiles"][name]["raw_checkpoint_replays"] = len(ledger) + result["profiles"][name]["station_snapshots_replayed"] = len(ledger) * len(endurance.CHECKED) + result["source_fingerprint_rechecked"] = True + result["protocol_hashes_rechecked"] = True + return result + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--run", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.output.exists(): + raise FileExistsError(args.output) + result = replay(args.run.resolve()) + write_json(args.output, result) + print("PASS: raw endurance checkpoints, inventory acknowledgements, artifact pins and protected sources", flush=True) diff --git a/test-rig/migration/audit_native114_partial.py b/test-rig/migration/audit_native114_partial.py new file mode 100644 index 0000000..3578cf7 --- /dev/null +++ b/test-rig/migration/audit_native114_partial.py @@ -0,0 +1,224 @@ +"""Recompute native broader-network/drill verdicts without trusting run summaries.""" + +import argparse +import json +from pathlib import Path + +from audit_pipe_only import bridge_result +import extended_lifecycle as historical +import pipe_only_lifecycle +from lifecycle import (LifecycleRun, at, changed_target_results, contents, counts, + parse, progress_results) +from matrix import digest, fingerprint, write_json +from native114 import pinned_profile +from native114_lifecycle import require_native_boot, verify_staged + + +def snapshot(runtime, label): + return json.loads((runtime / "advantage-works-results" / ("lifecycle-" + label + ".json")).read_text()) + + +def partial_cases(runtime, definition, cycles): + results = [] + for cycle, group in enumerate(cycles, 1): + label = f"partial-{cycle}-{group}" + samples = [snapshot(runtime, label + f"-sample-{n}") for n in range(8)] + confirmed = snapshot(runtime, label + "-confirmed") + confirmed["confirmed"] = True + results.append(dict(label=label, group=group, cases=historical.partial_result( + definition, snapshot(runtime, label + "-before"), samples + [confirmed], + snapshot(runtime, label + "-loaded"), group))) + return results + + +def order_cases(runtime, definition): + results = [] + for order in (("machine", "supply"), ("supply", "machine")): + label = "order-" + "-".join(order) + results.append(dict(label=label, order=list(order), cases=historical.order_results( + definition, *(snapshot(runtime, label + "-" + phase) for phase in + ("before", "absent", "first", "settled", "loaded")), order))) + return results + + +def check_processing(runtime, definition): + for label, quantity in [("network-commissioned", 1000)] + [ + (f"partial-resumed-{n}", (n + 1) * 1000) for n in range(1, 5)] + [ + ("order-machine-supply-resumed", 5000), ("order-supply-machine-resumed", 5000)]: + state = snapshot(runtime, label) + for f in definition: + if "groups" not in f: + continue + endpoint = at(state, f["id"], f["witnesses"][4]) + if f["id"] == "C-FLUID": + tank = parse(endpoint).get("Tank", {}) + if tank.get("FluidName") != "minecraft:water" or tank.get("Amount") != quantity: + raise AssertionError("Fluid conservation: " + label) + else: + items = counts([endpoint]) + if items["minecraft:iron_ingot"] != 4 or items["minecraft:iron_ore"] != 0 or sum(endpoint["energy"].values()) <= 0: + raise AssertionError("Network processing: " + label) + + +def check_canonical(runtime): + path = runtime / "native114-readiness.json" + records = json.loads(path.read_text()) if path.is_file() else [] + checked = 0 + for record in records: + if "canonical_label" not in record: + continue + source = runtime / "advantage-works-results" / ("lifecycle-" + record["selected_label"] + ".json") + canonical = runtime / "advantage-works-results" / ("lifecycle-" + record["canonical_label"] + ".json") + if source.read_bytes() != canonical.read_bytes(): + raise AssertionError("Canonical snapshot differs from actual raw bytes: " + canonical.name) + checked += 1 + return checked + + +def audit(root): + files = list(root.glob("native114-*-results.json")) + if len(files) != 1: + raise AssertionError("Specify one native campaign root") + report = json.loads(files[0].read_text()) + if not report.get("source_fixture_unchanged") or not report.get("helpers_unchanged"): + raise AssertionError("Protected source/helper changed") + if fingerprint(Path(report["source"])) != report["sourceRuntimeBefore"]: + raise AssertionError("Protected source no longer matches recorded fingerprint") + if digest(Path(report["targetManifest"])) != report["targetManifestSha256"]: + raise AssertionError("Target manifest changed") + target = pinned_profile(Path(report["targetManifest"]), "target114") + if set(report["profiles"]) != {"fresh114", "original110-chained113-native114"}: + raise AssertionError("Incomplete profile set") + definition = json.loads((root / "lifecycle-fixture.json").read_text()) + kind = report["campaign"].removeprefix("halo-").removeprefix("holder-") + expected = (historical.fixtures() if kind in ("extended", "extended-reads") else + pipe_only_lifecycle.fixtures() if kind == "bridge" else + [f for f in historical.fixtures() if "groups" in f]) + if report["campaign"].startswith("halo-"): + from native114_ticket_networks import bridge_fixtures, extended_fixtures, linear_fixtures + expected = {"partial": linear_fixtures, "bridge": bridge_fixtures, "extended": extended_fixtures}[kind]() + if report["campaign"].startswith("holder-"): + from native114_holder_networks import bridge_fixtures, extended_fixtures, linear_fixtures + expected = {"partial": linear_fixtures, "bridge": bridge_fixtures, "extended": extended_fixtures}[kind]() + if definition != json.loads(json.dumps(expected)): + raise AssertionError("Historical fixture geometry differs") + result = dict(evidence_audit="PASS", qualification="PASS", campaign=report["campaign"], profiles={}) + for name, summary in report["profiles"].items(): + runtime = root / name + details = result["profiles"][name] = dict(evidence_audit="PASS", failures=[], machine_qualification="FAIL") + try: + verify_staged(runtime, target) + if json.loads((runtime / "lifecycle-fixture.json").read_text()) != definition: + raise AssertionError("Runtime fixture geometry changed") + details["raw_canonical_pairs"] = check_canonical(runtime) + if report["campaign"].startswith("halo-"): + if not summary.get("native_ticket_protocol_unchanged") or digest( + runtime / "native114-ticket-network-protocol.py") != summary["native_ticket_protocol_sha256"]: + raise AssertionError("Native ticket-control protocol changed") + if report["campaign"].startswith("holder-"): + if not summary.get("holder_protocol_unchanged") or digest(runtime / "native114_holder_networks.py") != summary["holder_protocol_sha256"] \ + or digest(runtime / "native114_ticket_networks.py") != summary["holder_transport_sha256"]: + raise AssertionError("Native holder-control protocol changed") + if kind == "extended": + early = dict(insufficient_energy=historical.reserve_results( + definition, snapshot(runtime, "low-before"), snapshot(runtime, "low-stalled")), + moves=historical.movement_results(definition, snapshot(runtime, "step-4-complete"))) + details["completed_early_drill_groups"] = { + key: {"PASS": sum(c["status"] == "PASS" for c in group.values()), + "FAIL": sum(c["status"] != "PASS" for c in group.values())} + for key, group in early.items()} + details["native_moves"] = len(snapshot(runtime, "step-4-complete")["movements"]) + for key, value in early.items(): + if summary.get(key) != value: + raise AssertionError("Early verdict differs from raw evidence: " + key) + computed = dict( + insufficient_energy=historical.reserve_results(definition, snapshot(runtime, "low-before"), snapshot(runtime, "low-stalled")), + moves=historical.movement_results(definition, snapshot(runtime, "step-4-complete")), + partial_networks=partial_cases(runtime, definition, ("machine", "supply", "supply", "machine")), + reload_orders=order_cases(runtime, definition), + blocked_output=historical.output_results(definition, snapshot(runtime, "blocked-output"), True), + restart=historical.restart_results(definition, snapshot(runtime, "blocked-output"), snapshot(runtime, "blocked-retained")), + blocked_after_restart=historical.output_results(definition, snapshot(runtime, "blocked-retained"), True), + resumed_output=historical.output_results(definition, snapshot(runtime, "freed-output"), False), + redstone=historical.redstone_results(definition, *(snapshot(runtime, label) for label in + ("redstone-before", "redstone-held", "redstone-resumed"))), network_processing="PASS") + check_processing(runtime, definition) + qualification = historical.qualification(computed) + details["native_moves"] = len(snapshot(runtime, "step-4-complete")["movements"]) + details["drill_groups"] = {key: {"PASS": sum(c["status"] == "PASS" for c in computed[key].values()), + "FAIL": sum(c["status"] != "PASS" for c in computed[key].values())} + for key in ("insufficient_energy", "moves", "blocked_output", "restart", + "blocked_after_restart", "resumed_output", "redstone")} + elif kind == "extended-reads": + reader = LifecycleRun(None, None, None, definition) + computed = dict(moves=historical.movement_results(definition, snapshot(runtime, "step-4-complete")), + mining=reader.mining_results(snapshot(runtime, "reads-mining-complete")), + mid_block_progress=dict( + chunk_reload=progress_results(definition, snapshot(runtime, "midblock-before-chunk"), snapshot(runtime, "midblock-after-chunk")), + full_restart=progress_results(definition, snapshot(runtime, "midblock-before-restart"), snapshot(runtime, "midblock-after-restart"))), + saved_read_guards=dict( + legacy_read=progress_results(definition, snapshot(runtime, "save-read-before"), snapshot(runtime, "save-read-legacy")), + changed_target_read=changed_target_results(definition, snapshot(runtime, "save-read-legacy"), snapshot(runtime, "save-read-changed")))) + groups = [computed["moves"], computed["mining"], *computed["mid_block_progress"].values(), + *computed["saved_read_guards"].values()] + qualification = "PASS" if all(c["status"] == "PASS" for group in groups for c in group.values()) else "FAIL" + for f in definition: + if "route" in f and contents(at(snapshot(runtime, "save-read-before"), f["id"], f["to"])) != contents( + at(snapshot(runtime, "save-read-changed"), f["id"], f["to"])): + raise AssertionError("Saved-read item conservation") + if not summary.get("own_protocol_unchanged") or digest(runtime / "native114-extended-reads-protocol.py") != summary["protocolSha256"]: + raise AssertionError("Extra saved-read protocol changed") + details["native_moves"] = len(snapshot(runtime, "step-4-complete")["movements"]) + elif kind in ("partial", "empty-control"): + computed = dict(partial_networks=partial_cases(runtime, definition, ("machine", "supply")), + reload_orders=order_cases(runtime, definition)) + qualification = "PASS" if all(c["status"] == "PASS" for key in computed for row in computed[key] + for c in row["cases"].values()) else "FAIL" + elif kind == "bridge": + computed = dict(cycles=[dict(cycle=cycle, **bridge_result(definition, *(snapshot(runtime, + f"bridge-{cycle}-" + phase) for phase in ("before", "isolated", "confirmed", "loaded", "resumed")), cycle)) + for cycle in range(3)]) + qualification = "PASS" if all(c["status"] == "PASS" for c in computed["cycles"]) else "FAIL" + else: + raise ValueError("Unknown native campaign") + for key, value in computed.items(): + if summary.get(key) != value: + raise AssertionError("Summary differs from unchanged raw oracle: " + key) + if summary["status"] != qualification: + raise AssertionError("Reported machine status differs from raw evidence") + details["machine_qualification"] = qualification + except (AssertionError, FileNotFoundError, KeyError, ValueError) as error: + details.update(evidence_audit="FAIL", failures=[str(error)]) + logs = sorted(runtime.glob("console-*.log")) + details["boots"] = [log.name for log in logs] + details["timing_warnings"] = { + log.name: [line for line in log.read_text(encoding="utf-8", errors="replace").splitlines() + if "Can't keep up!" in line] for log in logs} + for log in logs: + try: + require_native_boot(log) + text = log.read_text(encoding="utf-8", errors="replace") + if "Stopping server" not in text: + raise AssertionError("No graceful stop: " + log.name) + if "An unexpected error occurred trying to execute that command" in text: + raise AssertionError("Native command exception retained: " + log.name) + except AssertionError as error: + details["failures"].append(str(error)) + if details["evidence_audit"] != "PASS": + result["evidence_audit"] = "FAIL" + if details["machine_qualification"] != "PASS" or details["failures"]: + result["qualification"] = "FAIL" + return result + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--run", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.output.exists(): + raise FileExistsError(args.output) + result = audit(args.run) + write_json(args.output, result) + print(json.dumps(result, indent=2)) + raise SystemExit(int(result["evidence_audit"] != "PASS" or result["qualification"] != "PASS")) diff --git a/test-rig/migration/matrix.py b/test-rig/migration/matrix.py index 1e16286..975ece3 100644 --- a/test-rig/migration/matrix.py +++ b/test-rig/migration/matrix.py @@ -257,7 +257,7 @@ def differences(expected, actual, path=""): return result -def compare(source, target): +def compare(source, target, normalize_nbt=normalized): before = json.loads(source.read_text()) after = json.loads(target.read_text()) findings = [] @@ -276,10 +276,10 @@ def enchantment_ids(snapshot): findings.extend(differences(old["properties"], new["properties"], pos + ".state")) if "nbt" in old: counts["tiles"] += 1 - old_nbt = normalized(plain(parse_nbt(old)), old_enchantments) + old_nbt = normalize_nbt(plain(parse_nbt(old)), old_enchantments) counts["items"] += sum(int(v.get("Count", 0)) for v in old_nbt.get("Items", [])) if "nbt" not in new: findings.append({"path": pos + ".tile", "expected": "tile", "actual": ""}) - else: findings.extend(differences(old_nbt, normalized(plain(parse_nbt(new)), new_enchantments), pos + ".nbt")) + else: findings.extend(differences(old_nbt, normalize_nbt(plain(parse_nbt(new)), new_enchantments), pos + ".nbt")) return {"status": "FAIL" if findings else "PASS", "witnesses": counts, "differences": findings} diff --git a/test-rig/migration/native114.py b/test-rig/migration/native114.py new file mode 100644 index 0000000..0173667 --- /dev/null +++ b/test-rig/migration/native114.py @@ -0,0 +1,187 @@ +"""Separate native114 destination pinning and frozen-copy migration witness runner. + +Never builds or launches a source. Historical matrix/connected/actor campaigns remain +separate and their failures are not reclassified by this destination-only runner. +""" + +import argparse +import json +from pathlib import Path +import re +import tomllib +import zipfile + +from matrix import WORLD, compare, copy_fixture, digest, fingerprint, normalized, stage, start_snapshot, write_json + + +RIG = Path(__file__).resolve().parents[1] +LEGACY_POWER = RIG.parents[2] / "PA113/PowerAdvantage" +LAUNCHER = "forge-1.14.4-28.2.26.jar" + + +def native_normalized(value, enchantments=None): + result = normalized(value, enchantments) + # Native 1.14 LockCode.write omits an empty lock. Give only vanilla chest + # roots their canonical unlocked value, retaining strict checks on real locks. + if isinstance(result, dict) and result.get("id") == "minecraft:chest": + result.setdefault("Lock", "") + return result + + +def require_clean_log(path): + text = path.read_text(encoding="utf-8", errors="replace") + failures = [line for line in text.splitlines() if "/ERROR]" in line or any(marker in line for marker in ( + "NoSuchFieldError", "NoSuchMethodError", "Failed to reload data packs", "Encountered an unexpected exception"))] + if failures: + raise AssertionError(f"Native runtime errors in {path}: {failures[:5]}") + + +def output_path(path): + path = path.resolve() + if not path.is_relative_to((RIG / "build").resolve()): + raise ValueError("Native114 outputs must be new paths under test-rig/build") + if path.exists(): + raise FileExistsError(path) + return path + + +def require_disjoint(output, protected): + output, protected = output.resolve(), protected.resolve() + if output.is_relative_to(protected) or protected.is_relative_to(output): + raise ValueError(f"Output overlaps a protected input: {protected}") + + +def jar_identity(path, mod_id, version): + with zipfile.ZipFile(path) as archive: + text = archive.read("META-INF/mods.toml").decode("utf-8") + for fields in tomllib.loads(text).get("mods", []): + if fields.get("modId") == mod_id and fields.get("version") == version: + return + raise ValueError(f"Wrong native mod identity/version: {path}") + + +def pin_target(profile_path, seed, output): + output = output_path(output) + seed = seed.resolve() + require_disjoint(output, seed) + profile = json.loads(profile_path.read_text(encoding="utf-8")) + if (profile.get("minecraftVersion") != "1.14.4" or profile.get("mode") != "packaged" + or profile.get("portStatus") != "PORTED_UNQUALIFIED" + or profile.get("forge", {}).get("version") != "1.14.4-28.2.26"): + raise ValueError("A ported native114 packaged destination profile is required") + if not (seed / LAUNCHER).is_file() or not (seed / "libraries").is_dir(): + raise FileNotFoundError("Supply an installed native Forge 28.2.26 server seed") + jars = [] + for mod in profile["mods"]: + path = (RIG / mod["path"]).resolve() + require_disjoint(output, path) + if not mod["version"].endswith(".114041") or path.name != mod["file"] or "deobf" in path.name: + raise ValueError(f"Not a native114 packaged input: {path}") + jar_identity(path, mod["id"], mod["version"]) + actual = digest(path) + if mod.get("sha256") and actual != mod["sha256"].upper(): + raise AssertionError(f"Destination jar hash mismatch: {path}") + jars.append({"path": str(path), "sha256": actual, "id": mod["id"], "version": mod["version"]}) + if not {"poweradvantage", "advantageworkstest", "orespawn"}.issubset({j["id"] for j in jars}): + raise ValueError("Missing native Power, OreSpawn or harness input") + write_json(output, {"target114": { + "seed": str(seed), "launcher": LAUNCHER, "launcher_sha256": digest(seed / LAUNCHER), + "jars": jars, "qualificationStatus": "UNQUALIFIED", + "legacySourceRoot": str(LEGACY_POWER), + }}) + + +def pinned_profile(manifest_path, key): + entry = json.loads(manifest_path.read_text(encoding="utf-8"))[key] + def resolve(value): + return (manifest_path.parent / value).resolve() + jars = [] + for jar in entry["jars"]: + path = resolve(jar["path"]) + if digest(path) != jar["sha256"].upper(): + raise AssertionError(f"Pinned input changed: {path}") + if key == "target114": + if not jar.get("version", "").endswith(".114041"): + raise ValueError("Destination manifest contains a non-native input") + jar_identity(path, jar["id"], jar["version"]) + jars.append(path) + seed = resolve(entry["seed"]) + if key == "target114": + if entry["launcher"] != LAUNCHER or digest(seed / LAUNCHER) != entry["launcher_sha256"].upper(): + raise AssertionError("Native114 server launcher changed") + identities = [jar["id"] for jar in entry["jars"]] + if len(set(identities)) != len(identities) or not {"poweradvantage", "advantageworkstest", "orespawn"}.issubset(identities): + raise ValueError("Destination must contain unique native Power, OreSpawn and harness inputs") + if not (seed / "libraries").is_dir(): + raise FileNotFoundError("Destination server libraries are missing") + return seed, entry["launcher"], jars + + +def run(args): + source = args.source.resolve() + output = output_path(args.output) + for protected in (source, args.source_manifest, args.target_manifest): + require_disjoint(output, protected) + if not re.fullmatch(r"migration-[A-Za-z0-9_-]+\.json", args.source_snapshot): + raise ValueError("Source snapshot must name an existing snapshot inside the source runtime") + # Explicit archived source manifests resolve against their original PA113 location, + # never against matrix.py's copied PA114 convenience defaults. + pinned_profile(args.source_manifest.resolve(), args.source_key) + target = pinned_profile(args.target_manifest.resolve(), "target114") + require_disjoint(output, target[0]) + before = fingerprint(source) + expected = json.loads(args.source_hashes.read_text(encoding="utf-8")) + if fingerprint(source / WORLD) != expected: + raise AssertionError("Immutable source world does not match its approved fingerprint") + snapshot = source / args.source_snapshot + if not snapshot.is_file() or not (source / "migration-fixture.json").is_file(): + raise FileNotFoundError("Missing retained source snapshot or fixture specification") + try: + launcher = stage(output, target) + copy_fixture(source, output) + results = {} + for label in ("converted", "restarted"): + start_snapshot(output, args.java8, launcher, label) + require_clean_log(output / f"console-{label}.log") + results[label] = compare(snapshot, output / f"migration-{label}.json", native_normalized) + results["idempotence"] = compare(output / "migration-converted.json", output / "migration-restarted.json", native_normalized) + write_json(output / "state-comparison.json", results) + write_json(output / "native114-provenance.json", { + "source": str(source), "sourceManifest": str(args.source_manifest.resolve()), + "sourceManifestSha256": digest(args.source_manifest), "sourceKey": args.source_key, + "sourceSnapshotSha256": digest(snapshot), "sourceHashes": expected, + "targetManifest": str(args.target_manifest.resolve()), "targetManifestSha256": digest(args.target_manifest), + "scope": "frozen-copy-state-and-restart-only", + "platformNormalization": "Only vanilla chest roots treat omitted Lock as the canonical empty string; nonempty locks remain strict.", + }) + return int(any(result["status"] != "PASS" for result in results.values())) + finally: + if fingerprint(source) != before: + raise AssertionError("Protected source runtime changed") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + pin = commands.add_parser("pin-target", help="Pin actual destination jars and an installed server, without opening worlds") + pin.add_argument("--profile", type=Path, default=RIG / "profiles/native114/packaged-current.json") + pin.add_argument("--server-seed", type=Path, required=True) + pin.add_argument("--output", type=Path, required=True) + launch = commands.add_parser("run", help="Convert only a fingerprinted copy; never construct or operate machinery") + launch.add_argument("--source", type=Path, required=True) + launch.add_argument("--source-manifest", type=Path, required=True) + launch.add_argument("--source-key", required=True) + launch.add_argument("--source-hashes", type=Path, required=True) + launch.add_argument("--source-snapshot", default="migration-source.json") + launch.add_argument("--target-manifest", type=Path, required=True) + launch.add_argument("--java8", type=Path, required=True) + launch.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.command == "pin-target": + pin_target(args.profile, args.server_seed, args.output) + return 0 + return run(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native114_actors.py b/test-rig/migration/native114_actors.py new file mode 100644 index 0000000..ee05b0b --- /dev/null +++ b/test-rig/migration/native114_actors.py @@ -0,0 +1,226 @@ +"""Native 1.14 destinations for retained actor/dimension fixtures; no source boots.""" + +import argparse +import copy +import json +from pathlib import Path +import shutil +import socket +import time + +import actors +import dimensions +from matrix import WORLD, Server, copy_fixture, digest, fingerprint, write_json +from native114 import output_path, pinned_profile, require_clean_log, require_disjoint + + +HISTORICAL = ("original110", "early110", "maintained110", "legacy112", "maintained112") +ROUTES = HISTORICAL + ("original110-chained-113", "maintained110-direct-113", "maintained112-direct-113") + + +def baseline(source, kind): + prefix = "dimension-semantic-" if kind == "dimensions" else "semantic-" + label = next((label for label in ("runtime-restart", "restarted", "source") + if (source / (prefix + label + ".json")).is_file()), None) + if label is None: + raise FileNotFoundError(f"No retained {kind} serializer checkpoint in {source}") + capture = dimensions.capture if kind == "dimensions" else actors.capture + return capture(source, label), label + + +def expected_state(value, kind, survival=False, adapter=True): + if kind == "dimensions": + result = dimensions.survival_expected(value) if survival else dimensions.strict_expected(value, adapter) + else: + result = copy.deepcopy(value) + if survival: + for entity in result["entities"].values(): + for key in ("Pos", "Rotation", "Motion"): + entity.pop(key, None) + if not adapter: + result.pop("adapter", None) + result.pop("playerWorldDimension", None) + # Embedded host data is observed at conversion, not rewritten by the server-only player adapter. + return result + + +def retain(runtime, kind, label): + filename = "dimension-fixture.json" if kind == "dimensions" else "actor-fixture.json" + definition = json.loads((runtime / filename).read_text()) + folder = runtime / ("dimension-evidence" if kind == "dimensions" else "actor-evidence") / label + folder.mkdir(parents=True, exist_ok=False) + for source, target in ((runtime / WORLD / "playerdata" / (definition["playerUuid"] + ".dat"), "player.dat"), + (runtime / WORLD / "level.dat", "level.dat")): + shutil.copy2(source, folder / target) + capture = dimensions.capture if kind == "dimensions" else actors.capture + state = capture(runtime, label, folder) + if kind == "dimensions": + state = native_dimension_state(state) + write_json(runtime / (("dimension-semantic-" if kind == "dimensions" else "semantic-") + label + ".json"), state) + return state + + +def native_dimension_state(state): + """1.14 MobEntity drops the redundant Leashed byte, retaining the Leash compound.""" + result = copy.deepcopy(state) + def visit(value): + if isinstance(value, list): + for entry in value: + visit(entry) + elif isinstance(value, dict): + if value.get("id") in ("minecraft:horse", "minecraft:wolf") and "Leashed" not in value: + value["Leashed"] = int(isinstance(value.get("Leash"), dict)) + for entry in list(value.values()): + visit(entry) + visit(result.get("entities", {})) + return result + + +def command(server, runtime, kind, operation, label): + root, complete = ("advdimensions", "ADVDIMENSIONS") if kind == "dimensions" else ("advactors", "ADVACTORS") + server.send(f"{root} {operation} {label}") + server.wait(f"{complete} COMPLETE {label}", 120) + server.send("save-all flush") + server.wait("Saved the", 45) + return retain(runtime, kind, label) + + +def release(server, runtime, kind, label): + if kind == "dimensions": + # Native leash resolution must happen before the first serializer/save. + server.send(f"advdimensions operate {label}") + server.wait(f"ADVDIMENSIONS COMPLETE {label}", 120) + return None + if kind == "actors": + # Native getChunk() loads FULL terrain but does not grant entity-ticking + # tickets. Ticket only the unchanged historical actor fixture rectangle. + server.send("forceload add 96 -32 223 47") + server.send("say NATIVE114-ACTOR-TICKETS-REQUESTED") + server.wait("NATIVE114-ACTOR-TICKETS-REQUESTED", 30) + return command(server, runtime, kind, "operate", label) + + +def raw(runtime, kind, label): + return json.loads((runtime / (("dimensions-" if kind == "dimensions" else "actors-") + label + ".json")).read_text()) + + +def run_route(source, runtime, target, args): + from actors import boot + before = fingerprint(source) + approved = source / "fixture-hashes.json" + if approved.is_file() and fingerprint(source / WORLD) != json.loads(approved.read_text()): + raise AssertionError(f"Protected fixture differs from approved hashes: {source}") + expected, source_label = baseline(source, args.kind) + launcher = boot(runtime, target, args.java8, args.port) + copy_fixture(source, runtime) + fixture_name = "dimension-fixture.json" if args.kind == "dimensions" else "actor-fixture.json" + shutil.copy2(source / fixture_name, runtime / fixture_name) + if args.kind == "dimensions": + definition = json.loads((runtime / fixture_name).read_text()) + definition["nativeOnLoad"] = True + write_json(runtime / fixture_name, definition) + properties = runtime / "server.properties" + properties.write_text(properties.read_text().replace("allow-nether=false", "allow-nether=true")) + write_json(runtime / "native114-source-provenance.json", { + "source": str(source.resolve()), "sourceSnapshot": source_label, + "sourceFingerprint": before, "sourceWorldFingerprint": fingerprint(source / WORLD), + "targetManifest": str(args.target_manifest.resolve()), "targetManifestSha256": digest(args.target_manifest), + "historicalOracle": "Unchanged actor/dimension semantic, exact item, freeze and tick assertions", + }) + verdict = dimensions.verdict if args.kind == "dimensions" else actors.compare + tick = dimensions.tick_result if args.kind == "dimensions" else actors.tick_result + checks = {} + try: + server = Server(runtime, args.java8, launcher, "conversion") + try: + if args.kind == "dimensions": + release(server, runtime, args.kind, "conversion-release") + time.sleep(4) + converted = command(server, runtime, args.kind, "snapshot", "converted") + live = args.kind == "dimensions" + checks["conversion"] = verdict(expected_state(expected, args.kind, survival=live, adapter=False), converted) + checks["conversion-ticks" if live else "conversion-frozen"] = (tick(raw(runtime, args.kind, "conversion-release"), raw(runtime, args.kind, "converted")) + if live else actors.freeze_result(raw(runtime, args.kind, "converted"))) + adapter = command(server, runtime, args.kind, "read", "adapter") + checks["adapter"] = verdict(expected_state(expected, args.kind, survival=live), adapter) + if not live: + checks["adapter-frozen"] = actors.freeze_result(raw(runtime, args.kind, "adapter")) + backups = actors.backup_state(runtime) + finally: + server.close() + require_clean_log(runtime / "console-conversion.log") + server = Server(runtime, args.java8, launcher, "restart") + try: + if args.kind == "dimensions": + release(server, runtime, args.kind, "restart-load-release") + time.sleep(4) + restarted = command(server, runtime, args.kind, "read", "restarted") + checks["restart"] = verdict(expected_state(expected, args.kind, survival=live), restarted) + if not live: + checks["restart-frozen"] = actors.freeze_result(raw(runtime, args.kind, "restarted")) + checks["idempotence"] = verdict(expected_state(adapter, args.kind, survival=live), restarted) + release(server, runtime, args.kind, "released") + time.sleep(4) + operated = command(server, runtime, args.kind, "read", "runtime") + checks["runtime"] = verdict(expected_state(expected, args.kind, survival=True), operated) + checks["runtime-ticks"] = tick(raw(runtime, args.kind, "released"), raw(runtime, args.kind, "runtime")) + finally: + server.close() + require_clean_log(runtime / "console-restart.log") + server = Server(runtime, args.java8, launcher, "runtime-restart") + try: + release(server, runtime, args.kind, "restart-released") + time.sleep(4) + operated_again = command(server, runtime, args.kind, "read", "runtime-restart") + checks["runtime-restart"] = verdict(expected_state(expected, args.kind, survival=True), operated_again) + checks["restart-ticks"] = tick(raw(runtime, args.kind, "restart-released"), raw(runtime, args.kind, "runtime-restart")) + finally: + server.close() + require_clean_log(runtime / "console-runtime-restart.log") + checks["backup-stability"] = {"status": "PASS" if actors.backup_state(runtime) == backups else "FAIL", "backups": backups} + return checks + finally: + write_json(runtime / "native114-comparison.json", checks) + unchanged = fingerprint(source) == before + write_json(runtime / "native114-source-integrity.json", {"status": "PASS" if unchanged else "FAIL", "unchanged": unchanged}) + if not unchanged: + raise AssertionError(f"Protected source changed: {source}") + + +def run(args): + root = output_path(args.output) + require_disjoint(root, args.fixtures) + target = pinned_profile(args.target_manifest.resolve(), "target114") + require_disjoint(root, target[0]) + with socket.socket() as port: + port.bind(("127.0.0.1", args.port)) + root.mkdir(parents=True) + results = {} + for route in args.routes: + if route not in ROUTES: + raise ValueError(f"Unknown retained route: {route}") + source = args.fixtures / route + try: + results[route] = run_route(source, root / route, target, args) + except Exception as error: + results[route] = {"campaign-error": {"status": "FAIL", "error": repr(error)}} + write_json(root / "results.json", results) + raise + write_json(root / "results.json", results) + print(route + ": " + ", ".join(k + "=" + v["status"] for k, v in results[route].items()), flush=True) + status = "FAIL" if any(check["status"] != "PASS" for route in results.values() for check in route.values()) else "PASS" + write_json(root / "summary.json", {"status": status, "kind": args.kind, "routes": args.routes, + "realClientLogin": "NOT_TESTED", "sourceFixtures": "UNCHANGED", "results": results}) + return int(status != "PASS") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--kind", choices=("actors", "dimensions"), required=True) + parser.add_argument("--fixtures", type=Path, required=True) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, choices=range(25584, 25588), required=True) + parser.add_argument("--routes", nargs="+", default=list(ROUTES)) + raise SystemExit(run(parser.parse_args())) diff --git a/test-rig/migration/native114_audit_actors.py b/test-rig/migration/native114_audit_actors.py new file mode 100644 index 0000000..b10dad7 --- /dev/null +++ b/test-rig/migration/native114_audit_actors.py @@ -0,0 +1,94 @@ +"""Read-only replay of native actor/dimension evidence and exact item NBT.""" + +import argparse +import json +from pathlib import Path + +import actors +import dimensions +from matrix import WORLD, digest, fingerprint, write_json +from native114 import pinned_profile, require_clean_log +from native114_actors import baseline, expected_state, native_dimension_state, raw + + +def replay(run, kind): + recorded = json.loads((run / "results.json").read_text()) + findings, routes = [], {} + capture = dimensions.capture if kind == "dimensions" else actors.capture + verdict = dimensions.verdict if kind == "dimensions" else actors.compare + tick = dimensions.tick_result if kind == "dimensions" else actors.tick_result + for route, checks in recorded.items(): + runtime = run / route + provenance = json.loads((runtime / "native114-source-provenance.json").read_text()) + source = Path(provenance["source"]) + if fingerprint(source) != provenance["sourceFingerprint"]: + findings.append(route + ": protected source fingerprint changed") + manifest = Path(provenance["targetManifest"]) + if digest(manifest) != provenance["targetManifestSha256"]: + findings.append(route + ": target manifest changed") + pinned_profile(manifest, "target114") + for jar in json.loads((runtime / "input-jars.json").read_text()): + if digest(runtime / "mods" / jar["file"]) != jar["sha256"]: + findings.append(route + ": staged jar changed: " + jar["file"]) + expected, _ = baseline(source, kind) + states = {} + live = kind == "dimensions" + prefix = "dimension-semantic-" if live else "semantic-" + evidence = "dimension-evidence" if live else "actor-evidence" + for label in ("converted", "adapter", "restarted", "runtime", "runtime-restart"): + state = capture(runtime, label, runtime / evidence / label) + if live: + state = native_dimension_state(state) + stored = json.loads((runtime / (prefix + label + ".json")).read_text()) + if state != stored: + findings.append(route + "/" + label + ": stored semantic differs from raw evidence") + states[label] = state + fresh = { + "conversion": verdict(expected_state(expected, kind, survival=live, adapter=False), states["converted"]), + "adapter": verdict(expected_state(expected, kind, survival=live), states["adapter"]), + "restart": verdict(expected_state(expected, kind, survival=live), states["restarted"]), + "idempotence": verdict(expected_state(states["adapter"], kind, survival=live), states["restarted"]), + "runtime": verdict(expected_state(expected, kind, survival=True), states["runtime"]), + "runtime-restart": verdict(expected_state(expected, kind, survival=True), states["runtime-restart"]), + "runtime-ticks": tick(raw(runtime, kind, "released"), raw(runtime, kind, "runtime")), + "restart-ticks": tick(raw(runtime, kind, "restart-released"), raw(runtime, kind, "runtime-restart")), + } + if live: + fresh["conversion-ticks"] = tick(raw(runtime, kind, "conversion-release"), raw(runtime, kind, "converted")) + else: + for key, label in (("conversion-frozen", "converted"), ("adapter-frozen", "adapter"), ("restart-frozen", "restarted")): + fresh[key] = actors.freeze_result(raw(runtime, kind, label)) + strict_items = {} + for label in ("converted", "adapter", "restarted", "runtime", "runtime-restart"): + strict = expected_state(expected, kind, survival=live or label.startswith("runtime"), adapter=label != "converted") + issues = dimensions.exact_item_differences(strict, states[label]) + strict_items[label] = {"status": "FAIL" if issues else "PASS", "differences": issues} + if issues: + findings.append(route + "/" + label + ": exact nested item NBT mismatch") + for key, value in fresh.items(): + if value != checks[key]: + findings.append(route + "/" + key + ": recomputed oracle differs from recorded result") + if value["status"] != "PASS": + findings.append(route + "/" + key + ": behavioral assertion fails") + if actors.backup_state(runtime) != checks["backup-stability"]["backups"]: + findings.append(route + ": migrated backup bytes changed") + for label in ("conversion", "restart", "runtime-restart"): + require_clean_log(runtime / ("console-" + label + ".log")) + routes[route] = {"oracles": fresh, "exactItems": strict_items, + "carriedItems": sum(item["Count"] for item in expected["file"].get("Inventory", [])), + "enderItems": sum(item["Count"] for item in expected["file"].get("EnderItems", []))} + return {"status": "FAIL" if findings else "PASS", "kind": kind, "routes": routes, "findings": findings} + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--run", type=Path, required=True) + parser.add_argument("--kind", choices=("actors", "dimensions"), required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.output.exists(): + raise FileExistsError(args.output) + result = replay(args.run.resolve(), args.kind) + write_json(args.output, result) + print(result["status"] + ": " + str(len(result["routes"])) + " routes; " + str(len(result["findings"])) + " integrity findings") + raise SystemExit(int(result["status"] != "PASS")) diff --git a/test-rig/migration/native114_audit_order.py b/test-rig/migration/native114_audit_order.py new file mode 100644 index 0000000..914b0d2 --- /dev/null +++ b/test-rig/migration/native114_audit_order.py @@ -0,0 +1,91 @@ +"""Read-only native coremod order, actor, block and backup evidence replay.""" + +import argparse +import json +from pathlib import Path + +import actors +import transformer_order as historical +from matrix import compare, digest, fingerprint, write_json +from native114 import LEGACY_POWER, native_normalized, require_clean_log +from native114_transformer_order import COREMODS, native_order_evidence + + +def replay(root): + results = json.loads((root / "results.json").read_text()) + sources = json.loads((root / "source-fingerprints.json").read_text()) + build = json.loads((root / "probe-build.json").read_text()) + summary = json.loads((root / "summary.json").read_text()) + findings, evidence, modes = [], {}, {} + patched = root / "coremods-order-test.jar" + if digest(patched) != build["patched_sha256"] or digest(patched) != summary["patchedLibrarySha256"]: + findings.append("Patched observer library changed") + for name, entry in sources.items(): + if fingerprint(Path(entry["source"])) != entry["fingerprint"]: + findings.append(name + ": protected source changed") + for case, recorded in results.items(): + name, mode = case.split("/") + runtime = root / (name + "-" + mode) + historical.assert_staged(runtime) + if digest(runtime / COREMODS) != digest(patched): + findings.append(case + ": staged observer library changed") + expected = json.loads((root / (name + "-mixed-input") / "semantic-source.json").read_text()) + actual = {} + proofs = {} + for label in ("converted", "converted-adapter", "restarted"): + actual[label] = actors.capture(runtime, label, runtime / "ordering-evidence" / label) + if label != "converted": + actual[label].pop("embedded", None) + for label in ("converted", "restarted"): + proof = native_order_evidence(runtime, mode, label) + if proof != recorded[("converted" if label == "converted" else "restart") + "-controls"]["order"]: + findings.append(case + "/" + label + ": order proof differs from raw events/dumps") + proofs[label] = proof + require_clean_log(runtime / ("console-" + label + ".log")) + entries = json.loads((runtime / f"migration-{label}.json").read_text()) + stock = runtime / f"migration-stock-{label}.json" + if json.loads(stock.read_text()) != entries[:-len(historical.METAL_BLOCKS)]: + findings.append(case + "/" + label + ": stock snapshot differs from raw full snapshot") + blocks = LEGACY_POWER / "run-migration-matrix" / ( + "original-cyano-20261005-r1/original110" if name == "original110" else "full-20261005-r1/maintained112") + blocks_result = compare(blocks / "migration-source.json", stock, native_normalized) + metals_result = historical.strict_equal(list(historical.METAL_BLOCKS), + [entry["actualName"] for entry in entries[-len(historical.METAL_BLOCKS):]]) + if blocks_result != recorded["blocks-" + label] or blocks_result["status"] != "PASS": + findings.append(case + "/" + label + ": source block/tile comparison differs/fails") + if metals_result != recorded["metals-" + label] or metals_result["status"] != "PASS": + findings.append(case + "/" + label + ": mixed metal comparison differs/fails") + fresh = { + "conversion": actors.compare({k: v for k, v in expected.items() if k != "adapter"}, actual["converted"]), + "adapter": actors.compare({k: v for k, v in expected.items() if k != "embedded"}, actual["converted-adapter"]), + "restart": actors.compare({k: v for k, v in expected.items() if k != "embedded"}, actual["restarted"]), + "idempotence": historical.strict_equal(actual["converted-adapter"], actual["restarted"]), + "backup-provenance": historical.backup_provenance(root / (name + "-mixed-input"), runtime), + } + for key, value in fresh.items(): + if value != recorded[key] or value["status"] != "PASS": + findings.append(case + "/" + key + ": actor or backup replay differs/fails") + modes.setdefault(name, {})[mode] = {"actors": actual["converted-adapter"], "actorsRestart": actual["restarted"], + "blocks": historical.block_semantics(runtime, "converted"), "blocksRestart": historical.block_semantics(runtime, "restarted")} + evidence[case] = {"oracles": fresh, "proofs": proofs} + cross = {} + for name, values in modes.items(): + reference = next(iter(values.values())) + cross[name] = {mode: historical.strict_equal(reference, value) for mode, value in values.items()} + if cross[name] != summary["crossOrder"][name] or any(result["status"] != "PASS" for result in cross[name].values()): + findings.append(name + ": cross-order actor/block/tile semantics differ") + return {"status": "FAIL" if findings else "PASS", "runtimes": len(results), "findings": findings, + "crossOrder": cross, "evidence": evidence, "backupCaveat": "Individual mod backup original-byte custody depends on callback order"} + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--run", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.output.exists(): + raise FileExistsError(args.output) + result = replay(args.run.resolve()) + write_json(args.output, result) + print(result["status"] + ": " + str(result["runtimes"]) + " runtimes; " + str(len(result["findings"])) + " integrity findings") + raise SystemExit(int(result["status"] != "PASS")) diff --git a/test-rig/migration/native114_audit_respawn.py b/test-rig/migration/native114_audit_respawn.py new file mode 100644 index 0000000..ae03a24 --- /dev/null +++ b/test-rig/migration/native114_audit_respawn.py @@ -0,0 +1,74 @@ +"""Read-only replay of native respawn quantities, source items and saved players.""" + +import argparse +import json +from pathlib import Path + +import nbtlib +from actors import enchantments, semantic +from dimensions import PLAYER_STATE, exact_item_differences +from matrix import WORLD, differences, digest, fingerprint, parse_nbt, write_json +from native114 import pinned_profile, require_clean_log +from native114_actors import baseline +from native114_respawn import native_respawn_verdict +from respawn import spawn_reference + + +def replay(root): + results = json.loads((root / "results.json").read_text()) + sources = json.loads((root / "source-fingerprints.json").read_text()) + summary = json.loads((root / "summary.json").read_text()) + if digest(Path(summary["targetManifest"])) != summary["targetManifestSha256"]: + raise AssertionError("Respawn target manifest changed") + pinned_profile(Path(summary["targetManifest"]), "target114") + findings, fresh = [], {} + for route, source_entry in sources.items(): + source = Path(source_entry["path"]) + if fingerprint(source) != source_entry["fingerprint"]: + findings.append(route + ": protected source changed") + runtime = root / route + expected_file = baseline(source, "dimensions")[0]["file"] + expected_items = {key: expected_file[key] for key in ("Inventory", "EnderItems")} + for jar in json.loads((runtime / "input-jars.json").read_text()): + if digest(runtime / "mods" / jar["file"]) != jar["sha256"]: + findings.append(route + ": staged mod changed") + for label in ("respawn", "respawn-restart"): + require_clean_log(runtime / ("console-" + label + ".log")) + for case in (case for case in results if case.startswith(route + "/")): + label = case.split("/")[1] + folder = runtime / "respawn-evidence" / label + record = json.loads((runtime / f"dimensions-{label}.json").read_text()) + registry = enchantments(runtime) + result = native_respawn_verdict(record, registry, spawn_reference(folder / "level.dat")) + before = semantic(parse_nbt(record["respawn"]["before"]), PLAYER_STATE, registry) + source_issues = differences(expected_items, before, "source-inventory") + source_issues.extend(exact_item_differences(expected_items, before, "source-inventory")) + result["differences"].extend(source_issues) + observed = semantic(parse_nbt(record["respawn"]["after"]), PLAYER_STATE, registry) + saved = semantic(nbtlib.load(folder / "player.dat").unpack(), PLAYER_STATE, registry) + save_issues = differences(observed, saved, "saved-player") + exact_item_differences(observed, saved, "saved-player") + result["differences"].extend(save_issues) + if source_issues or save_issues: + result["status"] = "FAIL" + fresh[case] = result + if result != results[case]: + findings.append(case + ": raw oracle result differs from recorded result") + if result["status"] != "PASS": + findings.append(case + ": behavioral assertion fails") + if len(fresh) != summary["cases"] or len(fresh) != len(results): + findings.append("Respawn case coverage is incomplete") + return {"status": "FAIL" if findings else "PASS", "cases": len(fresh), "findings": findings, + "results": fresh, "realClientDeathScreen": "NOT_TESTED"} + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--run", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.output.exists(): + raise FileExistsError(args.output) + result = replay(args.run.resolve()) + write_json(args.output, result) + print(result["status"] + ": " + str(result["cases"]) + " cases; " + str(len(result["findings"])) + " integrity findings") + raise SystemExit(int(result["status"] != "PASS")) diff --git a/test-rig/migration/native114_broad.py b/test-rig/migration/native114_broad.py new file mode 100644 index 0000000..ac6e7e4 --- /dev/null +++ b/test-rig/migration/native114_broad.py @@ -0,0 +1,318 @@ +"""Bounded native114 walkable-bay sweep with initial, movement and restart oracles. + +Production jars come only from a new immutable manifest. Historical station +definitions and failed broad runs remain unchanged. No source world is launched. +""" + +import argparse +from collections import Counter +import json +import math +from pathlib import Path +import shutil +import struct +import time +import traceback + +import nbtlib +from matrix import Server, digest, plain, stage, write_json +from native114 import RIG, output_path, pinned_profile, require_disjoint +from native114_lifecycle import require_native_boot, verify_staged + + +DIRECTIONS = ("down", "up", "north", "south", "west", "east") +REAR = ((0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1), (1, 0, 0), (-1, 0, 0)) +DRILL_BAYS = ("S-05", "E-06") + + +def read_json(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def drill_cases(station): + result = [] + for assertion in station["assertions"]: + if assertion.get("block") not in ("steamadvantage:steam_drill", "electricadvantage:electric_drill"): + continue + direction = assertion.get("meta", DIRECTIONS.index(assertion.get("expectedText", "down"))) + origin = tuple(a + b for a, b in zip(station["origin"], assertion["pos"])) + rear = tuple(a + b for a, b in zip(origin, REAR[direction])) + result.append(dict(id=station["id"] + ":" + assertion["id"], origin=origin, rear=rear, + facing=DIRECTIONS[direction], block=assertion["block"])) + if len(result) != 6 or {r["facing"] for r in result} != set(DIRECTIONS): + raise AssertionError("The original six-facing bay definition changed") + return result + + +def nbt(row): + return plain(nbtlib.parse_nbt(row["nbt"])) if "nbt" in row else {} + + +def drill_state(station, document, *, initial=False): + rows = {tuple(row["pos"]): row for row in document["snapshot"]} + if len(rows) != len(document["snapshot"]): + raise AssertionError("Duplicate snapshot positions") + cases = drill_cases(station) + stock = Counter() + for row in rows.values(): + for stack in nbt(row).get("Items", []): + marker = stack.get("tag", {}).get("AdvBayStock") + if marker: + if stack.get("id") != "minecraft:diamond": + raise AssertionError("Inventory witness item identity changed") + stock[marker] += int(stack["Count"]) + expected_positions, result = set(), {} + for case in cases: + candidates = [rows[pos] for pos in (case["origin"], case["rear"]) + if pos in rows and rows[pos]["block"] == case["block"]] + if len(candidates) != 1: + raise AssertionError(case["id"] + ": missing or duplicate drill in declared cells") + row = candidates[0] + pos = tuple(row["pos"]) + if initial and pos != case["origin"]: + raise AssertionError(case["id"] + ": initial placement moved before activation") + if initial: + track = "steamadvantage:steam_track" if station["id"] == "S-05" else "electricadvantage:electric_track" + if rows.get(case["rear"], {}).get("block") != track: + raise AssertionError(case["id"] + ": initial declared rear track is missing") + if "facing=" + case["facing"] not in row["state"]: + raise AssertionError(case["id"] + ": block facing changed") + data = nbt(row) + if data.get("ForgeData", {}).get("AdvBayWitness") != case["id"]: + raise AssertionError(case["id"] + ": persistent moved tile witness lost/replaced") + if tuple(int(data[axis]) for axis in ("x", "y", "z")) != pos: + raise AssertionError(case["id"] + ": tile coordinates do not match its block") + if stock[case["id"]] != 7: + raise AssertionError(case["id"] + ": inventory stock lost or duplicated") + energy = data.get("Energy", []) + if len(energy) != 1: + raise AssertionError(case["id"] + ": energy witness missing") + value = struct.unpack("!f", struct.pack("!I", int(energy[0]) & 0xffffffff))[0] + capacity = 50 if station["id"] == "S-05" else 500 + if not math.isfinite(value) or not 0 <= value <= capacity: + raise AssertionError(case["id"] + ": invalid energy") + expected_positions.add(pos) + result[case["id"]] = dict(pos=list(pos), facing=case["facing"], block=row["block"], + tile=data["id"], witness=case["id"], stock=7, + energyBits=[int(e) for e in energy], energy=value, + moved=pos == case["rear"]) + actual_positions = {pos for pos, row in rows.items() + if row["block"] in ("steamadvantage:steam_drill", "electricadvantage:electric_drill")} + if actual_positions != expected_positions or set(stock) != {case["id"] for case in cases}: + raise AssertionError("Extra/off-track drill or inventory witness in bay") + return result + + +def operating_state(initial, operated): + if initial.keys() != operated.keys() or len(initial) != 12: + raise AssertionError("Twelve unique drill witnesses are required") + for key in initial: + for field in ("facing", "block", "tile", "witness", "stock"): + if initial[key][field] != operated[key][field]: + raise AssertionError(key + ": movement changed " + field) + # These two original connected lanes are the Q-RIG-001 reproduction. + for key in ("S-05:down-facing", "E-06:up-facing"): + if not operated[key]["moved"]: + raise AssertionError(key + ": the broad run did not exercise actual track movement") + return dict(status="PASS", drills=12, moved=sum(v["moved"] for v in operated.values())) + + +def restarted_state(before, after): + if before != after: + changed = [key for key in set(before) | set(after) if before.get(key) != after.get(key)] + raise AssertionError("Restart changed position, identity, facing, stock or energy: " + str(changed)) + return dict(status="PASS", drills=len(after), exactEnergy=True) + + +def assertions_state(records, stations): + if set(records) != {station["id"] for station in stations}: + raise AssertionError("The broad sweep omitted a bay") + counts = Counter() + manual = [] + for key, document in records.items(): + if document["station"] != key or document["minecraft"] != "1.14.4": + raise AssertionError("Wrong bay/target report") + if document["status"] not in ("PASS", "MANUAL_REQUIRED"): + raise AssertionError(key + ": " + document["status"]) + expected = next(s for s in stations if s["id"] == key) + if (len(document["assertions"]) != len(expected["assertions"]) + or {a["id"] for a in document["assertions"]} != {a["id"] for a in expected["assertions"]}): + raise AssertionError(key + ": assertion set changed") + for assertion in document["assertions"]: + counts[assertion["status"]] += 1 + if assertion["status"] not in ("PASS", "SKIP"): + raise AssertionError(key + "/" + assertion["id"] + ": " + assertion["status"]) + if assertion["status"] == "SKIP": + spec = next(a for a in expected["assertions"] if a["id"] == assertion["id"]) + if not set(spec.get("requiredMods", [])) - set(document["mods"]): + raise AssertionError(key + "/" + assertion["id"] + ": unexplained skip") + if document["manualChecks"]: + manual.append(key) + return dict(status="PASS", assertions=dict(counts), manualRequired=manual) + + +def loaded_mods(records, expected): + for key, record in records.items(): + if record["mods"] != expected: + raise AssertionError(key + ": loaded mod identities/versions differ from the pinned profile") + + +def authorize_broad(runtime): + # matrix.stage is shared with frozen conversion campaigns. A live works + # campaign must explicitly opt out of MigrationProbe's tile-tick freeze. + for name in ("advantage-works.allow", "migration-operate.allow"): + (runtime / name).touch() + + +def configure_broad(runtime, port): + # Match the walkable WorksRunner rather than matrix.stage's migration-only + # defaults. Clearing a freshly generated village can race native POI tasks. + overrides = {"server-port": str(port), "generator-settings": "", "generate-structures": "false", + "level-seed": "advantage-works-1.14.4-v1", "difficulty": "0", "gamemode": "creative", + "allow-flight": "true", "enable-command-block": "true", "force-gamemode": "true", + "spawn-animals": "false", "spawn-monsters": "false", "spawn-npcs": "false", "pvp": "false"} + path = runtime / "server.properties" + lines = [line for line in path.read_text().splitlines() if line.partition("=")[0] not in overrides] + path.write_text("\n".join(lines) + "\n" + "".join(f"{key}={value}\n" for key, value in overrides.items())) + + +class BroadServer(Server): + def command(self, command, token): + self.send(command) + self.wait(token, 150) + + def checkpoint(self, station, label): + self.command(f"advworks checkpoint {station} {label}", "Checkpoint written for " + station) + return read_json(Path(self.log.name).parent / "advantage-works-results" / f"{station}-checkpoint-{label}.json") + + def check_all(self, stations, phase): + self.command("advworks check all", "Advantage Works check:") + directory = Path(self.log.name).parent / "advantage-works-results" + records = {s["id"]: read_json(directory / (s["id"] + "-latest.json")) for s in stations} + write_json(directory / (phase + "-all-stations.json"), records) + return records + + +def run_profile(args, profile, output, name, port, stations): + if name == "packaged-decoupled": + profile = (profile[0], profile[1], [p for p in profile[2] if not p.name.startswith("BaseMetals-")]) + runtime = output / name + launcher = stage(runtime, profile) + configure_broad(runtime, port) + authorize_broad(runtime) + result = dict(status="FAIL", profile=name, port=port, phases={}) + pins = read_json(args.target_manifest)["target114"]["jars"] + expected_mods = {pin["id"]: pin["version"] for pin in pins + if name != "packaged-decoupled" or pin["id"] != "basemetals"} + expected_mods.update(minecraft="1.14.4", forge="28.2.26") + result["expectedMods"] = expected_mods + server = None + try: + verify_staged(runtime, profile) + server = BroadServer(runtime, args.java8, launcher, "first") + server.command("advworks build all", "Advantage Works built 21 station(s)") + # Named finite stock and tile data are commissioning witnesses, not repairs + # performed after operation or restart. Every destination is unchanged later. + for station in stations: + if station["id"] not in DRILL_BAYS: + continue + for case in drill_cases(station): + pos = " ".join(map(str, case["origin"])) + marker = json.dumps(case["id"]) + tag = ('{ForgeData:{AdvBayWitness:' + marker + '},Items:[{Slot:0b,id:"minecraft:diamond",' + 'Count:7b,tag:{AdvBayStock:' + marker + '}}]}') + server.command(f"data merge block {pos} {tag}", "Modified block data of") + initial = {} + for station in stations: + if station["id"] in DRILL_BAYS: + initial.update(drill_state(station, server.checkpoint(station["id"], "initial"), initial=True)) + result["phases"]["initial"] = dict(status="PASS", drills=initial) + write_json(runtime / "broad-results.json", result) + for station in stations: + sid = station["id"] + server.command("advworks start " + sid, "Started " + sid) + time.sleep(max(60, args.run_seconds) if sid == "W-03" else args.run_seconds) + server.checkpoint(sid, "operated") + server.command("advworks stop " + sid, "Stopped " + sid) + first = server.check_all(stations, "first") + loaded_mods(first, expected_mods) + result["phases"]["first"] = assertions_state(first, stations) + before = {} + for station in stations: + if station["id"] in DRILL_BAYS: + before.update(drill_state(station, first[station["id"]])) + result["phases"]["operation"] = operating_state(initial, before) + result["beforeRestart"] = before + server.close() + server = None + require_native_boot(runtime / "console-first.log") + verify_staged(runtime, profile) + server = BroadServer(runtime, args.java8, launcher, "restarted") + # No start, reset, data merge, replacement, or refill on the second boot. + time.sleep(args.run_seconds) + second = server.check_all(stations, "restarted") + loaded_mods(second, expected_mods) + result["phases"]["restarted"] = assertions_state(second, stations) + after = {} + for station in stations: + if station["id"] in DRILL_BAYS: + after.update(drill_state(station, second[station["id"]])) + result["afterRestart"] = after + result["phases"]["persistence"] = restarted_state(before, after) + server.close() + server = None + require_native_boot(runtime / "console-restarted.log") + verify_staged(runtime, profile) + result["status"] = "PASS" + except Exception: + result["error"] = traceback.format_exc() + finally: + if server is not None: + try: + server.close() + except Exception: + result["shutdownError"] = traceback.format_exc() + write_json(runtime / "broad-results.json", result) + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--port", type=int, default=25588) + parser.add_argument("--run-seconds", type=int, default=8) + parser.add_argument("--profile", choices=("both", "packaged-current", "packaged-decoupled"), default="both") + args = parser.parse_args() + if not 1 <= args.run_seconds <= 60 or not 1024 <= args.port < 65535: + parser.error("Bounded run duration and unprivileged consecutive ports required") + output = output_path(args.output) + profile = pinned_profile(args.target_manifest, "target114") + target_pins = read_json(args.target_manifest)["target114"]["jars"] + required = {"poweradvantage", "steamadvantage", "electricadvantage", "advantageworkstest", "basemetals", "orespawn"} + if {pin["id"] for pin in target_pins} != required: + raise AssertionError("Supply the complete current-profile manifest; the runner derives its decoupled subset") + for protected in (args.target_manifest, profile[0], *profile[2]): + require_disjoint(output, protected) + stations_path = RIG / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json" + stations = read_json(stations_path)["stations"] + if len(stations) != 21: + raise AssertionError("The complete 21-bay specification is required") + output.mkdir(parents=True) + shutil.copy2(Path(__file__), output / "runner-source.py") + source_hash = digest(output / "runner-source.py") + names = ("packaged-current", "packaged-decoupled") if args.profile == "both" else (args.profile,) + results = {} + for index, name in enumerate(names): + results[name] = run_profile(args, profile, output, name, args.port + index, stations) + write_json(output / "native114-broad-results.json", dict( + status="PASS" if all(r["status"] == "PASS" for r in results.values()) else "FAIL", + targetManifest=str(args.target_manifest.resolve()), targetManifestSha256=digest(args.target_manifest), + runnerSha256=source_hash, stationDefinitionSha256=digest(stations_path), profiles=results)) + print(name, results[name]["status"], flush=True) + return int(any(r["status"] != "PASS" for r in results.values())) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native114_connected.py b/test-rig/migration/native114_connected.py new file mode 100644 index 0000000..1961d01 --- /dev/null +++ b/test-rig/migration/native114_connected.py @@ -0,0 +1,256 @@ +"""Resume prebuilt, pinned connected source copies in two genuine native114 boots. + +Only staging and process adapters are injected into connected.prepare/resume; +the connected state, stock and operation oracles are reused unchanged. +""" + +import argparse +import json +from pathlib import Path +import re +import socket +from unittest.mock import patch + +import connected +from matrix import WORLD, compare, digest, fingerprint, write_json +from native114 import (LAUNCHER, native_normalized, output_path, pinned_profile, + require_clean_log, require_disjoint) + + +def read_json(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def require_command(command): + if command in ("stop", "save-all", "save-all flush"): + return + if re.fullmatch(r"advconnected snapshot [A-Za-z0-9_-]+", command): + return + match = re.fullmatch(r"advconnected step ([0-9]+) [A-Za-z0-9_-]+", command) + if match and 1 <= int(match[1]) <= 4000: + return + raise AssertionError("Forbidden native connected mutation: " + command) + + +def require_document(document, definition, *, destination): + expected = {entry["id"]: entry for entry in definition["blocks"]} + rows = document["snapshot"] + actual = {entry["id"]: entry for entry in rows} + if len(expected) != len(definition["blocks"]) or len(actual) != len(rows) or actual.keys() != expected.keys(): + raise AssertionError("Connected witness IDs/count changed") + for key, entry in actual.items(): + for field in ("station", "kind", "name", "x", "y", "z"): + if entry[field] != expected[key][field]: + raise AssertionError(f"Connected witness changed: {key}.{field}") + if not document.get("held"): + raise AssertionError("Connected snapshot is not frozen") + if destination and document.get("buildCallsThisProcess") != 0: + raise AssertionError("Destination construction is forbidden") + verdict = connected.state(document) + if verdict["status"] != "PASS": + raise AssertionError("Invalid connected stocks: " + json.dumps(verdict)) + return verdict + + +def require_native_boot(log): + require_clean_log(log) + text = log.read_text(encoding="utf-8", errors="replace") + for marker in ("Starting minecraft server version 1.14.4", + "Forge mod loading, version 28.2.26, for MC 1.14.4 with MCP 20190829.143755"): + if marker not in text: + raise AssertionError("Missing genuine native114 boot proof: " + marker) + + +def verify_source(args): + source = args.source.resolve() + definition = read_json(source / "connected-fixture.json") + if definition.get("format") != 1: + raise ValueError("Unsupported connected fixture format") + if definition.get("sourceProfile") != args.source_key: + raise ValueError("Source fixture profile does not match the explicit source key") + before = fingerprint(source) + expected = read_json(args.source_hashes) + if not expected or fingerprint(source / WORLD) != expected: + raise AssertionError("Protected connected source world fingerprint mismatch") + profile = pinned_profile(args.source_manifest.resolve(), args.source_key) + pins = sorted((jar.name, digest(jar)) for jar in profile[2]) + staged = read_json(source / "input-jars.json") + if sorted((entry["file"], entry["sha256"].upper()) for entry in staged) != pins: + raise AssertionError("Source fixture inputs differ from the explicit archived profile") + if sorted((jar.name, digest(jar)) for jar in (source / "mods").glob("*.jar")) != pins: + raise AssertionError("Source runtime jars differ from the explicit archived profile") + if not re.fullmatch(r"[A-Za-z0-9_-]+", args.source_checkpoint): + raise ValueError("Unsafe source checkpoint") + snapshot = connected.read(source, args.source_checkpoint) + if snapshot["snapshot"] != read_json(source / f"migration-{args.source_checkpoint}.json"): + raise AssertionError("Retained connected and migration checkpoints disagree") + verdict = require_document(snapshot, definition, destination=False) + if snapshot.get("requestedTicks", 0) <= 0 or snapshot.get("advancedTicks") != snapshot["requestedTicks"]: + raise AssertionError("Source checkpoint lacks an exact measured native interval") + if args.source_checkpoint == "source": + source_operation = connected.operation(connected.read(source, "cold"), snapshot, snapshot["requestedTicks"]) + if source_operation["status"] != "PASS": + raise AssertionError("Retained source did not operate: " + json.dumps(source_operation)) + history = source.parent / "connected-results.json" + historical = read_json(history) if history.is_file() else None + if historical is not None and historical.get("sources", {}).get(args.source_key, {}).get("status") != "PASS": + raise AssertionError("Retained source operation did not pass; do not manufacture a replacement") + if fingerprint(source) != before: + raise AssertionError("Source changed during preflight") + return definition, snapshot, verdict, before, historical + + +def prepare_target(runtime, profile, definition, port, source, pins): + if profile[1] != LAUNCHER: + raise ValueError("Only the genuine native114 launcher is permitted") + launcher = connected.prepare(runtime, profile, definition, port, source) + # Existing chunks are copied unchanged. Only the new server's launch settings + # replace the legacy semicolon flat-generator syntax with native defaults. + properties = runtime / "server.properties" + lines = [line for line in properties.read_text().splitlines() if not line.startswith("generator-settings=")] + properties.write_text("\n".join(lines) + "\ngenerator-settings=\n", encoding="utf-8") + for marker in ("connected-build.allow", "connected-built.json"): + if (runtime / marker).exists(): + raise AssertionError("Source construction authority leaked into the destination") + expected = sorted((Path(jar["path"]).name, jar["sha256"].upper()) for jar in pins["jars"]) + actual = sorted((jar.name, digest(jar)) for jar in (runtime / "mods").glob("*.jar")) + if actual != expected: + raise AssertionError("Staged native candidate jars differ from the pinned profile") + if digest(runtime / launcher) != pins["launcher_sha256"].upper(): + raise AssertionError("Staged native launcher differs from the pinned profile") + return launcher + + +class NativeConnectedServer(connected.ConnectedServer): + def __init__(self, runtime, java, launcher, label, on_loaded): + if launcher != LAUNCHER: + raise AssertionError("Refusing a non-native114 process") + self.on_loaded = on_loaded + self.loaded_verified = False + super().__init__(runtime, java, launcher, label) + try: + require_native_boot(Path(self.log.name)) + except BaseException: + self.close() + raise + + def send(self, command): + require_command(command) + if command.startswith("advconnected step ") and not self.loaded_verified: + raise AssertionError("Preservation must pass before any destination operation") + super().send(command) + + def observe(self, operation_name, label, ticks=0): + super().observe(operation_name, label, ticks) + if operation_name == "snapshot" and ticks == 0: + self.on_loaded(label) + self.loaded_verified = True + + +def continued_output(first, last): + initial, final = connected.state(first), connected.state(last) + missing = [key for key, case in initial["cases"].items() + if key != "battery" and case["remaining"] > 0 + and final["cases"][key]["produced"] <= case["produced"]] + return dict(status="FAIL" if missing else "PASS", failures=missing) + + +def run(args): + if not 1 <= args.port <= 65535 or not 1 <= args.ticks <= 4000: + raise ValueError("Port/tick interval outside native command bounds") + output = output_path(args.output) + for protected in (args.source, args.source_manifest, args.source_hashes, args.target_manifest): + require_disjoint(output, protected) + definition, checkpoint, source_state, source_before, historical = verify_source(args) + target = pinned_profile(args.target_manifest.resolve(), "target114") + target_pins = read_json(args.target_manifest)["target114"] + identities = {entry["id"] for entry in target_pins["jars"]} + if not {"steamadvantage", "electricadvantage"}.issubset(identities): + raise ValueError("Connected qualification requires native Steam and Electric candidates") + require_disjoint(output, target[0]) + for jar in target[2]: + require_disjoint(output, jar) + with socket.socket() as listener: + listener.bind(("127.0.0.1", args.port)) + output.mkdir(parents=True, exist_ok=False) + runtime = output / "target114" + report_path = output / "native114-connected-results.json" + report = dict(status="RUNNING", scope="prebuilt-connected-source-to-native114-two-boots", + protocol="No source process; destination snapshot/step/save/stop only; no rebuilding/refilling/resetting", + source=str(args.source.resolve()), sourceKey=args.source_key, + sourceCheckpoint=args.source_checkpoint, sourceState=source_state, + sourceManifest=str(args.source_manifest.resolve()), sourceManifestSha256=digest(args.source_manifest), + sourceHashes=str(args.source_hashes.resolve()), sourceHashesSha256=digest(args.source_hashes), + sourceRuntimeBefore=source_before, sourceWorldExpected=read_json(args.source_hashes), + sourceCheckpointSha256=digest(args.source / f"connected-{args.source_checkpoint}.json"), + targetManifest=str(args.target_manifest.resolve()), targetManifestSha256=digest(args.target_manifest), + targetProfile=target_pins, port=args.port, ticks=args.ticks, + runnerSha256=digest(Path(__file__)), oracleSha256=digest(Path(connected.__file__)), + historicalCampaign=historical, results={}) + write_json(report_path, report) + try: + launcher = prepare_target(runtime, target, definition, args.port, args.source, target_pins) + report["stagedInputs"] = read_json(runtime / "input-jars.json") + for label, previous in (("converted", args.source / f"migration-{args.source_checkpoint}.json"), + ("restarted", runtime / "migration-converted-worked.json")): + def on_loaded(loaded_label): + loaded = connected.read(runtime, loaded_label) + require_document(loaded, definition, destination=True) + if loaded.get("requestedTicks") != 0 or loaded.get("advancedTicks") != 0: + raise AssertionError("Initial preservation requires a frozen zero-step snapshot") + result = compare(previous, runtime / f"migration-{loaded_label}.json", native_normalized) + report["results"][label + "_preservation"] = result + write_json(report_path, report) + if result["status"] != "PASS": + raise AssertionError(label + " native114 preservation failed") + + def boot(current, settings, selected_launcher, selected_label): + if current.resolve() != runtime.resolve(): + raise AssertionError("Only the newly staged target may be launched") + return NativeConnectedServer(current, settings.java8, selected_launcher, selected_label, on_loaded) + + # Dependency injection is confined to the process factory, never an oracle. + with patch.object(connected, "boot", boot): + verdict = connected.resume(runtime, args, launcher, label) + report["results"][label + "_operation"] = verdict + require_document(connected.read(runtime, label + "-worked"), definition, destination=True) + require_native_boot(runtime / f"console-{label}.log") + report["results"][label + "_native_boot"] = dict(status="PASS", launcher=launcher) + write_json(report_path, report) + report["results"]["real_output_across_restarts"] = continued_output( + connected.read(runtime, "converted-loaded"), connected.read(runtime, "restarted-worked")) + report["status"] = "PASS" if all(result["status"] == "PASS" for result in report["results"].values()) else "FAIL" + except BaseException as error: + report.update(status="FAIL", failure=str(error)) + raise + finally: + source_after = fingerprint(args.source) + report["sourceRuntimeAfter"] = source_after + preserved = source_after == source_before + report["sourcePreservation"] = dict(status="PASS" if preserved else "FAIL") + if not preserved: + report.update(status="FAIL", failure="Protected source runtime changed") + write_json(report_path, report) + if not preserved: + raise AssertionError("Protected source runtime changed") + print(report["status"] + ": " + str(report_path), flush=True) + return int(report["status"] != "PASS") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--source-manifest", type=Path, required=True) + parser.add_argument("--source-key", required=True) + parser.add_argument("--source-hashes", type=Path, required=True) + parser.add_argument("--source-checkpoint", default="source") + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, default=25576) + parser.add_argument("--ticks", type=int, default=512) + return run(parser.parse_args()) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native114_endurance.py b/test-rig/migration/native114_endurance.py new file mode 100644 index 0000000..af8a304 --- /dev/null +++ b/test-rig/migration/native114_endurance.py @@ -0,0 +1,243 @@ +"""Measured native114 endurance using unchanged batch and conservation oracles.""" + +import argparse +from collections import Counter +from datetime import datetime, timezone +import json +from pathlib import Path +import shutil +import socket +import time +import traceback + +import endurance +from audit_endurance import expected_inventory_acknowledgements +from matrix import WORLD, compare, digest, fingerprint, write_json +from native114 import native_normalized, output_path, pinned_profile, require_disjoint +from native114_lifecycle import (prepare, read_json, require_native_boot, + verify_source, verify_staged) + + +NAMES = ("fresh114", "original110-chained113-native114") + + +class NativeEnduranceRig(endurance.Rig): + def boot(self, phase): + super().boot(phase) + if phase != "endurance-restart": + return + # Restore only chunk tickets, never station actions or machine NBT. + # Observing a real tick is transport readiness, not a desired output. + for station in endurance.CHECKED: + definition = self.definitions[station] + low = self.pos(station, definition["boundsMin"]) + high = self.pos(station, definition["boundsMax"]) + self.command(f"forceload add {low[0]} {low[2]} {high[0]} {high[2]}") + self.checkpoint("native-restart-immediate") + initial = self.documents["W-01"]["worldTime"] + started = time.monotonic() + for attempt in range(200): + self.checkpoint(f"native-restart-ready-{attempt:03}") + observed = self.documents["W-01"]["worldTime"] + if observed > initial: + write_json(self.runtime / "native-restart-readiness.json", { + "initial_world_time": initial, "selected_world_time": observed, + "selected_attempt": attempt, "elapsed_seconds": time.monotonic() - started, + "selection": "First measured tick advance; no inventory/output inspection", + }) + return + if time.monotonic() - started >= 20: + break + time.sleep(0.01) + raise TimeoutError("No actual native tick after restoring endurance tickets") + + +def audit(root, require_endurance=True): + report = read_json(root / "native114-endurance-results.json") + expected_status = "PASS" if require_endurance else "SMOKE_PASS_NOT_ENDURANCE" + endurance.require(report["status"] == expected_status, "No completed native endurance result") + endurance.require(report["interval_seconds"] == 60, "Not the one-minute batch protocol") + endurance.require(report["duration_seconds"] >= (1800 if require_endurance else 60), "Duration too short") + endurance.require(report["source_fixture_unchanged"] and report["helpers_unchanged"], "Source/protocol changed") + endurance.require(set(report["profiles"]) == set(NAMES), "Missing native environment") + result = dict(status="PASS", scope="endurance" if require_endurance else "smoke-only", profiles={}) + failures = ("Unknown slot", "Could not replace", "Unknown or incomplete command", "Incorrect argument", + "Unknown item", "There is no container") + for name, profile in report["profiles"].items(): + endurance.require(profile["status"] == "PASS" and profile["restart"] == "PASS" + and profile["resumed_processing"] == "PASS", "Incomplete profile") + endurance.require(profile["elapsed_seconds"] >= report["duration_seconds"], "Wall time too short") + endurance.require(profile["game_ticks"] >= report["duration_seconds"] * 18, "Tick rate too low") + runtime = root / name + counts = expected_inventory_acknowledgements(profile["endurance_batches"]) + for filename, count in zip(("console-endurance.log", "console-endurance-restart.log"), counts): + text = (runtime / filename).read_text(encoding="utf-8") + require_native_boot(runtime / filename) + endurance.require(not any(f in text for f in failures), "Native command failure") + endurance.require(text.count("Replaced a slot at") == count, "Inventory command acknowledgements differ") + if "restart" in filename: + endurance.require(not any(f"Started {s} " in text for s in ("S-04", "E-03", "E-02", "S-02", "E-01")), + "Restart replaced source machines") + endurance.require("Advantage Works built" not in text and "Reset 1 station" not in text, + "Restart rebuilt the rig") + pins = {item["file"]: item["sha256"] for item in report["artifacts"]} + actual = {jar.name: digest(jar) for jar in (runtime / "mods").glob("*.jar")} + endurance.require(actual == pins, "Staged native artifacts changed") + ledger = read_json(runtime / "endurance-ledger.json") + batches = [b for b in ledger if b["label"].startswith("cycle-")] + total = report["duration_seconds"] // report["interval_seconds"] + endurance.require(len(batches) == profile["endurance_batches"] == total, "Batch count differs") + endurance.require({b["label"] for b in batches} == {f"cycle-{i:03}" for i in range(1, total + 1)}, + "Repeated or missing batch") + endurance.require([b["label"] for b in ledger[-3:]] == ["before-restart", "after-restart", "restart-resumed"], + "Missing restart checkpoints") + result["profiles"][name] = dict(status="PASS", batches=total, inventory_acknowledgements=list(counts)) + return result + + +def run(args): + endurance.require(args.seconds >= 60 and args.seconds % 60 == 0, "Use whole one-minute batches") + output = output_path(args.output) + for protected in (args.source, args.source_manifest, args.source_hashes, args.target_manifest): + require_disjoint(output, protected) + source_before, history = verify_source(args) + target = pinned_profile(args.target_manifest.resolve(), "target114") + for protected in (target[0], *target[2]): + require_disjoint(output, protected) + for port in (args.port, args.port + 1): + with socket.socket() as listener: + listener.bind(("127.0.0.1", port)) + definition_path = Path(__file__).parents[1] / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json" + definitions = {s["id"]: s for s in read_json(definition_path)["stations"]} + endurance.require(sum(bool(definitions[s].get("exclusive")) for s in endurance.ACTIVE) == 1, + "Exclusive stations interfere") + helpers = [Path(endurance.__file__), Path(__file__), definition_path] + before_helpers = {str(p): digest(p) for p in helpers} + output.mkdir(parents=True, exist_ok=False) + for path in helpers: + destination = output / "protocol-sources" / path.name + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, destination) + report_path = output / "native114-endurance-results.json" + report = dict(status="RUNNING", duration_seconds=args.seconds, interval_seconds=60, + feeding_protocol="IInventory replaceitem and fluid handlers; no inventory NBT writes", + source=str(args.source.resolve()), source_state_gates=history, + source_fingerprint=source_before, helper_sha256=before_helpers, + target_manifest=str(args.target_manifest.resolve()), target_manifest_sha256=digest(args.target_manifest), + artifacts=[dict(file=j.name, sha256=digest(j)) for j in target[2]], + active_stations=endurance.ACTIVE, checked_stations=endurance.CHECKED, profiles={}, + coverage="Rebuilt Works in fresh and migrated-copy worlds; not endurance of original source machinery", + manual_checks="Machine GUIs, weapons, sound and player interaction remain manual") + write_json(report_path, report) + rigs = [] + try: + for index, name in enumerate(NAMES): + runtime = output / name + launcher = prepare(runtime, target, [], args.port + index, args.source if index else None) + for marker in ("migration-operate.allow", "advantage-works.allow"): + (runtime / marker).touch() + rig = NativeEnduranceRig(name, runtime, args.java8, launcher, definitions) + rigs.append(rig) + if index: + rig.boot("frozen-import") + rig.server.snapshot("frozen-import") + rig.command("save-all flush") + rig.shutdown() + preserved = compare(args.source / "migration-restarted.json", runtime / "migration-frozen-import.json", + native_normalized) + write_json(runtime / "native114-import-comparison.json", preserved) + endurance.require(preserved["status"] == "PASS", "Copied source did not preserve frozen state") + require_native_boot(runtime / "console-frozen-import.log") + rig.boot("endurance") + rig.start(build=True) + initial_ticks = {r.name: r.documents["W-01"]["worldTime"] for r in rigs} + started = time.monotonic() + report["started_at_utc"] = datetime.now(timezone.utc).isoformat() + cycles = args.seconds // 60 + for cycle in range(1, cycles + 1): + deadline = started + cycle * 60 + while time.monotonic() < deadline: + for rig in rigs: + endurance.require(rig.server.process.poll() is None, rig.name + ": server died") + time.sleep(min(1, max(0, deadline - time.monotonic()))) + for rig in rigs: + rig.checkpoint(f"cycle-{cycle:03}") + rig.batch(f"cycle-{cycle:03}") + if cycle < cycles: + rig.seed() + report["profiles"][rig.name] = dict(status="RUNNING", completed_batches=len(rig.ledger), + latest_world_time=rig.documents["W-01"]["worldTime"]) + write_json(report_path, report) + print(f"{cycle * 60}s: both native profiles passed batch {cycle}", flush=True) + elapsed = time.monotonic() - started + ending_ticks = {r.name: r.documents["W-01"]["worldTime"] for r in rigs} + for rig in rigs: + endurance.require(ending_ticks[rig.name] - initial_ticks[rig.name] >= args.seconds * 18, + rig.name + ": fewer than 18 game ticks per second") + rig.structure() + rig.checkpoint("before-restart") + rig.batch("before-restart") + rig.command("save-all flush") + rig.shutdown() + rig.boot("endurance-restart") + rig.checkpoint("after-restart") + rig.batch("after-restart") + rig.structure() + rig.seed() + time.sleep(60) + for rig in rigs: + rig.checkpoint("restart-resumed") + rig.batch("restart-resumed") + statuses = rig.structure() + rig.command("save-all flush") + rig.shutdown() + totals = Counter() + for batch in rig.ledger: + if batch["label"].startswith("cycle-"): + totals.update(batch["outputs"]) + report["profiles"][rig.name] = dict(status="PASS", elapsed_seconds=elapsed, + endurance_batches=cycles, total_verified_checkpoints=len(rig.ledger), + station_statuses=statuses, restart="PASS", resumed_processing="PASS", + game_ticks=ending_ticks[rig.name] - initial_ticks[rig.name], + endurance_outputs=dict(totals)) + report["status"] = "PASS" if args.seconds >= 1800 else "SMOKE_PASS_NOT_ENDURANCE" + except BaseException as error: + report.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + raise + finally: + for rig in rigs: + try: + rig.shutdown() + verify_staged(rig.runtime, target) + for log in rig.runtime.glob("console-*.log"): + require_native_boot(log) + except Exception as error: + report.update(status="FAIL", shutdown_or_audit_error=str(error)) + report["source_fixture_unchanged"] = fingerprint(args.source.resolve()) == source_before + report["helpers_unchanged"] = {str(p): digest(p) for p in helpers} == before_helpers + if not report["source_fixture_unchanged"] or not report["helpers_unchanged"]: + report.update(status="FAIL", provenance_error="Protected source or protocol changed") + report["completed_at_utc"] = datetime.now(timezone.utc).isoformat() + write_json(report_path, report) + endurance.require(report["status"] != "FAIL", "See saved native endurance failure") + write_json(output / "qualification-audit.json", audit(output, require_endurance=args.seconds >= 1800)) + print(report["status"] + ": " + str(report_path), flush=True) + return 0 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--source-manifest", type=Path, required=True) + parser.add_argument("--source-key", default="target113") + parser.add_argument("--source-hashes", type=Path, required=True) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--seconds", type=int, default=1800) + parser.add_argument("--port", type=int, default=25590) + return run(parser.parse_args()) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native114_extended.py b/test-rig/migration/native114_extended.py new file mode 100644 index 0000000..685cba8 --- /dev/null +++ b/test-rig/migration/native114_extended.py @@ -0,0 +1,82 @@ +"""Four native moves, partial networks, blocked output, restart and redstone tests.""" + +import time + +import extended_lifecycle as historical +from native114_partial import NativeExtendedRun, campaign, common_parser, orders + + +def exercise(runner, progress): + definition = runner.definition + runner.boot("extended") + runner.build() + runner.probe("trace-loads", "trace-enabled") + result = dict(status="RUNNING") + low = runner.probe("low-prime", "low-before") + runner.release_drills(0) + time.sleep(2) + stalled = runner.probe("snapshot", "low-stalled") + result["insufficient_energy"] = historical.reserve_results(definition, low, stalled) + runner.hold_drills(0) + for step in range(4): + runner.probe("prime", f"prime-step-{step + 1}") + runner.release_drills(step) + moved = runner.wait_step(step + 1) + print(f"{runner.runtime.name}: native drill step {step + 1}/4 observed", flush=True) + result["moves"] = historical.movement_results(definition, moved) + progress(result) + time.sleep(50) + runner.processing("network-commissioned", 1000) + result["partial_networks"] = [] + for cycle, group in enumerate(("machine", "supply", "supply", "machine"), 1): + result["partial_networks"].append(runner.partial(group, cycle)) + runner.replenish() + runner.probe("offer", f"partial-fluid-{cycle}") + time.sleep(50) + runner.processing(f"partial-resumed-{cycle}", (cycle + 1) * 1000) + print(f"{runner.runtime.name}: {group}-only unload {cycle} and processing recorded", flush=True) + progress(result) + result["reload_orders"] = orders(runner, process=True) + runner.install_blocked_output() + time.sleep(45) + blocked = runner.probe("snapshot", "blocked-output") + result["blocked_output"] = historical.output_results(definition, blocked, True) + runner.command("save-all flush") + runner.close() + runner.boot("blocked-restart") + time.sleep(1) + retained = runner.probe("snapshot", "blocked-retained") + result["restart"] = historical.restart_results(definition, blocked, retained) + result["blocked_after_restart"] = historical.output_results(definition, retained, True) + runner.freed_output() + time.sleep(20) + result["resumed_output"] = historical.output_results(definition, runner.probe("snapshot", "freed-output"), False) + for f in definition: + if "route" in f: + runner.target_plane(f, "minecraft:obsidian") + time.sleep(4) + before = runner.probe("snapshot", "redstone-before") + runner.hold_drills(4) + time.sleep(2) + held = runner.probe("snapshot", "redstone-held") + runner.release_drills(4) + time.sleep(4) + after = runner.probe("snapshot", "redstone-resumed") + result["redstone"] = historical.redstone_results(definition, before, held, after) + runner.hold_drills(4) + runner.command("save-all flush") + runner.close() + result["network_processing"] = "PASS" + result["status"] = historical.qualification(result) + progress(result) + return result + + +def main(): + parser = common_parser(__doc__) + args = parser.parse_args() + return campaign(args, "extended", historical.fixtures(), exercise, NativeExtendedRun) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native114_extended_reads.py b/test-rig/migration/native114_extended_reads.py new file mode 100644 index 0000000..0daf730 --- /dev/null +++ b/test-rig/migration/native114_extended_reads.py @@ -0,0 +1,47 @@ +"""All eighteen extended drill routes through mining, reload and saved-read guards.""" + +from pathlib import Path +import shutil +import time + +import extended_lifecycle as historical +from matrix import digest +from native114_partial import campaign, common_parser + + +def exercise(runner, progress): + protocol = Path(__file__) + original_hash = digest(protocol) + archive = runner.runtime / "native114-extended-reads-protocol.py" + shutil.copy2(protocol, archive) + runner.boot("extended-reads") + runner.build() + result = dict(status="RUNNING", protocolSha256=original_hash) + for step in range(4): + runner.probe("prime", f"prime-step-{step + 1}") + runner.release_drills(step) + moved = runner.wait_step(step + 1) + result["moves"] = historical.movement_results(runner.definition, moved) + runner.mine() + time.sleep(45) + result["mining"] = runner.mining_results(runner.probe("snapshot", "reads-mining-complete")) + progress(result) + print(runner.runtime.name + ": four moves and normal mining observed", flush=True) + result["mid_block_progress"] = runner.midblock() + progress(result) + result["saved_read_guards"] = runner.save_read_guards() + runner.command("save-all flush") + runner.close() + groups = [result["moves"], result["mining"], *result["mid_block_progress"].values(), + *result["saved_read_guards"].values()] + result["status"] = "PASS" if all(c["status"] == "PASS" for group in groups for c in group.values()) else "FAIL" + result["own_protocol_unchanged"] = digest(protocol) == original_hash == digest(archive) + if not result["own_protocol_unchanged"]: + result["status"] = "FAIL" + progress(result) + return result + + +if __name__ == "__main__": + args = common_parser(__doc__).parse_args() + raise SystemExit(campaign(args, "extended-reads", historical.fixtures(), exercise)) diff --git a/test-rig/migration/native114_holder_networks.py b/test-rig/migration/native114_holder_networks.py new file mode 100644 index 0000000..347a852 --- /dev/null +++ b/test-rig/migration/native114_holder_networks.py @@ -0,0 +1,85 @@ +"""Real holder-unload fixtures outside Forge's full generation dependency halo.""" + +from pathlib import Path +import shutil + +import extended_lifecycle as historical +from matrix import digest +import native114_extended +from native114_partial import campaign, common_parser, exercise_bridge, exercise_partial +from native114_ticket_networks import NativeTicketExtendedRun, NativeTicketNetworkRun + + +FORCED_LEVEL = 31 +LAST_HOLDER_LEVEL = 44 +HOLDER_RADIUS = LAST_HOLDER_LEVEL - FORCED_LEVEL +ANCHOR_DISTANCE = 32 + + +def holder_halo(fixture, group): + return [[x, z] for x in range(fixture["min"][0] >> 4, (fixture["max"][0] >> 4) + 1) + for z in range(fixture["min"][2] >> 4, (fixture["max"][2] >> 4) + 1) + if any(max(abs(x-a), abs(z-b)) <= HOLDER_RADIUS for a, b in fixture["groups"][group])] + + +def linear_fixtures(): + result = [] + for index, name in enumerate(("C-STEAM", "C-ELECTRIC", "C-FLUID")): + base = 32768 + index * 1024 + origin, sink = [base + 8, 80, 2056], [base + 520, 80, 2056] + pipes = [[x, 80, 2056] for x in range(base + 9, base + 520)] + f = dict(id=name, nativeTicketNetwork=True, min=[base, 72, 2048], max=[base + 543, 90, 2079], + witnesses=[origin, *pipes[:3], sink, *pipes[3:]], pipes=pipes, + groups=dict(supply=[[base >> 4, 128]], machine=[[(base >> 4) + ANCHOR_DISTANCE, 128]])) + for group in ("supply", "machine"): + f["groups"][group + "halo"] = holder_halo(f, group) + result.append(f) + return result + + +def bridge_fixtures(): + result = [] + for index, name in enumerate(("C-STEAM", "C-ELECTRIC", "C-FLUID")): + base = 36864 + index * 1024 + origin, sink = [base + 13, 80, 2056], [base + 9, 80, 2056] + path = [[base + 13, 80, z] for z in range(2057, 2569)] + path += [[base + x, 80, 2568] for x in range(12, 8, -1)] + path += [[base + 9, 80, z] for z in range(2567, 2056, -1)] + f = dict(id=name, nativeTicketNetwork=True, min=[base, 72, 2048], max=[base + 31, 90, 2591], + witnesses=[origin, sink, *path], pipes=path, + groups=dict(bridge=[[base >> 4, 160]], endpoints=[[base >> 4, 128]])) + f["groups"]["bridgehalo"] = holder_halo(f, "bridge") + result.append(f) + return result + + +def extended_fixtures(): + return [f for f in historical.fixtures() if "route" in f] + linear_fixtures() + + +def main(): + parser = common_parser(__doc__) + parser.add_argument("--campaign", choices=("partial", "bridge", "extended"), required=True) + args = parser.parse_args() + definitions = {"partial": linear_fixtures, "bridge": bridge_fixtures, "extended": extended_fixtures} + operations = {"partial": exercise_partial, "bridge": exercise_bridge, "extended": native114_extended.exercise} + runner_type = NativeTicketNetworkRun if args.campaign == "bridge" else NativeTicketExtendedRun + own, own_hash = Path(__file__), digest(Path(__file__)) + transport = Path(__file__).with_name("native114_ticket_networks.py") + transport_hash = digest(transport) + def exercise(runner, progress): + shutil.copy2(own, runner.runtime / own.name) + shutil.copy2(transport, runner.runtime / transport.name) + result = operations[args.campaign](runner, progress) + result.update(holder_protocol_sha256=own_hash, holder_transport_sha256=transport_hash, + holder_protocol_unchanged=(digest(own) == own_hash == digest(runner.runtime / own.name) + and digest(transport) == transport_hash == digest(runner.runtime / transport.name))) + if not result["holder_protocol_unchanged"]: + result["status"] = "FAIL" + progress(result) + return result + return campaign(args, "holder-" + args.campaign, definitions[args.campaign](), exercise, runner_type) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native114_interop.py b/test-rig/migration/native114_interop.py new file mode 100644 index 0000000..17206bd --- /dev/null +++ b/test-rig/migration/native114_interop.py @@ -0,0 +1,72 @@ +"""Native capability, portable loot and locked automation witnesses on disposable servers.""" + +import argparse +from datetime import datetime, timezone +from pathlib import Path +import traceback + +from matrix import Server, digest, write_json +from native114 import output_path, pinned_profile, require_disjoint +from native114_lifecycle import prepare, read_json, require_native_boot, verify_staged + + +def require_checks(document, phase): + expected = 20 if phase == "exercise" else 2 + if document.get("phase") != phase or len(document.get("checks", [])) != expected: + raise AssertionError("Incomplete native interop witness set") + if len({row["id"] for row in document["checks"]}) != expected: + raise AssertionError("Duplicate native interop witnesses") + failed = [row for row in document["checks"] if row.get("status") != "PASS"] + if failed or document.get("status") != "PASS": + raise AssertionError("Native interop defects: " + str(failed)) + + +def run(args): + output = output_path(args.output) + require_disjoint(output, args.target_manifest) + target = pinned_profile(args.target_manifest.resolve(), "target114") + for protected in (target[0], *target[2]): + require_disjoint(output, protected) + launcher = prepare(output, target, [], args.port) + (output / "advantage-native-interop.allow").touch() + report = dict(status="RUNNING", started_at_utc=datetime.now(timezone.utc).isoformat(), phases={}, + target_manifest=str(args.target_manifest.resolve()), target_manifest_sha256=digest(args.target_manifest), + scope="Live Forge capability contracts, both conversion directions, native tank loot, locked chest automation and full restart", + limitation="No third-party mod-specific energy implementation or real client input is claimed") + server = None + try: + for phase in ("exercise", "restored"): + server = Server(output, args.java8, launcher, phase) + server.send("forceload add 1023 1023 1100 1030") + server.send("advinterop " + phase) + server.wait("ADVINTEROP COMPLETE " + phase, 120) + report["phases"][phase] = read_json(output / "advantage-works-results" / f"native-interop-{phase}.json") + server.send("save-all flush") + server.close() + server = None + require_native_boot(output / f"console-{phase}.log") + for phase, document in report["phases"].items(): + require_checks(document, phase) + verify_staged(output, target) + report["status"] = "PASS" + except Exception as error: + report.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + finally: + if server: + try: + server.close() + except Exception as error: + report.update(status="FAIL", shutdown_error=str(error)) + report["completed_at_utc"] = datetime.now(timezone.utc).isoformat() + write_json(output / "native114-interop-results.json", report) + print(report["status"] + ": " + str(output / "native114-interop-results.json"), flush=True) + return int(report["status"] != "PASS") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, default=25592) + raise SystemExit(run(parser.parse_args())) diff --git a/test-rig/migration/native114_lifecycle.py b/test-rig/migration/native114_lifecycle.py new file mode 100644 index 0000000..480a00a --- /dev/null +++ b/test-rig/migration/native114_lifecycle.py @@ -0,0 +1,537 @@ +"""Run unchanged lifecycle fixtures/oracles in fresh and retained-copy native114 worlds. + +Requires Python 3.11+, migration/requirements.txt, an explicit archived source +manifest and a hash-pinned native target manifest. Never launches a source. +""" + +import argparse +from datetime import datetime, timezone +import json +from pathlib import Path +import socket +import shutil +import time +import traceback + +import lifecycle +from matrix import WORLD, compare, copy_fixture, digest, fingerprint, stage, write_json +from native114 import (LAUNCHER, native_normalized, output_path, pinned_profile, + require_clean_log, require_disjoint) + + +def read_json(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def portable_hashes(values): + return {key.replace("\\", "/"): value.upper() for key, value in values.items()} + + +def require_equal_hashes(actual, expected, detail): + if not expected or portable_hashes(actual) != portable_hashes(expected): + raise AssertionError(detail) + + +def verify_source(args): + source = args.source.resolve() + before = fingerprint(source) + require_equal_hashes(fingerprint(source / WORLD), read_json(args.source_hashes), + "Protected retained13 source world fingerprint mismatch") + profile = pinned_profile(args.source_manifest.resolve(), args.source_key) + if "1.13.2" not in profile[1]: + raise ValueError("The retained source must use its archived 1.13.2 profile") + expected = sorted((jar.name, digest(jar)) for jar in profile[2]) + staged = sorted((entry["file"], entry["sha256"].upper()) + for entry in read_json(source / "input-jars.json")) + actual = sorted((jar.name, digest(jar)) for jar in (source / "mods").glob("*.jar")) + if staged != expected or actual != expected: + raise AssertionError("Retained source jars differ from the explicit archived profile") + history = read_json(source / "state-comparison.json") + for gate in ("conversion", "restart", "idempotence"): + if history.get(gate, {}).get("status") != "PASS": + raise AssertionError("Retained source state gate did not pass: " + gate) + for name in ("migration-fixture.json", "migration-restarted.json"): + if not (source / name).is_file(): + raise FileNotFoundError(source / name) + if fingerprint(source) != before: + raise AssertionError("Retained source changed during preflight") + return before, history + + +def require_native_boot(path): + require_clean_log(path) + text = path.read_text(encoding="utf-8", errors="replace") + for marker in ("Starting minecraft server version 1.14.4", + "Forge mod loading, version 28.2.26, for MC 1.14.4 with MCP 20190829.143755"): + if marker not in text: + raise AssertionError("Missing genuine native114 boot proof: " + marker) + # Forge 28.2.26 ForgeConfig.onFileChange logs this exact notice at FATAL + # without throwing. No other fatal message is exempted from this adapter gate. + reload_notice = "[ne.mi.co.ForgeConfig/CORE]: Forge config just got changed on the file system!" + if any("/FATAL]" in line and not line.endswith(reload_notice) for line in text.splitlines()): + raise AssertionError("Fatal native runtime error in " + str(path)) + + +def verify_staged(runtime, profile): + expected = sorted((jar.name, digest(jar)) for jar in profile[2]) + actual = sorted((jar.name, digest(jar)) for jar in (runtime / "mods").glob("*.jar")) + recorded = sorted((entry["file"], entry["sha256"].upper()) + for entry in read_json(runtime / "input-jars.json")) + if actual != expected or recorded != expected: + raise AssertionError("Staged native jar set changed: " + str(runtime)) + if digest(runtime / LAUNCHER) != digest(profile[0] / LAUNCHER): + raise AssertionError("Staged native launcher changed") + + +def prepare(runtime, profile, definition, port, source=None): + if profile[1] != LAUNCHER: + raise ValueError("Refusing a non-native114 lifecycle launcher") + launcher = stage(runtime, profile) + if source is not None: + copy_fixture(source, runtime) + properties = runtime / "server.properties" + lines = [line for line in properties.read_text().splitlines() + if not line.startswith(("server-port=", "generator-settings="))] + properties.write_text("\n".join(lines) + f"\nserver-port={port}\ngenerator-settings=\n", encoding="utf-8") + write_json(runtime / "lifecycle-fixture.json", definition) + verify_staged(runtime, profile) + return launcher + + +def authorize_lifecycle(runtime): + for marker in ("migration-operate.allow", "advantage-works.allow", "advantage-lifecycle.allow"): + (runtime / marker).touch() + + +def require_groups(definition, groups): + expected = {fixture["id"] for fixture in definition if "from" in fixture} + if len(expected) != 12: + raise AssertionError("Lifecycle requires the original twelve drill cases") + return len(groups) == 7 and all(set(group) == expected and all(row.get("status") == "PASS" for row in group.values()) + for group in groups) + + +READINESS_SECONDS = 20 +READINESS_TICKS = 160 +READINESS_SAMPLES = 200 +DEFERRED_TICKS = 1 + + +def required_chunks(definition): + return {(x, z) for fixture in definition + for x in range(fixture["min"][0] >> 4, (fixture["max"][0] >> 4) + 1) + for z in range(fixture["min"][2] >> 4, (fixture["max"][2] >> 4) + 1)} + + +def loaded_issues(snapshot, definition): + issues = [] + chunks = snapshot.get("chunks", []) + if len(chunks) != len(required_chunks(definition)) or {tuple(c["pos"]) for c in chunks} != required_chunks(definition): + issues.append("fixture chunk witness set differs") + if any(not chunk.get("loaded") for chunk in chunks): + issues.append("fixture chunks unloaded") + for fixture in definition: + rows = snapshot.get("cases", {}).get(fixture["id"], []) + if len(rows) != len(fixture["witnesses"]) or {tuple(row["pos"]) for row in rows} != {tuple(p) for p in fixture["witnesses"]}: + issues.append(fixture["id"] + ": witness set differs") + if any(not row.get("loaded") for row in rows): + issues.append(fixture["id"] + ": witnesses unloaded") + return issues + + +def load_event_issues(before, after): + old = {tuple(chunk["pos"]): chunk for chunk in before["chunks"]} + events = {tuple(event["pos"]) for event in after.get("chunkEvents", []) if event["operation"] == "load"} + issues = [] + for chunk in after["chunks"]: + pos = tuple(chunk["pos"]) + previous = old.get(pos) + if (previous is None or pos not in events or chunk["loads"] <= 0 + or (not previous["loaded"] and chunk["loads"] <= previous["loads"])): + issues.append("missing actual Chunk.Load: " + str(pos)) + return issues + + +def unload_event_issues(before, after): + old = {tuple(chunk["pos"]): chunk for chunk in before["chunks"]} + current = {tuple(chunk["pos"]): chunk for chunk in after["chunks"]} + if not old or old.keys() != current.keys() or len(current) != len(after["chunks"]): + return ["unload chunk witness set differs"] + events = {tuple(event["pos"]) for event in after.get("chunkEvents", []) if event["operation"] == "unload"} + return ["missing actual Chunk.Unload: " + str(pos) for pos, chunk in current.items() + if pos not in events or chunk["unloads"] <= old[pos]["unloads"]] + + +class NativeLifecycleRun(lifecycle.LifecycleRun): + """Native transport readiness only; inherited fixtures, operations and oracles.""" + + def __init__(self, *args): + super().__init__(*args) + self.observations = [] + self._unload_context = None + + def _raw_probe(self, operation, label): + path = self.runtime / "advantage-works-results" / f"lifecycle-{label}.json" + if path.exists(): + raise FileExistsError("Refusing to overwrite native observation: " + str(path)) + return super().probe(operation, label) + + def _record(self): + write_json(self.runtime / "native114-readiness.json", self.observations) + + def _wait_ready(self, label, initial, reload_before=None): + started = time.monotonic() + record = dict(label=label, status="PENDING", min_real_ticks=DEFERRED_TICKS, + max_seconds=READINESS_SECONDS, max_ticks=READINESS_TICKS, + initial_world_time=initial["worldTime"], samples=[]) + self.observations.append(record) + loaded_at = None + snapshot = initial + try: + for attempt in range(READINESS_SAMPLES): + issues = loaded_issues(snapshot, self.definition) + issues += load_event_issues(reload_before if reload_before is not None else initial, snapshot) + now = snapshot["worldTime"] + if now < initial["worldTime"]: + raise AssertionError("Native worldTime moved backwards") + loaded_at = None if issues else (now if loaded_at is None else loaded_at) + sample = dict(label=label + ("-initial" if attempt == 0 else f"-pending-{attempt:03}"), + world_time=now, elapsed_seconds=time.monotonic() - started, issues=issues, + loaded_since_world_time=loaded_at) + record["samples"].append(sample) + if sample["elapsed_seconds"] >= READINESS_SECONDS or now - initial["worldTime"] >= READINESS_TICKS: + break + ready = loaded_at is not None and now - loaded_at >= DEFERRED_TICKS + if ready: + record.update(status="PASS", first_ready=sample, selected_label=sample["label"]) + self._record() + return snapshot + self._record() + if attempt + 1 == READINESS_SAMPLES: + break + # Yield between observations; elapsed sleep is never evidence of a native tick. + time.sleep(0.01) + snapshot = self._raw_probe("snapshot", label + f"-pending-{attempt + 1:03}") + raise TimeoutError("Native readiness did not observe loaded witnesses and actual ticks: " + label) + except Exception as error: + record.update(status="FAIL", failure=str(error)) + self._record() + raise + + def boot(self, label): + super().boot(label) + if label not in ("lifecycle-restart", "midblock-restart"): + return + require_native_boot(self.runtime / f"console-{label}.log") + before = self._raw_probe("snapshot", label + "-native-unrestored") + loaded = self._raw_probe("load", label + "-native-initial") + self._wait_ready(label + "-native", loaded, reload_before=before) + self.observations[-1]["reload_before_label"] = label + "-native-unrestored" + self._record() + + def unload_reload(self, name): + self._unload_context = dict(name=name, before=None, deadline=None) + try: + return super().unload_reload(name) + finally: + self._unload_context = None + + def _unload_observation(self, label): + context = self._unload_context + selected_label = label + "-native-initial" + snapshot = self._raw_probe("snapshot", selected_label) + record = dict(label=label, phase="native-unload-callback-readiness", samples=[]) + self.observations.append(record) + # Forge 1.14 queues Unload after removing the holder. Stay within the + # inherited 45-second budget; never fabricate flags or callback counters. + for attempt in range(45): + pending = unload_event_issues(context["before"], snapshot) + absent = all(not chunk["loaded"] for chunk in snapshot["chunks"]) + record["samples"].append(dict(label=selected_label, world_time=snapshot["worldTime"], + all_absent=absent, pending_events=pending)) + if not absent or not pending: + break + remaining = context["deadline"] - time.monotonic() + if remaining <= 0 or attempt == 44: + break + self._record() + time.sleep(min(1, remaining)) + selected_label = label + f"-native-pending-{attempt + 1:02}" + snapshot = self._raw_probe("snapshot", selected_label) + source = self.runtime / "advantage-works-results" / f"lifecycle-{selected_label}.json" + canonical = self.runtime / "advantage-works-results" / f"lifecycle-{label}.json" + if canonical.exists(): + raise FileExistsError(canonical) + shutil.copy2(source, canonical) + record.update(selected_label=selected_label, canonical_label=label, + status="READY" if absent and not pending else "PENDING") + self._record() + return snapshot + + def probe(self, operation, label): + context = self._unload_context + if context is not None: + prefix = context["name"] + "-absent-" + if operation == "snapshot" and label.startswith(prefix) and label[len(prefix):].isdigit(): + return self._unload_observation(label) + if operation == "snapshot" and label == context["name"] + "-before": + result = self._raw_probe(operation, label) + context["before"] = result + return result + if operation == "release" and label == context["name"] + "-released": + result = self._raw_probe(operation, label) + context["deadline"] = time.monotonic() + 45 + return result + if operation == "snapshot" and label == context["name"] + "-absent-confirmed": + result = self._raw_probe(operation, label) + context["absent"] = result + return result + if operation == "load" and label == context["name"] + "-loaded": + initial = self._raw_probe(operation, label + "-native-initial") + selected = self._wait_ready(label + "-native", initial, reload_before=context["absent"]) + source = self.runtime / "advantage-works-results" / ( + "lifecycle-" + self.observations[-1]["selected_label"] + ".json") + canonical = self.runtime / "advantage-works-results" / f"lifecycle-{label}.json" + if canonical.exists(): + raise FileExistsError(canonical) + shutil.copy2(source, canonical) + return selected + if operation not in ("legacy-read", "changed-target-read"): + return self._raw_probe(operation, label) + before = self._raw_probe("snapshot", label + "-native-before") + issues = loaded_issues(before, self.definition) + load_event_issues(before, before) + if issues: + raise AssertionError("Saved-read prerequisite is not loaded: " + "; ".join(issues)) + immediate = self._raw_probe(operation, label + "-native-initial") + selected = self._wait_ready(label + "-native", immediate) + record = self.observations[-1] + # Select by loaded states and real ticks, never by the expected oracle verdict. + selected_path = self.runtime / "advantage-works-results" / ("lifecycle-" + record["selected_label"] + ".json") + canonical = self.runtime / "advantage-works-results" / ("lifecycle-" + label + ".json") + if canonical.exists(): + raise FileExistsError(canonical) + shutil.copy2(selected_path, canonical) + record.update(operation=operation, canonical_label=label) + self._record() + return selected + + +def exercise(runner, label, progress, trace_loads=False): + """Original sequence/fixtures/oracles, with native observation readiness gates.""" + definition = runner.definition + runner.boot("lifecycle") + require_native_boot(runner.runtime / "console-lifecycle.log") + if trace_loads: + runner.probe("trace-loads", "tracing-loads") + runner.build() + moved = runner.wait_moves() + movements = {movement["id"]: movement for movement in moved["movements"]} + if len(movements) != len(moved["movements"]): + raise AssertionError("Duplicate movement witnesses") + movement_results = {fixture["id"]: lifecycle.move_result(fixture, movements[fixture["id"]]) + for fixture in definition if "from" in fixture} + result = dict(status="RUNNING", move_cases=movement_results, chunk_cycles=0) + progress(result) + print(f"{label}: twelve finite-energy moves observed", flush=True) + time.sleep(60) + runner.processing("commissioned", 1000) + for cycle in range(1, 4): + runner.unload_reload(f"chunk-cycle-{cycle}") + runner.replenish() + runner.probe("offer", f"fluid-offer-{cycle}") + time.sleep(60) + runner.processing(f"chunk-resumed-{cycle}", (cycle + 1) * 1000) + result["chunk_cycles"] = cycle + progress(result) + print(f"{label}: real chunk unload/reload {cycle} and resumed processing PASS", flush=True) + before = runner.probe("snapshot", "before-restart") + runner.command("save-all flush") + runner.close() + runner.boot("lifecycle-restart") + require_native_boot(runner.runtime / "console-lifecycle-restart.log") + time.sleep(1) + after = runner.probe("snapshot", "after-restart") + for fixture in definition: + if "to" not in fixture: + continue + old = lifecycle.at(before, fixture["id"], fixture["to"]) + new = lifecycle.at(after, fixture["id"], fixture["to"]) + if not (lifecycle.contents(old) == lifecycle.contents(new) and old["block"] == new["block"] + and "facing=" + fixture["facing"] in new["state"]): + raise AssertionError("Moved drill changed across full restart: " + fixture["id"]) + runner.processing("restart-retained", 4000) + runner.replenish() + runner.probe("offer", "restart-fluid-offer") + runner.mine() + time.sleep(60) + mined = runner.processing("restart-resumed", 5000) + mining_results = runner.mining_results(mined) + runner.unload_reload("mining-chunk-reload") + time.sleep(5) + mining_after = runner.mining_results(runner.probe("snapshot", "mining-retained")) + result.update(mining=mining_results, mining_after_chunk_reload=mining_after) + progress(result) + midblock_results = runner.midblock() + result["mid_block_progress"] = midblock_results + progress(result) + save_read_results = runner.save_read_guards() + failed_changed = [name for name, row in save_read_results["changed_target_read"].items() if row["status"] != "PASS"] + if failed_changed: + result["changed_target_observation_gap"] = dict(cases=failed_changed, detail= + "Strict oracle failed on the first loaded snapshot after an actual tick. Immediate/pending phases retained for retarget-race diagnosis; no verdict-based resampling.") + runner.command("save-all flush") + runner.close() + groups = (movement_results, mining_results, mining_after, midblock_results["chunk_reload"], + midblock_results["full_restart"], save_read_results["legacy_read"], + save_read_results["changed_target_read"]) + result.update(status="PASS" if require_groups(definition, groups) else "FAIL", chunk_cycles=5, + full_restart="PASS", network_processing="PASS", fluid_mB=5000, + mining=mining_results, mining_after_chunk_reload=mining_after, + mid_block_progress=midblock_results, save_read_guards=save_read_results) + progress(result) + print(f"{label}: {result['status']}; movement, mining, chunk and restart oracles retained", flush=True) + return result + + +def frozen_import(runner, source): + # This is a separate frozen boot, before construction authority or live ticks. + runner.boot("retained13-import") + try: + require_native_boot(runner.runtime / "console-retained13-import.log") + runner.server.snapshot("retained13-import") + runner.command("save-all flush") + time.sleep(2) + finally: + runner.close() + result = compare(source / "migration-restarted.json", runner.runtime / "migration-retained13-import.json", + native_normalized) + write_json(runner.runtime / "native114-import-comparison.json", result) + if result["status"] != "PASS": + raise AssertionError("Frozen retained13-to-native114 preservation failed") + return result + + +def run(args): + if not 1 <= args.port <= 65535 or not args.java8.is_file(): + raise ValueError("Supply an available TCP port and an existing Java 8 executable") + output = output_path(args.output) + for protected in (args.source, args.source_manifest, args.source_hashes, args.target_manifest): + require_disjoint(output, protected) + source_before, history = verify_source(args) + target = pinned_profile(args.target_manifest.resolve(), "target114") + identities = {entry["id"] for entry in read_json(args.target_manifest)["target114"]["jars"]} + if not {"steamadvantage", "electricadvantage", "basemetals"}.issubset(identities): + raise ValueError("Full lifecycle requires native Steam, Electric and Base Metals") + for protected in (target[0], *target[2]): + require_disjoint(output, protected) + with socket.socket() as listener: + listener.bind(("127.0.0.1", args.port)) + definition = lifecycle.fixtures() + helper_before = {path.name: digest(path) for path in (Path(lifecycle.__file__), Path(__file__))} + output.mkdir(parents=True, exist_ok=False) + report_path = output / "native114-lifecycle-results.json" + report = dict(status="RUNNING", started_at_utc=datetime.now(timezone.utc).isoformat(), + scope="independent-native114-movement-aware-lifecycle", + protocol="Inherited LifecycleRun operations, unchanged fixtures/oracles; bounded native loaded/tick readiness; genuine chunk events; no source process", + source=str(args.source.resolve()), sourceKey=args.source_key, + sourceManifest=str(args.source_manifest.resolve()), sourceManifestSha256=digest(args.source_manifest), + sourceHashes=str(args.source_hashes.resolve()), sourceHashesSha256=digest(args.source_hashes), + sourceRuntimeBefore=source_before, sourceWorldExpected=read_json(args.source_hashes), + sourceStateGates=history, targetManifest=str(args.target_manifest.resolve()), + targetManifestSha256=digest(args.target_manifest), targetProfile=read_json(args.target_manifest)["target114"], + helperSha256=helper_before, port=args.port, profiles={}) + report["load_stack_tracing"] = getattr(args, "trace_loads", False) + for path in (Path(lifecycle.__file__), Path(__file__)): + archive = output / "protocol-sources" / path.name + archive.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, archive) + write_json(output / "lifecycle-fixture.json", definition) + write_json(report_path, report) + runner = None + try: + for name, source in (("fresh114", None), ("original110-chained113-native114", args.source)): + runner = None + result = dict(status="RUNNING") + report["profiles"][name] = result + write_json(report_path, report) + try: + runtime = output / name + launcher = prepare(runtime, target, definition, args.port, source) + runner = NativeLifecycleRun(runtime, args.java8, launcher, definition) + if source is not None: + result["frozen_import"] = frozen_import(runner, source) + authorize_lifecycle(runtime) + + def progress(value): + result.update(value) + write_json(report_path, report) + + exercise(runner, name, progress, trace_loads=getattr(args, "trace_loads", False)) + except Exception as error: + result.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + print(f"{name}: FAIL: {error}", flush=True) + finally: + if runner is not None: + result["native_readiness"] = dict(file="native114-readiness.json", stages=len(runner.observations)) + try: + runner.close() + except Exception as error: + result.update(status="FAIL", shutdown_error=str(error)) + runtime = output / name + if (runtime / "input-jars.json").is_file(): + try: + verify_staged(runtime, target) + logs = sorted(runtime.glob("console-*.log")) + for log in logs: + require_native_boot(log) + result["native_boots"] = [log.name for log in logs] + result["forge_config_reload_notices"] = { + log.name: log.read_text(encoding="utf-8", errors="replace").count( + "[ne.mi.co.ForgeConfig/CORE]: Forge config just got changed on the file system!") + for log in logs} + except Exception as error: + result.update(status="FAIL", runtime_audit_error=str(error)) + write_json(report_path, report) + report["status"] = "PASS" if all(row["status"] == "PASS" for row in report["profiles"].values()) else "FAIL" + except BaseException as error: + report.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + raise + finally: + if runner is not None: + try: + runner.close() + except Exception as error: + report.update(status="FAIL", shutdown_error=str(error)) + after = fingerprint(args.source.resolve()) + report["sourceRuntimeAfter"] = after + report["source_fixture_unchanged"] = after == source_before + report["sourceWorldAfter"] = fingerprint(args.source.resolve() / WORLD) + if after != source_before: + report.update(status="FAIL", source_error="Protected retained13 source changed") + helper_after = {path.name: digest(path) for path in (Path(lifecycle.__file__), Path(__file__))} + report["helpers_unchanged"] = helper_after == helper_before + if helper_after != helper_before: + report.update(status="FAIL", helper_error="Lifecycle code changed during execution") + report["completed_at_utc"] = datetime.now(timezone.utc).isoformat() + write_json(report_path, report) + print(report["status"] + ": " + str(report_path), flush=True) + return int(report["status"] != "PASS") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--source-manifest", type=Path, required=True) + parser.add_argument("--source-key", default="target113") + parser.add_argument("--source-hashes", type=Path, required=True) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, default=25580) + parser.add_argument("--trace-loads", action="store_true", + help="Record native chunk-load caller stacks; does not change any lifecycle oracle or timeout") + return run(parser.parse_args()) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native114_oil.py b/test-rig/migration/native114_oil.py new file mode 100644 index 0000000..6603504 --- /dev/null +++ b/test-rig/migration/native114_oil.py @@ -0,0 +1,363 @@ +"""Qualify native114 bucket/drain/pipe transport in a disposable pinned runtime. + +The original oil_transfer geometry and quantity/identity oracles are unchanged. +Only the initial empty construction and two native bucket placements may mutate +the fixture; subsequent boots must retain exactly 1000, then 2000 mB per lane. +""" + +import argparse +from copy import deepcopy +from datetime import datetime, timezone +import hashlib +import json +from pathlib import Path +import re +import socket +import time +import traceback + +from lifecycle import LifecycleRun, at, parse +from matrix import digest, fingerprint, stage, write_json +from native114 import (LAUNCHER, RIG, output_path, pinned_profile, + require_clean_log, require_disjoint) +import oil_transfer + + +MINERALOGY_VERSION = "6.0.1.114041" +MINERALOGY_SHA256 = "75CE58BCDCB845407DE55F476FDB06211E2F8DBE6FCB0AC0F7FBDB5B7841F7BA" +BOOT_LABELS = ("oil-first-fill", "oil-restart-refill", "oil-second-restart") +CONFIG_NOTICE = "[ne.mi.co.ForgeConfig/CORE]: Forge config just got changed on the file system!" + + +def read_json(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def fixtures(pins): + definition = deepcopy(oil_transfer.fixtures()) + mineralogy = [jar for jar in pins["jars"] if jar["id"] == "mineralogy"] + if mineralogy: + if (len(mineralogy) != 1 or mineralogy[0]["version"] != MINERALOGY_VERSION + or mineralogy[0]["sha256"].upper() != MINERALOGY_SHA256): + raise ValueError("Mineralogy oil lanes require the pinned native 6.0.1.114041 candidate") + for template in definition[:2]: + lane = deepcopy(template) + for pos in [lane["min"], lane["max"], lane["source"], lane["drain"], *lane["pipes"], lane["output"]]: + pos[0] += 256 + lane["witnesses"] = [lane["source"], lane["drain"], *lane["pipes"], lane["output"]] + lane.update(id="OIL-mineralogy-crude_oil-" + lane["tank"].split(":")[1], + fluid="mineralogy:crude_oil", bucket="mineralogy:crude_oil_bucket") + definition.append(lane) + require_bounds(definition) + return definition + + +def require_bounds(definition): + ids = set() + for lane in definition: + key, low, high = lane["id"], lane["min"], lane["max"] + if key in ids or not re.fullmatch(r"[A-Za-z0-9_-]+", key): + raise AssertionError("Invalid or duplicate oil lane ID") + ids.add(key) + if (len(low) != 3 or len(high) != 3 or low[0] < 1024 or low[2] < 1024 + or low[1] < 8 or high[1] > 240 + or any(not 0 <= b - a <= 31 for a, b in zip(low, high))): + raise AssertionError("Oil lane is outside original remote fixture bounds") + route = [lane["source"], lane["drain"], *lane["pipes"], lane["output"]] + if lane["witnesses"] != route or len({tuple(pos) for pos in route}) != len(route): + raise AssertionError("Oil lane witness geometry changed") + x, y, z = lane["source"] + walls = [[x + dx, y, z + dz] for dx, dz in ((-1, 0), (1, 0), (0, -1), (0, 1))] + for pos in [*route, *walls]: + if len(pos) != 3 or not all(a <= value <= b for a, value, b in zip(low, pos, high)): + raise AssertionError("Oil witness or containment wall outside fixture bounds") + if y >= high[1] or any(sum(abs(a - b) for a, b in zip(p, q)) != 1 + for p, q in zip(route, route[1:])): + raise AssertionError("Oil route disconnected or missing bucket headroom") + for other in definition: + if other is not lane and all(a <= d and c <= b for a, b, c, d in + zip(low, high, other["min"], other["max"])): + raise AssertionError("Oil lane bounds overlap") + + +def require_document(snapshot, definition): + cases = snapshot["cases"] + if set(cases) != {lane["id"] for lane in definition}: + raise AssertionError("Oil snapshot lane IDs/count changed") + for lane in definition: + positions = [tuple(entry["pos"]) for entry in cases[lane["id"]]] + if len(set(positions)) != len(positions) or set(positions) != {tuple(p) for p in lane["witnesses"]}: + raise AssertionError("Oil snapshot has missing, duplicate or moved witnesses") + + +def require_native_boot(path): + require_clean_log(path) + text = path.read_text(encoding="utf-8", errors="replace") + for marker in ("Starting minecraft server version 1.14.4", + "Forge mod loading, version 28.2.26, for MC 1.14.4 with MCP 20190829.143755"): + if marker not in text: + raise AssertionError("Missing genuine native114 boot proof: " + marker) + # Forge 28 logs this one configuration-watcher notice at FATAL without + # throwing; every other FATAL remains a qualification failure. + failures = [line for line in text.splitlines() if + ("/FATAL]" in line and not line.endswith(CONFIG_NOTICE)) or any(token in line for token in ( + "Lifecycle probe failed", "ADVLIFECYCLE FAIL", "Unknown or incomplete command", + "Incorrect argument for command", "An unexpected error occurred trying to execute that command"))] + if failures: + raise AssertionError("Native oil runtime failure: " + repr(failures[:5])) + + +def verify_staged(runtime, pins, seed_before): + expected = sorted((Path(jar["path"]).name, jar["sha256"].upper()) for jar in pins["jars"]) + actual = sorted((jar.name, digest(jar)) for jar in (runtime / "mods").glob("*.jar")) + recorded = sorted((jar["file"], jar["sha256"].upper()) for jar in read_json(runtime / "input-jars.json")) + if len({name for name, _ in expected}) != len(expected) or actual != expected or recorded != expected: + raise AssertionError("Staged native jars differ from captured manifest pins") + if digest(runtime / LAUNCHER) != pins["launcher_sha256"].upper(): + raise AssertionError("Staged native launcher differs from captured manifest pin") + # The installed seed's complete libraries and root launch jars, not its + # worlds/config/mods, are the only seed content matrix.stage may copy. + expected_libraries = {str(Path(name).relative_to("libraries")): value for name, value in seed_before.items() + if Path(name).parts[0] == "libraries"} + if not expected_libraries or fingerprint(runtime / "libraries") != expected_libraries: + raise AssertionError("Staged native libraries differ from seed fingerprint") + expected_roots = {name: value for name, value in seed_before.items() + if len(Path(name).parts) == 1 and name.endswith(".jar") and "installer" not in name} + if {jar.name: digest(jar) for jar in runtime.glob("*.jar")} != expected_roots: + raise AssertionError("Staged root jars differ from seed fingerprint") + + +def prepare(runtime, profile, definition, port, pins, seed_before): + if profile[1] != LAUNCHER: + raise ValueError("Refusing a non-native114 oil runtime") + launcher = stage(runtime, profile) + properties = runtime / "server.properties" + lines = [line for line in properties.read_text().splitlines() + if not line.startswith(("server-port=", "generator-settings="))] + properties.write_text("\n".join(lines) + f"\nserver-port={port}\ngenerator-settings=\n", encoding="utf-8") + for marker in ("migration-operate.allow", "advantage-works.allow", "advantage-lifecycle.allow"): + (runtime / marker).touch() + write_json(runtime / "lifecycle-fixture.json", definition) + verify_staged(runtime, pins, seed_before) + return launcher + + +def construction_commands(definition): + commands = [] + for lane in definition: + commands.append("fill " + " ".join(map(str, lane["min"] + lane["max"])) + " minecraft:air replace") + blocks = [(p, "poweradvantage:fluid_pipe") for p in lane["pipes"]] + blocks += [(lane["drain"], "poweradvantage:fluid_drain"), (lane["output"], lane["tank"])] + x, y, z = lane["source"] + blocks += [([x + dx, y, z + dz], "minecraft:glass") for dx, dz in ((-1, 0), (1, 0), (0, -1), (0, 1))] + commands.extend("setblock " + " ".join(map(str, pos)) + " " + block for pos, block in blocks) + return commands + + +class NativeOilRun(LifecycleRun): + def __init__(self, runtime, java, launcher, definition, pins, seed_before): + super().__init__(runtime, java, launcher, definition) + self.pins, self.seed_before = pins, seed_before + self.boots = [] + self.pending_construction = construction_commands(definition) + self.construction_sealed = False + self.bucket_permission = False + self.bucket_placements = 0 + + def boot(self, label): + if self.server is not None or self.launcher != LAUNCHER or self.boots != list(BOOT_LABELS[:len(self.boots)]): + raise AssertionError("Invalid native oil process lifecycle") + if len(self.boots) >= 3 or label != BOOT_LABELS[len(self.boots)]: + raise AssertionError("Only the three ordered native oil boots are permitted") + if self.boots and not self.construction_sealed: + raise AssertionError("Restart before initial construction was sealed") + verify_staged(self.runtime, self.pins, self.seed_before) + super().boot(label) + self.boots.append(label) + require_native_boot(self.runtime / f"console-{label}.log") + + def seal_construction(self): + if self.pending_construction or self.construction_sealed or len(self.boots) != 1: + raise AssertionError("Incomplete or repeated initial oil construction") + self.construction_sealed = True + + def permit_buckets(self): + if not self.construction_sealed or self.bucket_placements != len(self.boots) - 1 or len(self.boots) > 2: + raise AssertionError("Only one bucket placement after each preservation gate is permitted") + self.bucket_permission = True + + def command(self, command): + if self.server is None: + raise AssertionError("No native oil process is running") + if self.pending_construction and command == self.pending_construction[0]: + if len(self.boots) != 1 or self.construction_sealed: + raise AssertionError("Oil construction after initial baseline is forbidden") + self.pending_construction.pop(0) + elif command == "save-all flush": + pass + else: + match = re.fullmatch(r"advlifecycle (load|snapshot|place-buckets) [A-Za-z0-9_-]+", command) + if not match: + raise AssertionError("Forbidden native oil mutation: " + command) + if match[1] == "place-buckets": + if not self.bucket_permission or self.bucket_placements >= 2: + raise AssertionError("Bucket placement requires the empty/restored preservation gate") + self.bucket_permission = False + self.bucket_placements += 1 + super().command(command) + + +def exercise(runner, seconds, progress): + definition = runner.definition + phases = [] + + def record(phase, snapshot, expected): + require_document(snapshot, definition) + phases.append(dict(phase=phase, cases=oil_transfer.inspect(snapshot, definition, expected))) + progress(dict(phases=phases, full_process_restarts=len(runner.boots) - 1, + bucket_placements=runner.bucket_placements)) + + runner.boot(BOOT_LABELS[0]) + runner.probe("load", "oil-loaded") + for command in construction_commands(definition): + runner.command(command) + runner.seal_construction() + empty = runner.probe("snapshot", "oil-empty") + require_document(empty, definition) + for lane in definition: + for pos in (lane["drain"], lane["output"]): + if parse(at(empty, lane["id"], pos)).get("Tank", {}).get("Amount", 0) != 0: + raise AssertionError("Fixture was not empty: " + lane["id"]) + oil_transfer.require_same_machines(empty, empty, definition) + runner.permit_buckets() + runner.probe("place-buckets", "oil-first-buckets") + time.sleep(seconds) + first = runner.probe("snapshot", "oil-first-fill") + record("first-fill", first, 1000) + oil_transfer.require_same_machines(empty, first, definition) + runner.command("save-all flush") + runner.close() + + runner.boot(BOOT_LABELS[1]) + runner.probe("load", "oil-restart-loaded") + time.sleep(seconds) + restored = runner.probe("snapshot", "oil-restored") + record("restored", restored, 1000) + runner.permit_buckets() + runner.probe("place-buckets", "oil-refill-buckets") + time.sleep(seconds) + refilled = runner.probe("snapshot", "oil-refilled") + record("refilled", refilled, 2000) + oil_transfer.require_same_machines(restored, refilled, definition) + runner.command("save-all flush") + runner.close() + + runner.boot(BOOT_LABELS[2]) + runner.probe("load", "oil-second-restart-loaded") + time.sleep(seconds) + record("retained", runner.probe("snapshot", "oil-retained"), 2000) + runner.command("save-all flush") + runner.close() + if runner.boots != list(BOOT_LABELS) or runner.bucket_placements != 2: + raise AssertionError("Incomplete native oil restart/refill protocol") + + +def run(args): + if not 1 <= args.port <= 65535 or not 2 <= args.seconds <= 120 or not args.java8.is_file(): + raise ValueError("Supply Java 8, a TCP port, and 2..120 seconds per observation interval") + output = output_path(args.output) + if not output.name.startswith("native114-oil-"): + raise ValueError("Use a new native114-oil-* output under test-rig/build") + manifest = args.target_manifest.resolve() + require_disjoint(output, manifest) + manifest_bytes = manifest.read_bytes() + manifest_hash = hashlib.sha256(manifest_bytes).hexdigest().upper() + pins = json.loads(manifest_bytes)["target114"] + profile = pinned_profile(manifest, "target114") + if digest(manifest) != manifest_hash: + raise AssertionError("Target manifest changed during preflight") + for protected in (profile[0], *profile[2]): + require_disjoint(output, protected) + definition = fixtures(pins) + with socket.socket() as listener: + listener.bind(("127.0.0.1", args.port)) + seed_before = fingerprint(profile[0]) + helpers = [Path(oil_transfer.__file__), Path(__file__), Path(__file__).with_name("lifecycle.py")] + helpers_before = {path.name: digest(path) for path in helpers} + output.mkdir(parents=True, exist_ok=False) + (output / "target-manifest.json").write_bytes(manifest_bytes) + report_path = output / "native114-oil-results.json" + runtime = output / "target114" + report = dict(status="RUNNING", started_at_utc=datetime.now(timezone.utc).isoformat(), + scope="native114-buckets-descending-pipes-two-full-restarts", + protocol="Initial empty construction only; native bucket sources twice; no tile.read() restores/API offers/manual tank refills/resets", + targetManifest=str(manifest), targetManifestSha256=manifest_hash, targetProfile=pins, + seed=str(profile[0]), seedFingerprintBefore=seed_before, helperSha256=helpers_before, + port=args.port, seconds=args.seconds, laneCount=len(definition), phases=[], full_process_restarts=0, + mineralogy="INCLUDED" if len(definition) == 6 else "ABSENT_FROM_MANIFEST") + write_json(output / "oil-fixture.json", definition) + write_json(report_path, report) + runner = None + try: + launcher = prepare(runtime, profile, definition, args.port, pins, seed_before) + report["stagedInputs"] = read_json(runtime / "input-jars.json") + runner = NativeOilRun(runtime, args.java8, launcher, definition, pins, seed_before) + + def progress(value): + report.update(value) + write_json(report_path, report) + print("Oil " + value["phases"][-1]["phase"] + ": PASS, " + str(len(definition)) + " lanes", flush=True) + + exercise(runner, args.seconds, progress) + report["status"] = "PASS" + except Exception as error: + report.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + except BaseException as error: + report.update(status="FAIL", failure=repr(error), traceback=traceback.format_exc()) + raise + finally: + if runner is not None: + try: + runner.close() + except Exception as error: + report.update(status="FAIL", shutdown_failure=str(error)) + try: + verify_staged(runtime, pins, seed_before) + logs = sorted(runtime.glob("console-*.log")) + for log in logs: + require_native_boot(log) + report["nativeBoots"] = [log.name for log in logs] + report["forge_config_reload_notices"] = { + log.name: log.read_text(encoding="utf-8", errors="replace").count(CONFIG_NOTICE) for log in logs} + if report["status"] == "PASS" and len(logs) != 3: + raise AssertionError("Three complete native14 boot logs are required") + except Exception as error: + report.update(status="FAIL", runtime_audit_failure=str(error)) + try: + after = fingerprint(profile[0]) + report["seedFingerprintAfter"] = after + report["seed_unchanged"] = after == seed_before + report["helpers_unchanged"] = {path.name: digest(path) for path in helpers} == helpers_before + report["manifest_unchanged"] = digest(manifest) == manifest_hash + if not all(report[key] for key in ("seed_unchanged", "helpers_unchanged", "manifest_unchanged")): + raise AssertionError("Protected seed, helper or manifest changed") + except Exception as error: + report.update(status="FAIL", preservation_failure=str(error)) + report["completed_at_utc"] = datetime.now(timezone.utc).isoformat() + write_json(report_path, report) + print(report["status"] + ": " + str(report_path), flush=True) + return int(report["status"] != "PASS") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, default=25576) + parser.add_argument("--seconds", type=int, default=8) + return run(parser.parse_args()) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native114_partial.py b/test-rig/migration/native114_partial.py new file mode 100644 index 0000000..e066a1f --- /dev/null +++ b/test-rig/migration/native114_partial.py @@ -0,0 +1,362 @@ +"""Native 1.14 transport for unchanged partial-network and pipe-bridge oracles.""" + +import argparse +from datetime import datetime, timezone +import json +from pathlib import Path +import shutil +import socket +import time +import traceback + +from audit_pipe_only import bridge_result +import extended_lifecycle as extended +import pipe_only_lifecycle as bridge +from lifecycle import at, counts, parse +from matrix import digest, fingerprint, write_json +from native114 import output_path, pinned_profile, require_disjoint +from native114_lifecycle import (NativeLifecycleRun, authorize_lifecycle, frozen_import, + prepare, read_json, require_native_boot, verify_source, + verify_staged) + + +def selected_chunks(definition, group=None): + if group is None: + return {(x, z) for f in definition + for x in range(f["min"][0] >> 4, (f["max"][0] >> 4) + 1) + for z in range(f["min"][2] >> 4, (f["max"][2] >> 4) + 1)} + return {tuple(p) for f in definition for p in f.get("groups", {}).get(group, [])} + + +def subset_load_issues(before, current, selected): + old = {tuple(c["pos"]): c for c in before["chunks"]} + chunks = {tuple(c["pos"]): c for c in current["chunks"]} + events = {tuple(e["pos"]) for e in current.get("chunkEvents", []) if e["operation"] == "load"} + errors = [] + for pos in selected: + a, b = old.get(pos), chunks.get(pos) + if a is None or b is None or not b["loaded"]: + errors.append("selected chunk not loaded: " + str(pos)) + elif pos not in events or b["loads"] <= 0 or (not a["loaded"] and b["loads"] <= a["loads"]): + errors.append("missing actual Chunk.Load: " + str(pos)) + for rows in current["cases"].values(): + for row in rows: + if (row["pos"][0] >> 4, row["pos"][2] >> 4) in selected and not row.get("loaded"): + errors.append("selected witness not loaded: " + str(row["pos"])) + return errors + + +def subset_unload_pending(before, current, selected): + old = {tuple(c["pos"]): c for c in before["chunks"]} + chunks = {tuple(c["pos"]): c for c in current["chunks"]} + events = {tuple(e["pos"]) for e in current.get("chunkEvents", []) if e["operation"] == "unload"} + absent = all(pos in chunks and not chunks[pos]["loaded"] for pos in selected) + pending = [str(pos) for pos in selected if pos not in old or pos not in chunks + or pos not in events or chunks[pos]["unloads"] <= old[pos]["unloads"]] + return absent, pending + + +class NativePartialRun(NativeLifecycleRun): + """Select actual snapshots by chunk callbacks/ticks, never by machine results.""" + + def __init__(self, *args): + super().__init__(*args) + self._released_groups = {} + + def _canonical(self, selected, label): + root = self.runtime / "advantage-works-results" + destination = root / ("lifecycle-" + label + ".json") + if destination.exists(): + raise FileExistsError(destination) + shutil.copy2(root / ("lifecycle-" + selected + ".json"), destination) + + def _load_ready(self, operation, label): + selected = selected_chunks(self.definition, None if operation == "load" else operation[5:]) + if not selected: + raise ValueError("Unknown fixture chunk group: " + operation) + before = self._raw_probe("snapshot", label + "-native-before") + current_label = label + "-native-initial" + current = self._raw_probe(operation, current_label) + initial_tick, loaded_tick, started = current["worldTime"], None, time.monotonic() + record = dict(label=label, phase="native-subset-load-readiness", operation=operation, + selected_chunks=sorted(selected), samples=[], max_seconds=20, max_ticks=160) + self.observations.append(record) + try: + for attempt in range(200): + errors = subset_load_issues(before, current, selected) + tick = current["worldTime"] + if tick < initial_tick: + raise AssertionError("Native worldTime moved backwards") + loaded_tick = None if errors else (tick if loaded_tick is None else loaded_tick) + record["samples"].append(dict(label=current_label, world_time=tick, issues=errors, + loaded_since_world_time=loaded_tick)) + if loaded_tick is not None and tick - loaded_tick >= 1: + self._canonical(current_label, label) + record.update(status="PASS", selected_label=current_label, canonical_label=label) + self._record() + return current + if time.monotonic() - started >= 20 or tick - initial_tick >= 160 or attempt == 199: + raise TimeoutError("Native selected chunks did not load and advance a real tick: " + label) + self._record() + time.sleep(0.01) + current_label = label + f"-native-pending-{attempt + 1:03}" + current = self._raw_probe("snapshot", current_label) + except BaseException as error: + record.update(status="FAIL", failure=str(error)) + self._record() + raise + + def _snapshot_ready(self, operation, label): + current_label = label + "-native-initial" + current = self._raw_probe(operation, current_label) + record = dict(label=label, phase="native-subset-unload-callback-readiness", samples=[]) + self.observations.append(record) + for attempt in range(45): + pending_groups = [] + for group, context in self._released_groups.items(): + absent, pending = subset_unload_pending(context["before"], current, context["selected"]) + if absent and pending and time.monotonic() < context["deadline"]: + pending_groups.append(group) + record["samples"].append(dict(label=current_label, world_time=current["worldTime"], + pending_groups=pending_groups)) + if not pending_groups or attempt == 44: + break + self._record() + deadline = min(self._released_groups[g]["deadline"] for g in pending_groups) + time.sleep(min(1, max(0, deadline - time.monotonic()))) + current_label = label + f"-native-pending-{attempt + 1:02}" + current = self._raw_probe("snapshot", current_label) + self._canonical(current_label, label) + record.update(status="OBSERVED", selected_label=current_label, canonical_label=label) + self._record() + return current + + def probe(self, operation, label): + if operation == "load" or operation.startswith("load-"): + result = self._load_ready(operation, label) + if operation == "load": + self._released_groups.clear() + else: + self._released_groups.pop(operation[5:], None) + return result + if operation.startswith("release-"): + group = operation[8:] + selected = selected_chunks(self.definition, group) + if not selected: + raise ValueError("Unknown fixture chunk group: " + group) + before = self._raw_probe("snapshot", label + "-native-before") + result = self._raw_probe(operation, label) + self._released_groups[group] = dict(before=before, selected=selected, + deadline=time.monotonic() + 45) + return result + if self._released_groups and operation in ("snapshot", "isolated"): + return self._snapshot_ready(operation, label) + return super().probe(operation, label) + + def boot(self, label): + super().boot(label) + require_native_boot(self.runtime / f"console-{label}.log") + if label == "blocked-restart": + self.probe("load", "blocked-restart-native-restored") + + +class NativeExtendedRun(extended.ExtendedRun, NativePartialRun): + pass + + +def orders(runner, process=False): + results = [] + for order in (("machine", "supply"), ("supply", "machine")): + label = "order-" + "-".join(order) + before = runner.probe("snapshot", label + "-before") + runner.probe("release-supply", label + "-release-supply") + runner.probe("release-machine", label + "-release-machine") + time.sleep(5) + absent = runner.probe("snapshot", label + "-absent") + first = runner.probe("load-" + order[0], label + "-first") + time.sleep(2) + settled = runner.probe("snapshot", label + "-settled") + loaded = runner.probe("load-" + order[1], label + "-loaded") + results.append(dict(label=label, order=list(order), cases=extended.order_results( + runner.definition, before, absent, first, settled, loaded, order))) + if process: + runner.replenish() + time.sleep(50) + runner.processing(label + "-resumed", 5000) + return results + + +def exercise_partial(runner, progress): + runner.boot("partial-trace") + runner.build() + time.sleep(50) + runner.processing("commissioned", 1000) + runner.probe("trace-loads", "trace-enabled") + result = dict(status="RUNNING", partial_networks=[], reload_orders=[]) + for cycle, group in enumerate(("machine", "supply"), 1): + result["partial_networks"].append(runner.partial(group, cycle)) + progress(result) + result["reload_orders"] = orders(runner) + result["status"] = "PASS" if all(c["status"] == "PASS" for r in + result["partial_networks"] + result["reload_orders"] for c in r["cases"].values()) else "FAIL" + runner.command("save-all flush") + runner.close() + progress(result) + return result + + +def exercise_bridge(runner, progress): + runner.boot("pipe-only") + runner.probe("load", "loaded") + for f in runner.definition: + runner.command("fill " + " ".join(map(str, f["min"] + f["max"])) + " minecraft:air replace") + fluid, steam = f["id"] == "C-FLUID", f["id"] == "C-STEAM" + pipe = "poweradvantage:fluid_pipe" if fluid else "steamadvantage:steam_pipe" if steam else "electricadvantage:electric_conduit" + runner.block(f["witnesses"][0], pipe if fluid else "poweradvantage:infinite_" + ("steam" if steam else "electricity")) + for pos in f["pipes"]: + runner.block(pos, pipe) + runner.block(f["witnesses"][1], "poweradvantage:fluid_metal_tank" if fluid else + "steamadvantage:steam_furnace" if steam else "electricadvantage:electric_furnace") + if steam: + runner.inventory(f["witnesses"][1], 0, "minecraft:coal", 64) + runner.probe("trace-loads", "trace-enabled") + time.sleep(2) + runner.probe("snapshot", "commissioned") + result = dict(status="RUNNING", cycles=[]) + for cycle in range(3): + before = runner.probe("snapshot", f"bridge-{cycle}-before") + runner.command("save-all flush") + runner.probe("release-bridge", f"bridge-{cycle}-released") + time.sleep(3) + absent = runner.probe("isolated", f"bridge-{cycle}-isolated") + runner.probe("offer-blocked", f"bridge-{cycle}-blocked-offer") + time.sleep(3) + confirmed = runner.probe("snapshot", f"bridge-{cycle}-confirmed") + loaded = runner.probe("load-bridge", f"bridge-{cycle}-loaded") + runner.probe("offer", f"bridge-{cycle}-reconnected-offer") + for f in runner.definition: + if f["id"] == "C-FLUID": + continue + machine, steam = f["witnesses"][1], f["id"] == "C-STEAM" + for slot in range(1 if steam else 0, 7 if steam else 10): + runner.inventory(machine, slot, "minecraft:air", 1) + runner.inventory(machine, 1 if steam else 0, "minecraft:iron_ore", 4) + time.sleep(50) + resumed = runner.probe("snapshot", f"bridge-{cycle}-resumed") + verdict = bridge_result(runner.definition, before, absent, confirmed, loaded, resumed, cycle) + result["cycles"].append(dict(cycle=cycle, **verdict)) + progress(result) + print(f"{runner.runtime.name}: bridge cycle {cycle + 1}: {verdict['status']}", flush=True) + result["status"] = "PASS" if all(c["status"] == "PASS" for c in result["cycles"]) else "FAIL" + runner.command("save-all flush") + runner.close() + progress(result) + return result + + +def common_parser(description): + parser = argparse.ArgumentParser(description=description) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--source-manifest", type=Path, required=True) + parser.add_argument("--source-key", default="target113") + parser.add_argument("--source-hashes", type=Path, required=True) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, required=True, choices=(25581, 25582, 25583)) + return parser + + +def campaign(args, name, definition, exercise, runner_type=NativeExtendedRun): + output = output_path(args.output) + for protected in (args.source, args.source_manifest, args.source_hashes, args.target_manifest): + require_disjoint(output, protected) + if not args.java8.is_file(): + raise FileNotFoundError(args.java8) + source_before, history = verify_source(args) + target = pinned_profile(args.target_manifest.resolve(), "target114") + for protected in (target[0], *target[2]): + require_disjoint(output, protected) + with socket.socket() as listener: + listener.bind(("127.0.0.1", args.port)) + output.mkdir(parents=True) + report_path = output / ("native114-" + name + "-results.json") + helpers = [Path(__file__), Path(extended.__file__), Path(bridge.__file__), + Path(__file__).with_name("lifecycle.py"), Path(__file__).with_name("native114_lifecycle.py"), + Path(__file__).with_name("native114_extended.py")] + hashes = {path.name: digest(path) for path in helpers if path.is_file()} + report = dict(status="RUNNING", campaign=name, started_at_utc=datetime.now(timezone.utc).isoformat(), + scope="native114 fresh and hash-protected retained13 copy; unchanged historical semantic oracles", + source=str(args.source.resolve()), sourceRuntimeBefore=source_before, sourceStateGates=history, + sourceManifestSha256=digest(args.source_manifest), sourceHashesSha256=digest(args.source_hashes), + targetManifest=str(args.target_manifest.resolve()), targetManifestSha256=digest(args.target_manifest), + targetProfile=read_json(args.target_manifest)["target114"], helpersBefore=hashes, port=args.port, profiles={}) + for path in helpers: + if path.is_file(): + archive = output / "protocol-sources" / path.name + archive.parent.mkdir(exist_ok=True) + shutil.copy2(path, archive) + write_json(output / "lifecycle-fixture.json", definition) + write_json(report_path, report) + try: + for label, source in (("fresh114", None), ("original110-chained113-native114", args.source)): + runtime, runner = output / label, None + result = report["profiles"][label] = dict(status="RUNNING") + write_json(report_path, report) + try: + launcher = prepare(runtime, target, definition, args.port, source) + runner = runner_type(runtime, args.java8, launcher, definition) + if source is not None: + result["frozen_import"] = frozen_import(runner, source) + authorize_lifecycle(runtime) + def progress(value): + result.update(value) + write_json(report_path, report) + exercise(runner, progress) + except BaseException as error: + result.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + print(label + ": FAIL: " + str(error), flush=True) + finally: + if runner is not None: + try: + runner.close() + except BaseException as error: + result.update(status="FAIL", shutdown_error=str(error)) + if (runtime / "input-jars.json").is_file(): + try: + verify_staged(runtime, target) + logs = sorted(runtime.glob("console-*.log")) + for log in logs: + require_native_boot(log) + text = log.read_text(encoding="utf-8", errors="replace") + if "Stopping server" not in text: + raise AssertionError("Missing graceful shutdown: " + log.name) + result["native_boots"] = [log.name for log in logs] + result["keep_up_warnings"] = {log.name: log.read_text(encoding="utf-8").count("Can't keep up!") for log in logs} + except BaseException as error: + result.update(status="FAIL", runtime_audit_error=str(error)) + write_json(report_path, report) + report["status"] = "PASS" if all(p["status"] == "PASS" for p in report["profiles"].values()) else "FAIL" + finally: + report["sourceRuntimeAfter"] = fingerprint(args.source.resolve()) + report["source_fixture_unchanged"] = report["sourceRuntimeAfter"] == source_before + report["helpersAfter"] = {path.name: digest(path) for path in helpers if path.is_file()} + report["helpers_unchanged"] = hashes == report["helpersAfter"] + if not report["source_fixture_unchanged"] or not report["helpers_unchanged"]: + report["status"] = "FAIL" + report["completed_at_utc"] = datetime.now(timezone.utc).isoformat() + write_json(report_path, report) + print(report["status"] + ": " + str(report_path), flush=True) + return int(report["status"] != "PASS") + + +def main(): + parser = common_parser(__doc__) + parser.add_argument("--campaign", choices=("partial", "bridge"), required=True) + args = parser.parse_args() + definition = [f for f in extended.fixtures() if "groups" in f] if args.campaign == "partial" else bridge.fixtures() + return campaign(args, args.campaign, definition, exercise_partial if args.campaign == "partial" else exercise_bridge) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native114_partial_diagnostic.py b/test-rig/migration/native114_partial_diagnostic.py new file mode 100644 index 0000000..8a802cd --- /dev/null +++ b/test-rig/migration/native114_partial_diagnostic.py @@ -0,0 +1,30 @@ +"""Empty-bay control for native partial-unload fixture ticket interference.""" + +import time + +import extended_lifecycle +from native114_partial import campaign, common_parser, orders + + +def exercise(runner, progress): + runner.boot("partial-empty-control") + runner.probe("load", "loaded") + for f in runner.definition: + runner.command("fill " + " ".join(map(str, f["min"] + f["max"])) + " minecraft:air replace") + runner.probe("trace-loads", "trace-enabled") + result = dict(status="RUNNING", purpose="empty bays; no Advantage networks or machines constructed", partial_networks=[]) + for cycle, group in enumerate(("machine", "supply"), 1): + result["partial_networks"].append(runner.partial(group, cycle)) + progress(result) + result["reload_orders"] = orders(runner) + result["status"] = "PASS" if all(c["status"] == "PASS" for r in + result["partial_networks"] + result["reload_orders"] for c in r["cases"].values()) else "FAIL" + runner.command("save-all flush") + runner.close() + progress(result) + return result + + +if __name__ == "__main__": + args = common_parser(__doc__).parse_args() + raise SystemExit(campaign(args, "empty-control", [f for f in extended_lifecycle.fixtures() if "groups" in f], exercise)) diff --git a/test-rig/migration/native114_player_layout.py b/test-rig/migration/native114_player_layout.py new file mode 100644 index 0000000..64aab00 --- /dev/null +++ b/test-rig/migration/native114_player_layout.py @@ -0,0 +1,119 @@ +"""Native destinations for the historical player-only bag/Ender layout oracle.""" + +import argparse +import json +from pathlib import Path +import shutil +import socket + +import actors +from dimensions import exact_item_differences +from matrix import WORLD, Server, copy_fixture, digest, fingerprint, write_json +from native114 import output_path, pinned_profile, require_clean_log, require_disjoint +from native114_actors import baseline, command, raw +from player_layout import player_verdict + + +ROUTES = ("original110", "maintained110", "maintained112", "original110-chained-113", + "maintained110-direct-113", "maintained112-direct-113") + + +def verdict(expected, observed): + result = player_verdict(expected, observed) + extra = exact_item_differences({key: expected[key] for key in ("file", "adapter")}, observed) + result["differences"].extend(extra) + if extra: + result["status"] = "FAIL" + return result + + +def audit(root): + summary = json.loads((root / "summary.json").read_text()) + findings = [] + manifest = Path(summary["targetManifest"]) + if digest(manifest) != summary["targetManifestSha256"]: + findings.append("Target manifest changed") + pinned_profile(manifest, "target114") + for route in summary["results"]: + runtime = root / route + provenance = json.loads((runtime / "source-provenance.json").read_text()) + source = Path(provenance["source"]) + if fingerprint(source) != provenance["fingerprint"]: + findings.append(route + ": protected source changed") + expected, _ = baseline(source, "actors") + for jar in json.loads((runtime / "input-jars.json").read_text()): + if digest(runtime / "mods" / jar["file"]) != jar["sha256"]: + findings.append(route + ": staged jar changed") + for label in ("converted", "restarted"): + state = actors.capture(runtime, label, runtime / "actor-evidence" / label) + current = {"player": verdict(expected, state), "frozen": actors.freeze_result(raw(runtime, "actors", label))} + if current != summary["results"][route][label] or any(value["status"] != "PASS" for value in current.values()): + findings.append(route + "/" + label + ": raw player/freeze replay differs or fails") + require_clean_log(runtime / ("console-" + label + ".log")) + return {"status": "FAIL" if findings else "PASS", "routes": len(summary["results"]), "findings": findings, + "scope": "Player storage only; no actor release or turbine operation assertion"} + + +def run(args): + root = output_path(args.output) + require_disjoint(root, args.fixtures) + target = pinned_profile(args.target_manifest.resolve(), "target114") + with socket.socket() as port: + port.bind(("127.0.0.1", args.port)) + root.mkdir(parents=True) + results = {} + for route in ROUTES: + source = args.fixtures / route + before = fingerprint(source) + approved = source / "fixture-hashes.json" + if approved.is_file() and fingerprint(source / WORLD) != json.loads(approved.read_text()): + raise AssertionError("Protected layout fixture changed") + expected, source_label = baseline(source, "actors") + runtime = root / route + launcher = actors.boot(runtime, target, args.java8, args.port) + copy_fixture(source, runtime) + shutil.copy2(source / "actor-fixture.json", runtime / "actor-fixture.json") + write_json(runtime / "source-provenance.json", {"source": str(source.resolve()), "fingerprint": before, "checkpoint": source_label}) + checks = {} + try: + for label in ("converted", "restarted"): + server = Server(runtime, args.java8, launcher, label) + try: + state = command(server, runtime, "actors", "read", label) + checks[label] = {"player": verdict(expected, state), "frozen": actors.freeze_result(raw(runtime, "actors", label))} + finally: + server.close() + require_clean_log(runtime / ("console-" + label + ".log")) + finally: + write_json(runtime / "player-comparison.json", checks) + if fingerprint(source) != before: + raise AssertionError("Protected player-layout source changed") + results[route] = checks + write_json(root / "results.json", results) + print(route + ": " + ", ".join(label + "=" + check["player"]["status"] for label, check in checks.items()), flush=True) + status = "FAIL" if any(value["status"] != "PASS" for checks in results.values() for phase in checks.values() for value in phase.values()) else "PASS" + write_json(root / "summary.json", {"status": status, "results": results, "sourceFixtures": "UNCHANGED", + "targetManifest": str(args.target_manifest.resolve()), "targetManifestSha256": digest(args.target_manifest), + "scope": "Unchanged historical player-only oracle plus exact item NBT and genuinely frozen actor guard"}) + return int(status != "PASS") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--audit", type=Path) + parser.add_argument("--fixtures", type=Path) + parser.add_argument("--target-manifest", type=Path) + parser.add_argument("--java8", type=Path) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, choices=range(25584, 25588)) + args = parser.parse_args() + if args.audit: + if args.output.exists(): + raise FileExistsError(args.output) + result = audit(args.audit.resolve()) + write_json(args.output, result) + print(result["status"] + ": " + str(result["routes"]) + " routes; " + str(len(result["findings"])) + " integrity findings") + raise SystemExit(int(result["status"] != "PASS")) + if any(value is None for value in (args.fixtures, args.target_manifest, args.java8, args.port)): + parser.error("Running requires fixtures, target manifest, Java 8 and port") + raise SystemExit(run(args)) diff --git a/test-rig/migration/native114_recipes.py b/test-rig/migration/native114_recipes.py new file mode 100644 index 0000000..49622df --- /dev/null +++ b/test-rig/migration/native114_recipes.py @@ -0,0 +1,319 @@ +"""Native 1.14 recipe exports and progression on new hash-pinned disposable servers. + +Historical campaign modules supply assertion data only. No legacy profile, seed, +world, artifact fallback or historical result is used as native runtime evidence. +""" + +import argparse +from collections import Counter +from datetime import datetime, timezone +import json +from pathlib import Path +import re +import socket +import zipfile + +import nbtlib + +from matrix import WORLD, Server, digest, fingerprint, stage, write_json +from native114 import RIG, output_path, pinned_profile, require_clean_log, require_disjoint +from progression import fixture as progression_fixture, validate_tags +from recipe_unlocks import MATERIALS + + +MOD_COUNTS = {"poweradvantage": 35, "steamadvantage": 24, "electricadvantage": 50} +RECIPE_TYPES = {"minecraft:crafting_shaped", "minecraft:crafting_shapeless", "minecraft:smelting"} +UNLOCK_BOOTS = ( + ("NORMAL", "normal", ("sweep", "persist")), + ("NORMAL", "normal-restart", ("restore",)), + ("TECH_PROGRESSION", "tech", ("restore", "sweep", "persist")), + ("APOCALYPTIC", "apocalyptic", ("restore", "sweep", "persist")), + ("NORMAL", "normal-return", ("restore",)), +) +MODES = ("NORMAL", "TECH_PROGRESSION", "APOCALYPTIC") + + +def read_json(path): + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def recipe_fixture(jars): + recipes = [] + sources = {} + for path in jars: + with zipfile.ZipFile(path) as jar: + for mod in MOD_COUNTS: + prefix = f"data/{mod}/recipes/" + names = sorted(name for name in jar.namelist() if name.startswith(prefix) + and name.endswith(".json") and "/" not in name[len(prefix):]) + for name in names: + recipe = json.loads(jar.read(name)) + if recipe["type"] not in RECIPE_TYPES: + continue + stem = Path(name).stem + advancement_name = f"data/{mod}/advancements/recipes/{stem}.json" + advancement = json.loads(jar.read(advancement_name)) + predicates = {} + for key, criterion in advancement["criteria"].items(): + if criterion["trigger"] != "minecraft:inventory_changed": + continue + items = criterion["conditions"]["items"] + if len(items) != 1 or set(items[0]) not in ({"item"}, {"tag"}): + raise AssertionError(f"Unsupported inventory predicate: {advancement_name}/{key}") + predicates[key] = items[0] + if not predicates: + raise AssertionError(f"No ingredient-discovery predicates: {advancement_name}") + identity = f"{mod}:{stem}" + if identity in sources: + raise AssertionError(f"Duplicate packaged recipe: {identity}") + sources[identity] = {"jar": str(path), "recipe": name, "advancement": advancement_name} + recipes.append({"id": identity, "ingredients": predicates}) + counts = Counter(row["id"].split(":")[0] for row in recipes) + if counts != MOD_COUNTS: + raise AssertionError(f"Packaged recipe inventory changed: {dict(counts)}; expected {MOD_COUNTS}") + return {"recipes": recipes, "materials": MATERIALS}, sources + + +def native_target(manifest): + entry = read_json(manifest)["target114"] + required = {"advantageworkstest", "poweradvantage", "steamadvantage", "electricadvantage", "basemetals", "orespawn"} + if {jar["id"] for jar in entry["jars"]} != required: + raise ValueError("This campaign requires exactly the native harness and five packaged mods") + target = pinned_profile(manifest, "target114") + if any("deobf" in path.name.lower() for path in target[2]): + raise ValueError("Use packaged native jars, not development candidates") + return target + + +def require_free_port(port): + with socket.socket() as check: + check.bind(("127.0.0.1", port)) + + +def stage_native(runtime, target, port, campaign): + require_free_port(port) + launcher = stage(runtime, target) + # Replace only the disposable stage's legacy generator and port defaults. + props = dict(line.split("=", 1) for line in (runtime / "server.properties").read_text().splitlines() if "=" in line) + props.update({"server-port": str(port), "generator-settings": "", "level-type": "FLAT"}) + (runtime / "server.properties").write_text("".join(f"{key}={value}\n" for key, value in props.items()), encoding="utf-8") + marker = "advantage-recipes.allow" if campaign == "unlock" else "advantage-progression.allow" + (runtime / marker).touch() + if digest(runtime / launcher) != digest(target[0] / launcher): + raise AssertionError("Staged launcher differs from native input") + for jar in target[2]: + if digest(runtime / "mods" / jar.name) != digest(jar): + raise AssertionError(f"Staged mod differs from pinned input: {jar}") + if (runtime / WORLD).exists(): + raise AssertionError("Staging must not import an existing world") + return launcher + + +def configure(runtime, mode, rubber=True): + config = runtime / "config/poweradvantage-common.toml" + config.parent.mkdir(exist_ok=True) + config.write_text(f'[options]\nrecipeMode = "{mode}"\nplasticEqualsRubber = {str(rubber).lower()}\n', encoding="utf-8") + + +def validate_unlock(result, definition, operation): + expected = {row["id"] for row in definition["recipes"]} + rows = result["cases"] + if (result["status"] != "PASS" or len(rows) != len(expected) + or {row["id"] for row in rows} != expected or result["potatoCraftingCases"] != 59): + raise AssertionError("Incomplete native unlock/potato-crafting report") + if not result["bookRecipes"]: + raise AssertionError("Native server exported no recipe-book recipes") + if operation == "persist" and result["additionalMaterials"]: + raise AssertionError("Human materials checklist has gaps: " + str(result["additionalMaterials"])) + allowed = {"PASS", "SKIP_NO_INGREDIENT_PROVIDER"} + if any(row["status"] not in allowed for row in rows): + raise AssertionError("Failed native discovery case") + return {"cases": len(rows), "active": sum(row["recipeActive"] for row in rows), + "passed": sum(row["status"] == "PASS" for row in rows), + "skipped": sum(row["status"] != "PASS" for row in rows), "potatoCraftingCases": 59} + + +def validate_progression(result, mode, rubber): + validate_tags(result, mode, rubber) + if result["mode"] != mode or result["plasticEqualsRubber"] != rubber: + raise AssertionError("Native progression ignored its requested mode/rubber configuration") + if result["status"] != "PASS" or result["blocked"] != 0 or result["blockedRecipes"]: + raise AssertionError("Native manufacture blocked: " + str(result.get("blockedRecipes")) + "; " + + str([row["id"] for row in result["targets"] if row["status"] == "BLOCKED"])) + if not result["targets"] or not result["reachable"] or not result["executedRecipes"] or not result["processWitnesses"]: + raise AssertionError("Empty native manufacture evidence") + if not result["lootDataPackOverrideVerified"]: + raise AssertionError("Player loot override was not preserved") + + +def verify_level(runtime, entry): + level_path = runtime / WORLD / "level.dat" + level = nbtlib.load(level_path) + if str(level["Data"]["Version"]["Name"]) != "1.14.4": + raise AssertionError("Server did not write a native 1.14.4 level header") + mods = {str(row["ModId"]): str(row["ModVersion"]) for row in level["fml"]["LoadingModList"]} + if mods.get("forge") != "28.2.26" or any(mods.get(jar["id"]) != jar["version"] for jar in entry["jars"]): + raise AssertionError(f"Saved native mod identities differ from the target manifest: {mods}") + registry = level["fml"]["Registries"] + if any(not registry.get(name, {}).get("ids") for name in ("minecraft:item", "minecraft:block")): + raise AssertionError("Missing native item/block registry snapshot; legacy plural registry names are not valid") + return {"path": str(level_path), "sha256": digest(level_path), "minecraft": "1.14.4", "mods": mods} + + +def audit_unlock_log(path, previous_recipes, current_recipes): + """Witness vanilla's exact mode-conditional saved-book pruning, not arbitrary errors.""" + removed = set(previous_recipes) - set(current_recipes) + observed = set() + events = [] + for original in path.read_text(encoding="utf-8", errors="replace").splitlines(): + line = re.sub(r"\x1b\[[0-9;]*m", "", original) + if "/ERROR]" not in line and not any(token in line for token in ( + "NoSuchFieldError", "NoSuchMethodError", "Failed to reload data packs", "Encountered an unexpected exception")): + continue + match = re.search(r"\[minecraft/ServerRecipeBook\]: Tried to load unrecognized recipe: ([a-z0-9_]+:[a-z0-9_./-]+) removed now\.$", line) + if not match or match[1] not in removed: + raise AssertionError(f"Unexpected native recipe runtime error: {original}") + observed.add(match[1]) + events.append(original) + if observed != removed: + raise AssertionError(f"Saved-book pruning did not match the native packet-set difference: expected {sorted(removed)}, saw {sorted(observed)}") + return {"expected_removed_ids": sorted(removed), "observed_removed_ids": sorted(observed), "events": events} + + +def unlock_campaign(root, target, entry, java, port, definition, report): + runtime = root / "unlock" + launcher = stage_native(runtime, target, port, "unlock") + write_json(runtime / "recipe-fixture.json", definition) + previous_recipes = set() + for mode, label, operations in UNLOCK_BOOTS: + configure(runtime, mode) + row = {"mode": mode, "label": label, "operations": [], "status": "RUNNING"} + report["unlock_boots"].append(row) + write_json(root / "native114-recipes-results.json", report) + print(f"Native114 unlock boot {label}: {', '.join(operations)}", flush=True) + server = Server(runtime, java, launcher, label) + try: + for operation in operations: + name = f"{label}-{operation}" + server.send(f"advrecipes {operation} {name}") + server.wait(f"ADVRECIPES COMPLETE {operation} {name}", 120) + result = read_json(runtime / f"recipes-{name}.json") + counts = validate_unlock(result, definition, operation) + packet = runtime / f"recipes-{name}.bin" + if not packet.is_file() or packet.stat().st_size == 0: + raise AssertionError("Missing native server recipe packet") + row["operations"].append({"operation": operation, "report": f"recipes-{name}.json", + "packet_sha256": digest(packet), **counts}) + server.send("save-all flush") + finally: + server.close() + current_recipes = {recipe["id"] for recipe in result["bookRecipes"]} + row["vanilla_recipe_pruning"] = audit_unlock_log(runtime / f"console-{label}.log", previous_recipes, current_recipes) + previous_recipes = current_recipes + verify_level(runtime, entry) + row["status"] = "PASS" + artifacts = [runtime / f"recipes-{mode}-sweep.{extension}" for mode in ("normal", "tech", "apocalyptic") + for extension in ("bin", "json")] + report["recipe_fixture"] = {"directory": str(runtime), "registry": verify_level(runtime, entry), + "files": [{"file": path.name, "sha256": digest(path)} for path in artifacts]} + + +def progression_campaign(root, target, entry, java, port, report): + runtime = root / "progression" + launcher = stage_native(runtime, target, port, "progression") + write_json(runtime / "progression-fixture.json", progression_fixture()) + pack = runtime / WORLD / "datapacks/progression-override" + write_json(pack / "pack.mcmeta", {"pack": {"pack_format": 4, "description": "Disposable loot-override witness"}}) + write_json(pack / "data/minecraft/loot_tables/chests/simple_dungeon.json", { + "pools": [{"name": "test_override", "rolls": 1, "entries": [{"type": "item", "name": "minecraft:dirt"}]}]}) + for mode in MODES: + for rubber in (True, False): + configure(runtime, mode, rubber) + for phase in ("initial", "restart"): + label = mode.lower() + ("-rubber" if rubber else "-no-rubber") + "-" + phase + print(f"Native114 progression boot {label}", flush=True) + server = Server(runtime, java, launcher, label) + try: + for reload in ((False, True) if phase == "initial" else (False,)): + name = label + ("-reload" if reload else "") + row = {"mode": mode, "rubber": rubber, "phase": "reload" if reload else phase, + "report": f"progression-{name}.json", "status": "RUNNING"} + report["progression_cases"].append(row) + write_json(root / "native114-recipes-results.json", report) + if reload: + server.send("reload") + server.wait("Reloading!", 60) + server.send(f"advprogression {name}") + server.wait(f"ADVPROGRESSION COMPLETE {name}", 240) + result = read_json(runtime / row["report"]) + validate_progression(result, mode, rubber) + row.update(status="PASS", targets=len(result["targets"]), + witnesses=len(result["processWitnesses"]), blocked=result["blocked"]) + server.send("save-all flush") + finally: + server.close() + require_clean_log(runtime / f"console-{label}.log") + verify_level(runtime, entry) + + +def run(args): + manifest = args.target_manifest.resolve() + target = native_target(manifest) + entry = read_json(manifest)["target114"] + root = output_path(args.output) + for protected in (manifest, target[0], *target[2]): + require_disjoint(root, protected) + java = args.java8.resolve() + if not java.is_file() or 'JAVA_VERSION="1.8.' not in (java.parent.parent / "release").read_text(): + raise ValueError("Supply an existing Java 8 bin/java.exe") + require_free_port(args.port) + definition, sources = recipe_fixture(target[2]) + before = fingerprint(target[0]) + root.mkdir(parents=True, exist_ok=False) + report = {"status": "RUNNING", "minecraft": "1.14.4", "forge": "28.2.26", "campaign": args.campaign, + "target_manifest": str(manifest), "target_manifest_sha256": digest(manifest), + "target_jars": entry["jars"], "java": str(java), "java_sha256": digest(java), + "unlock_boots": [], "progression_cases": [], "recipe_inventory": dict(MOD_COUNTS), + "scope": "packaged native server assertions and recipe exports; not graphical/manual acceptance"} + write_json(root / "packaged-recipe-sources.json", sources) + write_json(root / "seed-fingerprint-before.json", before) + try: + if args.campaign in ("all", "unlock"): + unlock_campaign(root, target, entry, java, args.port, definition, report) + if args.campaign in ("all", "progression"): + progression_campaign(root, target, entry, java, args.port, report) + if args.campaign in ("all", "unlock") and len(report["unlock_boots"]) != 5: + raise AssertionError("The native unlock campaign did not complete five boots") + if args.campaign in ("all", "progression") and len(report["progression_cases"]) != 18: + raise AssertionError("The native progression campaign did not complete eighteen checks") + native_target(manifest) + if digest(manifest) != report["target_manifest_sha256"]: + raise AssertionError("Pinned manifest changed during the campaign") + report["status"] = "PASS" + except BaseException as failure: + report.update(status="FAIL", error=f"{type(failure).__name__}: {failure}") + raise + finally: + after = fingerprint(target[0]) + write_json(root / "seed-fingerprint-after.json", after) + report["seed_unchanged"] = before == after + if before != after: + report.update(status="FAIL", seed_error="Protected installed server seed changed") + write_json(root / "native114-recipes-results.json", report) + if before != after: + raise AssertionError("Protected installed server seed changed") + print(f"Native114 recipe campaign PASS: {root}", flush=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, default=RIG / "build" / ("native114-recipes-" + datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S"))) + parser.add_argument("--port", type=int, default=25577) + parser.add_argument("--campaign", choices=("all", "unlock", "progression"), default="all") + run(parser.parse_args()) + + +if __name__ == "__main__": + main() diff --git a/test-rig/migration/native114_refresh.py b/test-rig/migration/native114_refresh.py new file mode 100644 index 0000000..e553cf1 --- /dev/null +++ b/test-rig/migration/native114_refresh.py @@ -0,0 +1,109 @@ +"""Rerun archived frozen/connected fixture inputs against a new pinned candidate. + +Previous results select immutable inputs only. Every destination is new and all +state/operating verdicts are recomputed by the existing native runners. +""" + +import argparse +from argparse import Namespace +from pathlib import Path +import socket +import traceback +from unittest.mock import patch + +import native114 +import native114_connected +from matrix import digest, write_json +from native114_lifecycle import read_json + + +def snapshot_name(source, expected): + matches = sorted(p.name for p in source.glob("migration-*.json") if digest(p) == expected) + if not matches: + raise AssertionError("No retained snapshot matches its archived digest") + # Frozen conversion/restart snapshots can be byte-for-byte identical. + # Identity remains the exact archived SHA, never a preferred passing state. + return "migration-restarted.json" if "migration-restarted.json" in matches else matches[0] + + +def run(args): + output = native114.output_path(args.output) + for protected in (args.previous, args.target_manifest): + native114.require_disjoint(output, protected) + native114.pinned_profile(args.target_manifest.resolve(), "target114") + previous = read_json(args.previous) + if previous.get("status") != "PASS": + raise AssertionError("Select a completed archived campaign as fixture provenance") + output.mkdir(parents=True, exist_ok=False) + report = dict(status="RUNNING", scope=args.kind, previous=str(args.previous.resolve()), + previousSha256=digest(args.previous), targetManifest=str(args.target_manifest.resolve()), + targetManifestSha256=digest(args.target_manifest), routes=[]) + rows = previous["routes"] if args.kind == "frozen" else previous["sources"] + for index, row in enumerate(rows): + outcome = dict(index=index, status="RUNNING") + report["routes"].append(outcome) + try: + with socket.socket() as listener: + listener.bind(("127.0.0.1", args.port)) + if args.kind == "frozen": + old = Path(row["runtime"]) / "native114-provenance.json" + if digest(old) != row["provenanceSha256"]: + raise AssertionError("Archived fixture provenance changed") + provenance = read_json(old) + source = Path(provenance["source"]) + hashes = output / f"source-{index:02}-approved-hashes.json" + write_json(hashes, provenance["sourceHashes"]) + selected = snapshot_name(source, provenance["sourceSnapshotSha256"]) + settings = Namespace(source=source, source_manifest=Path(provenance["sourceManifest"]), + source_key=provenance["sourceKey"], source_hashes=hashes, + source_snapshot=selected, target_manifest=args.target_manifest, + java8=args.java8, output=output / f"route-{index:02}") + stage = native114.stage + def native_stage(runtime, profile): + launcher = stage(runtime, profile) + properties = runtime / "server.properties" + lines = [line for line in properties.read_text().splitlines() + if not line.startswith(("server-port=", "generator-settings="))] + properties.write_text("\n".join(lines) + f"\nserver-port={args.port}\ngenerator-settings=\n", encoding="utf-8") + return launcher + with patch.object(native114, "stage", native_stage): + code = native114.run(settings) + outcome.update(source=str(source), sourceSnapshot=selected, runtime=str(settings.output), + comparison=read_json(settings.output / "state-comparison.json")) + else: + old = Path(row["report"]) + if digest(old) != row["sha256"]: + raise AssertionError("Archived connected provenance changed") + provenance = read_json(old) + source = Path(provenance["source"]) + hashes = Path(provenance["sourceHashes"]) + if digest(hashes) != provenance["sourceHashesSha256"]: + raise AssertionError("Archived source fingerprint changed") + settings = Namespace(source=source, source_manifest=Path(provenance["sourceManifest"]), + source_key=provenance["sourceKey"], source_hashes=hashes, + source_checkpoint=provenance["sourceCheckpoint"], target_manifest=args.target_manifest, + java8=args.java8, output=output / f"route-{index:02}", + ticks=512, port=args.port) + code = native114_connected.run(settings) + outcome.update(source=str(source), runtime=str(settings.output), + result=str(settings.output / "native114-connected-results.json")) + outcome["status"] = "PASS" if code == 0 else "FAIL" + except Exception as error: + outcome.update(status="FAIL", failure=str(error), traceback=traceback.format_exc()) + write_json(output / "native114-refresh-results.json", report) + print(f"{args.kind}/{index + 1}/{len(rows)}: {outcome['status']}", flush=True) + report["status"] = "PASS" if len(rows) == (13 if args.kind == "frozen" else 5) and all( + row["status"] == "PASS" for row in report["routes"]) else "FAIL" + write_json(output / "native114-refresh-results.json", report) + return int(report["status"] != "PASS") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("kind", choices=("frozen", "connected")) + parser.add_argument("--previous", type=Path, required=True) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, default=25595) + raise SystemExit(run(parser.parse_args())) diff --git a/test-rig/migration/native114_respawn.py b/test-rig/migration/native114_respawn.py new file mode 100644 index 0000000..71a4abe --- /dev/null +++ b/test-rig/migration/native114_respawn.py @@ -0,0 +1,129 @@ +"""Retained dimension/player fixtures through native 1.14 death and respawn.""" + +import argparse +import copy +import itertools +import json +from pathlib import Path +import shutil +import socket + +import nbtlib +from actors import enchantments, semantic +from dimensions import PLAYER_STATE, exact_item_differences +from native114_actors import ROUTES, baseline +from matrix import WORLD, Server, copy_fixture, differences, digest, fingerprint, parse_nbt, write_json +from native114 import output_path, pinned_profile, require_clean_log, require_disjoint +from respawn import respawn_verdict, spawn_reference + + +def native_respawn_verdict(record, registry, world_spawn): + # Only translate the exact renamed platform class. Unknown handlers still fail. + value = copy.deepcopy(record) + if value.get("respawn", {}).get("handler") == "ServerPlayNetHandler.PERFORM_RESPAWN": + value["respawn"]["handler"] = "NetHandlerPlayServer.PERFORM_RESPAWN" + return respawn_verdict(value, registry, world_spawn) + + +def run(args): + from actors import boot + root = output_path(args.output) + require_disjoint(root, args.fixtures) + target = pinned_profile(args.target_manifest.resolve(), "target114") + with socket.socket() as port: + port.bind(("127.0.0.1", args.port)) + root.mkdir(parents=True) + results = {} + sources = {} + for route in args.routes: + source = args.fixtures / route + before = fingerprint(source) + sources[route] = {"path": str(source.resolve()), "fingerprint": before} + write_json(root / "source-fingerprints.json", sources) + runtime = root / route + launcher = boot(runtime, target, args.java8, args.port) + copy_fixture(source, runtime) + shutil.copy2(source / "dimension-fixture.json", runtime / "dimension-fixture.json") + props = runtime / "server.properties" + props.write_text(props.read_text().replace("allow-nether=false", "allow-nether=true")) + definition = json.loads((runtime / "dimension-fixture.json").read_text()) + player_file = runtime / WORLD / "playerdata" / (definition["playerUuid"] + ".dat") + expected_player = baseline(source, "dimensions")[0]["file"] + expected_items = {key: expected_player[key] for key in ("Inventory", "EnderItems")} + migrated_player = None + try: + for boot_label in ("respawn", "respawn-restart"): + server = Server(runtime, args.java8, launcher, boot_label) + try: + if migrated_player is None: + # Let the real pre-datafix world hook migrate the copy once. + # Independent deaths restore that native player, not raw old bytes + # after the hook has already completed for this process. + migrated_player = player_file.read_bytes() + bootstrap = semantic(nbtlib.load(player_file).unpack(), PLAYER_STATE, enchantments(runtime)) + bootstrap_issues = differences(expected_items, bootstrap, "initial-migration") + bootstrap_issues.extend(exact_item_differences(expected_items, bootstrap, "initial-migration")) + if bootstrap_issues: + write_json(runtime / "bootstrap-failure.json", bootstrap_issues) + raise AssertionError("Native player bootstrap lost or changed source inventory") + folder = runtime / "bootstrap-evidence" + folder.mkdir() + shutil.copy2(player_file, folder / "player.dat") + shutil.copy2(runtime / WORLD / "level.dat", folder / "level.dat") + for keep, mode in itertools.product((True, False), ("forced", "bed", "missing")): + label = boot_label + "-" + ("keep" if keep else "drop") + "-" + mode + player_file.write_bytes(migrated_player) + definition["respawn"] = {"keepInventory": keep, "bedMode": mode} + write_json(runtime / "dimension-fixture.json", definition) + server.send("advdimensions respawn " + label) + server.wait("ADVDIMENSIONS COMPLETE " + label, 120) + server.send("save-all flush") + server.wait("Saved the", 45) + record = json.loads((runtime / f"dimensions-{label}.json").read_text()) + registry = enchantments(runtime) + verdict = native_respawn_verdict(record, registry, spawn_reference(runtime / WORLD / "level.dat")) + observed_before = semantic(parse_nbt(record["respawn"]["before"]), PLAYER_STATE, registry) + source_issues = differences(expected_items, observed_before, "source-inventory") + source_issues.extend(exact_item_differences(expected_items, observed_before, "source-inventory")) + verdict["differences"].extend(source_issues) + if source_issues: + verdict["status"] = "FAIL" + saved = semantic(nbtlib.load(player_file).unpack(), PLAYER_STATE, registry) + observed = semantic(parse_nbt(record["respawn"]["after"]), PLAYER_STATE, registry) + issues = differences(observed, saved, "saved-player") + exact_item_differences(observed, saved, "saved-player") + verdict["differences"].extend(issues) + if issues: + verdict["status"] = "FAIL" + folder = runtime / "respawn-evidence" / label + folder.mkdir(parents=True) + shutil.copy2(player_file, folder / "player.dat") + shutil.copy2(runtime / WORLD / "level.dat", folder / "level.dat") + results[route + "/" + label] = verdict + write_json(root / "results.json", results) + print(route + "/" + label + "=" + verdict["status"], flush=True) + finally: + server.close() + require_clean_log(runtime / ("console-" + boot_label + ".log")) + finally: + unchanged = fingerprint(source) == before + write_json(runtime / "source-integrity.json", {"unchanged": unchanged}) + if not unchanged: + raise AssertionError("Protected respawn source changed") + status = "FAIL" if any(result["status"] != "PASS" for result in results.values()) else "PASS" + write_json(root / "summary.json", {"status": status, "cases": len(results), "results": results, + "targetManifest": str(args.target_manifest.resolve()), "targetManifestSha256": digest(args.target_manifest), + "sourceFixtures": "UNCHANGED", "realClientLoginAndDeathScreen": "NOT_TESTED", + "oracleAdapter": "Exact ServerPlayNetHandler to NetHandlerPlayServer class rename only"}) + return int(status != "PASS") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--fixtures", type=Path, required=True) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, choices=range(25584, 25588), required=True) + parser.add_argument("--routes", nargs="+", choices=ROUTES, + default=["original110", "maintained112", "original110-chained-113"]) + raise SystemExit(run(parser.parse_args())) diff --git a/test-rig/migration/native114_ticket_networks.py b/test-rig/migration/native114_ticket_networks.py new file mode 100644 index 0000000..e324957 --- /dev/null +++ b/test-rig/migration/native114_ticket_networks.py @@ -0,0 +1,175 @@ +"""Ticket-halo-aware native network layouts; historical oracles remain unchanged.""" + +from pathlib import Path +import json +import shutil +import time + +import extended_lifecycle as historical +from matrix import digest, write_json +from native114 import RIG, jar_identity, output_path, pinned_profile, require_disjoint +import native114_extended +from native114_partial import (NativeExtendedRun, NativePartialRun, campaign, + common_parser, exercise_bridge, exercise_partial, + selected_chunks) + + +def halo_chunks(fixture, group): + cores = fixture["groups"][group] + return [[x, z] for x in range(fixture["min"][0] >> 4, (fixture["max"][0] >> 4) + 1) + for z in range(fixture["min"][2] >> 4, (fixture["max"][2] >> 4) + 1) + if any(max(abs(x - a), abs(z - b)) <= 2 for a, b in cores)] + + +def linear_fixtures(): + result = [] + for index, name in enumerate(("C-STEAM", "C-ELECTRIC", "C-FLUID")): + base = 20480 + index * 512 + origin, sink = [base + 8, 80, 2056], [base + 136, 80, 2056] + pipes = [[x, 80, 2056] for x in range(base + 9, base + 136)] + fixture = dict(id=name, nativeTicketNetwork=True, min=[base, 72, 2048], max=[base + 159, 90, 2079], + witnesses=[origin, *pipes[:3], sink, *pipes[3:]], pipes=pipes, + groups=dict(supply=[[base >> 4, 128]], machine=[[(base >> 4) + 8, 128]])) + for group in ("supply", "machine"): + fixture["groups"][group + "halo"] = halo_chunks(fixture, group) + result.append(fixture) + return result + + +def bridge_fixtures(): + result = [] + for index, name in enumerate(("C-STEAM", "C-ELECTRIC", "C-FLUID")): + base = 24576 + index * 512 + origin, sink = [base + 13, 80, 2056], [base + 9, 80, 2056] + path = [[base + 13, 80, z] for z in range(2057, 2185)] + path += [[base + x, 80, 2184] for x in range(12, 8, -1)] + path += [[base + 9, 80, z] for z in range(2183, 2056, -1)] + fixture = dict(id=name, nativeTicketNetwork=True, min=[base, 72, 2048], max=[base + 31, 90, 2207], + witnesses=[origin, sink, *path], pipes=path, + groups=dict(bridge=[[base >> 4, 136]], endpoints=[[base >> 4, 128]])) + fixture["groups"]["bridgehalo"] = halo_chunks(fixture, "bridge") + result.append(fixture) + return result + + +def extended_fixtures(): + return [f for f in historical.fixtures() if "route" in f] + linear_fixtures() + + +def archive_harness(base_manifest, harness, output): + """Generate a new immutable input set, never editing its parent manifest.""" + output = output_path(output) + require_disjoint(output, base_manifest) + require_disjoint(output, harness) + pinned_profile(base_manifest.resolve(), "target114") + definition = json.loads(base_manifest.read_text()) + entry = next(j for j in definition["target114"]["jars"] if j["id"] == "advantageworkstest") + jar_identity(harness, entry["id"], entry["version"]) + checksum = digest(harness) + archive = RIG / "build/native114-artifacts" / checksum / harness.name + archive.parent.mkdir(parents=True, exist_ok=True) + if archive.exists(): + if digest(archive) != checksum: + raise AssertionError("Immutable harness archive changed") + else: + shutil.copy2(harness, archive) + if digest(archive) != checksum or digest(harness) != checksum: + raise AssertionError("Harness changed during archive staging") + entry.update(path=str(archive.resolve()), sha256=checksum, originPath=str(harness.resolve())) + definition["target114"].update(sourceManifest=str(base_manifest.resolve()), sourceManifestSha256=digest(base_manifest)) + write_json(output, definition) + pinned_profile(output, "target114") + return output + + +def fill_commands(definition, command): + parts = command.split() + if len(parts) != 9 or parts[0] != "fill" or parts[7:] != ["minecraft:air", "replace"]: + return None + minimum, maximum = list(map(int, parts[1:4])), list(map(int, parts[4:7])) + f = next((f for f in definition if f.get("nativeTicketNetwork") and f["min"] == minimum and f["max"] == maximum), None) + if f is None: + return None + result = [] + for x in range(minimum[0], maximum[0] + 1, 16): + for z in range(minimum[2], maximum[2] + 1, 16): + low = [x, minimum[1], z] + high = [min(x + 15, maximum[0]), maximum[1], min(z + 15, maximum[2])] + result.append("fill " + " ".join(map(str, low + high)) + " minecraft:air replace") + return result + + +class NativeTicketNetworkRun(NativePartialRun): + def command(self, command): + pieces = fill_commands(self.definition, command) + for piece in pieces if pieces is not None else [command]: + super().command(piece) + + def probe(self, operation, label): + if operation.startswith("release-"): + group = operation[8:] + if selected_chunks(self.definition, group + "halo"): + before = self._raw_probe("snapshot", label + "-native-before") + self._raw_probe("release-" + group + "halo", label + "-native-halo") + result = self._raw_probe(operation, label) + self._released_groups[group] = dict(before=before, selected=selected_chunks(self.definition, group), + deadline=time.monotonic() + 45) + return result + if operation.startswith("load-") and selected_chunks(self.definition, operation[5:] + "halo"): + self._raw_probe(operation + "halo", label + "-native-halo") + return super().probe(operation, label) + + +class NativeTicketExtendedRun(NativeTicketNetworkRun, NativeExtendedRun): + def build(self): + self.probe("load", "loaded") + for f in self.definition: + self.command("fill " + " ".join(map(str, f["min"] + f["max"])) + " minecraft:air replace") + if "route" in f: + self.block(f["controls"][0], "minecraft:redstone_block") + self.block(f["from"], f["block"] + "[facing=" + f["facing"] + "]") + for pos in f["route"][1:] + f.get("alternatives", []): + self.block(pos, f["block"].replace("_drill", "_track")) + for pos in f["route"]: + self.block(historical.offset(pos, f["direction"]), "minecraft:bedrock") + self.inventory(f["from"], 0, 'minecraft:dirt{AdvWorksWitness:"' + f["id"] + '"}', 7) + self.inventory(f["from"], 1, "minecraft:apple", 13) + continue + fluid, steam = f["id"] == "C-FLUID", f["id"] == "C-STEAM" + pipe = "poweradvantage:fluid_pipe" if fluid else "steamadvantage:steam_pipe" if steam else "electricadvantage:electric_conduit" + self.block(f["witnesses"][0], pipe if fluid else "poweradvantage:infinite_" + ("steam" if steam else "electricity")) + for pos in f["pipes"]: + self.block(pos, pipe) + self.block(f["witnesses"][4], "poweradvantage:fluid_metal_tank" if fluid else + "steamadvantage:steam_furnace" if steam else "electricadvantage:electric_furnace") + if not fluid: + if steam: + self.inventory(f["witnesses"][4], 0, "minecraft:coal", 64) + self.inventory(f["witnesses"][4], 1 if steam else 0, "minecraft:iron_ore", 4) + self.probe("offer", "commission-fluid") + self.probe("watch", "watching") + + +def main(): + parser = common_parser(__doc__) + parser.add_argument("--campaign", choices=("partial", "bridge", "extended"), required=True) + args = parser.parse_args() + definition = {"partial": linear_fixtures, "bridge": bridge_fixtures, "extended": extended_fixtures}[args.campaign]() + operation = {"partial": exercise_partial, "bridge": exercise_bridge, "extended": native114_extended.exercise}[args.campaign] + runner_type = NativeTicketNetworkRun if args.campaign == "bridge" else NativeTicketExtendedRun + own_path, own_hash = Path(__file__), digest(Path(__file__)) + def exercise(runner, progress): + archive = runner.runtime / "native114-ticket-network-protocol.py" + shutil.copy2(own_path, archive) + result = operation(runner, progress) + result["native_ticket_protocol_unchanged"] = digest(own_path) == own_hash == digest(archive) + result["native_ticket_protocol_sha256"] = own_hash + if not result["native_ticket_protocol_unchanged"]: + result["status"] = "FAIL" + progress(result) + return result + return campaign(args, "halo-" + args.campaign, definition, exercise, runner_type) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test-rig/migration/native114_transformer_order.py b/test-rig/migration/native114_transformer_order.py new file mode 100644 index 0000000..bf9f480 --- /dev/null +++ b/test-rig/migration/native114_transformer_order.py @@ -0,0 +1,193 @@ +"""Observe and reverse real native Forge 28 Base/Power migration transformers.""" + +import argparse +import json +from pathlib import Path +import shutil +import socket +import subprocess +import time +import types + +import actors +import transformer_order as historical +from matrix import WORLD, Server, compare, copy_fixture, digest, fingerprint, write_json +from native114 import LEGACY_POWER, native_normalized, output_path, pinned_profile, require_clean_log, require_disjoint + + +COREMODS = Path("libraries/net/minecraftforge/coremods/1.0.0/coremods-1.0.0.jar") +SUFFIXES = { + "world_info": ("net/minecraft/world/storage/SaveFormat", ["prepareLegacyWorld"]), + "chunk_status": ("net/minecraft/world/chunk/storage/ChunkLoader", ["prepareLegacyChunk", "finalizeLegacyChunk"]), + "worldgen_guard": historical.SUFFIXES["worldgen_guard"], +} +# Execute the historical evidence oracle unchanged, binding only target class names. +native_order_evidence = types.FunctionType(historical.order_evidence.__code__, + dict(historical.order_evidence.__globals__, SUFFIXES=SUFFIXES), "native_order_evidence", + historical.order_evidence.__defaults__) + + +def build_bridge(root, target, java): + seed = target[0] + libraries = seed / "libraries" + dependencies = [libraries / path for path in ( + "org/ow2/asm/asm/6.2/asm-6.2.jar", "org/ow2/asm/asm-tree/6.2/asm-tree-6.2.jar", + "cpw/mods/modlauncher/4.1.0/modlauncher-4.1.0.jar")] + dependencies.append(next((libraries / "com/google/code/gson/gson").rglob("gson-*.jar"))) + classes = root / "probe-classes" + classes.mkdir() + source = Path(__file__).parent / "transformer-order-probe/TransformerOrderBridge.java" + cp = ";".join(str(path) for path in dependencies) + commands = [[str(java.with_name("javac.exe")), "-source", "8", "-target", "8", "-encoding", "UTF-8", + "-cp", cp, "-d", str(classes), str(source)], + [str(java), "-cp", str(classes) + ";" + cp, "qualification.order.TransformerOrderBridge", + str(seed / COREMODS), str(root / "coremods-order-test.jar"), str(classes)]] + for command in commands: + result = subprocess.run(command, capture_output=True, text=True, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)) + if result.returncode: + raise RuntimeError(result.stdout + result.stderr) + proof = historical.validate_patch(seed / COREMODS, root / "coremods-order-test.jar") + proof.update({"sourceSha256": digest(source), "commands": commands, + "dependencies": {str(path): digest(path) for path in dependencies}}) + write_json(root / "probe-build.json", proof) + return root / "coremods-order-test.jar" + + +def checkpoint(runtime, java, launcher, label, first): + server = Server(runtime, java, launcher, label) + try: + server.snapshot(label) + actors.actor_command(server, True, "snapshot" if first else "read", label) + frozen = actors.freeze_result(json.loads((runtime / f"actors-{label}.json").read_text())) + state = actors.capture(runtime, label) + historical.retain_player_evidence(runtime, label) + if first: + actors.actor_command(server, True, "read", label + "-adapter") + adapter = actors.capture(runtime, label + "-adapter") + adapter_freeze = actors.freeze_result(json.loads((runtime / f"actors-{label}-adapter.json").read_text())) + historical.retain_player_evidence(runtime, label + "-adapter") + else: + adapter, adapter_freeze = state, frozen + adapter.pop("embedded", None) + if not first: + state.pop("embedded", None) + if frozen["status"] != "PASS" or adapter_freeze["status"] != "PASS": + raise AssertionError("Transformer controls require genuinely frozen actors") + remote = 1024 if first else 2048 + server.send(f"forceload add {remote} 1024") + server.send("say NATIVE114-ORDER-GUARD-SCHEDULED") + server.wait("NATIVE114-ORDER-GUARD-SCHEDULED", 30) + event_file = runtime / "transformer-order-events.jsonl" + deadline = time.monotonic() + 90 + while time.monotonic() < deadline: + if event_file.exists() and event_file.read_text().count('"class":"net/minecraft/world/gen/WorldGenRegion"') == 2: + break + time.sleep(0.2) + else: + raise AssertionError("Native remote generation did not apply both real guard transformers") + server.send(f"forceload remove {remote} 1024") + server.send("save-all flush") + server.wait("Saved the", 45) + finally: + server.close() + require_clean_log(runtime / f"console-{label}.log") + proof = native_order_evidence(runtime, (runtime / "transformer-order.mode").read_text().strip()) + for old, new in (("transformer-order-events.jsonl", f"transformer-order-events-{label}.jsonl"), + ("transformer-order-bytecode", f"transformer-order-bytecode-{label}")): + shutil.move(str(runtime / old), str(runtime / new)) + write_json(runtime / f"order-proof-{label}.json", proof) + write_json(runtime / f"semantic-{label}.json", state) + write_json(runtime / f"semantic-{label}-adapter.json", adapter) + return state, adapter, {"status": "PASS", "order": proof, "freeze": frozen, "adapterFreeze": adapter_freeze} + + +def run(args): + root = output_path(args.output) + require_disjoint(root, args.fixtures) + target = pinned_profile(args.target_manifest.resolve(), "target114") + if not args.probe_only: + with socket.socket() as port: + port.bind(("127.0.0.1", args.port)) + root.mkdir(parents=True) + patched = build_bridge(root, target, args.java8) + if args.probe_only: + return 0 + results, cross, fingerprints = {}, {}, {} + for name in args.sources: + source = args.fixtures / name + before = fingerprint(source) + if fingerprint(source / WORLD) != json.loads((source / "fixture-hashes.json").read_text()): + raise AssertionError("Protected original actor fixture changed") + fingerprints[name] = {"source": str(source.resolve()), "fingerprint": before} + write_json(root / "source-fingerprints.json", fingerprints) + try: + mixed = root / (name + "-mixed-input") + mixed.mkdir() + expected = historical.prepare_fixture(source, mixed) + mixed_hashes = fingerprint(mixed) + blocks = LEGACY_POWER / "run-migration-matrix" / ( + "original-cyano-20261005-r1/original110" if name == "original110" else "full-20261005-r1/maintained112") + modes = {} + for mode in args.orders: + runtime = root / (name + "-" + mode) + launcher = actors.boot(runtime, target, args.java8, args.port) + shutil.copy2(patched, runtime / COREMODS) + (runtime / "transformer-order.mode").write_text(mode) + copy_fixture(mixed, runtime) + shutil.copy2(mixed / "actor-fixture.json", runtime / "actor-fixture.json") + converted, adapter, first = checkpoint(runtime, args.java8, launcher, "converted", True) + backups = historical.migration_backups(runtime) + provenance = historical.backup_provenance(mixed, runtime) + restarted, _, second = checkpoint(runtime, args.java8, launcher, "restarted", False) + checks = { + "conversion": actors.compare({k: v for k, v in expected.items() if k != "adapter"}, converted), + "adapter": actors.compare({k: v for k, v in expected.items() if k != "embedded"}, adapter), + "restart": actors.compare({k: v for k, v in expected.items() if k != "embedded"}, restarted), + "idempotence": historical.strict_equal(adapter, restarted), + "backup-stability": historical.strict_equal(backups, historical.migration_backups(runtime)), + "backup-provenance": provenance, "converted-controls": first, "restart-controls": second, + } + for label in ("converted", "restarted"): + entries = json.loads((runtime / f"migration-{label}.json").read_text()) + stock = runtime / f"migration-stock-{label}.json" + write_json(stock, entries[:-len(historical.METAL_BLOCKS)]) + checks["blocks-" + label] = compare(blocks / "migration-source.json", stock, native_normalized) + checks["metals-" + label] = historical.strict_equal(list(historical.METAL_BLOCKS), + [entry["actualName"] for entry in entries[-len(historical.METAL_BLOCKS):]]) + historical.assert_staged(runtime) + modes[mode] = {"actors": adapter, "actorsRestart": restarted, + "blocks": historical.block_semantics(runtime, "converted"), + "blocksRestart": historical.block_semantics(runtime, "restarted")} + results[name + "/" + mode] = checks + write_json(runtime / "order-results.json", checks) + write_json(root / "results.json", results) + print(name + "/" + mode + ": " + ", ".join(key + "=" + value["status"] for key, value in checks.items()), flush=True) + cross[name] = {mode: historical.strict_equal(modes[args.orders[0]], value) for mode, value in modes.items()} + if fingerprint(mixed) != mixed_hashes: + raise AssertionError("Disposable mixed input was altered by destination runs") + finally: + if fingerprint(source) != before: + raise AssertionError("Protected transformer source changed") + failed = any(value["status"] != "PASS" for checks in results.values() for value in checks.values()) + failed |= any(value["status"] != "PASS" for modes in cross.values() for value in modes.values()) + dependent = any(not checks["backup-provenance"]["power_backup_matches_original"] for checks in results.values()) + status = "FAIL" if failed else "PASS_WITH_BACKUP_ORDER_DEPENDENCE" if dependent else "PASS" + write_json(root / "summary.json", {"status": status, "semanticStatus": "FAIL" if failed else "PASS", "crossOrder": cross, + "runtimes": len(results), "serverLaunches": len(results) * 2, "sourceFixtures": "UNCHANGED", + "targetManifestSha256": digest(args.target_manifest), "patchedLibrarySha256": digest(patched), + "powerBackupOriginOrderDependent": dependent}) + return int(failed) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--fixtures", type=Path, required=True) + parser.add_argument("--target-manifest", type=Path, required=True) + parser.add_argument("--java8", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--port", type=int, choices=range(25584, 25588), required=True) + parser.add_argument("--sources", nargs="+", choices=("original110", "maintained112"), default=["original110", "maintained112"]) + parser.add_argument("--orders", nargs="+", choices=historical.ORDERS, default=list(historical.ORDERS)) + parser.add_argument("--probe-only", action="store_true") + raise SystemExit(run(parser.parse_args())) diff --git a/test-rig/migration/test_native114.py b/test-rig/migration/test_native114.py new file mode 100644 index 0000000..01d2dc4 --- /dev/null +++ b/test-rig/migration/test_native114.py @@ -0,0 +1,93 @@ +"""Destination-only safety checks; no Minecraft process or source world is opened.""" + +import json +from pathlib import Path +import tempfile +import unittest +import zipfile + +from native114 import LEGACY_POWER, RIG, jar_identity, native_normalized, output_path, pinned_profile, require_clean_log, require_disjoint +from matrix import differences, normalized + + +class Native114DestinationTest(unittest.TestCase): + def setUp(self): + root = RIG / "build/port-evidence" + root.mkdir(parents=True, exist_ok=True) + self.temp = tempfile.TemporaryDirectory(dir=root) + self.root = Path(self.temp.name) + + def tearDown(self): + self.temp.cleanup() + + def test_legacy_source_root_is_explicit_pa113(self): + self.assertEqual(LEGACY_POWER.name, "PowerAdvantage") + self.assertEqual(LEGACY_POWER.parent.name, "PA113") + + def test_output_must_be_new_and_inside_native_build_scope(self): + self.assertEqual(output_path(self.root / "fresh"), self.root / "fresh") + with self.assertRaises(FileExistsError): + output_path(self.root) + with self.assertRaises(ValueError): + output_path(RIG.parent / "world") + + def test_neither_ancestor_nor_descendant_of_source_is_writable(self): + source = self.root / "source" + for output in (source, source / "nested", self.root): + with self.assertRaises(ValueError): + require_disjoint(output, source) + require_disjoint(self.root / "destination", source) + + def test_native_identity_and_version_are_both_required(self): + jar = self.root / "example.jar" + with zipfile.ZipFile(jar, "w") as archive: + archive.writestr("META-INF/mods.toml", '[[mods]]\nmodId="poweradvantage"\nversion="2.4.5.114041"\n') + jar_identity(jar, "poweradvantage", "2.4.5.114041") + for identity, version in (("steamadvantage", "2.4.5.114041"), ("poweradvantage", "2.4.4.113021")): + with self.assertRaises(ValueError): + jar_identity(jar, identity, version) + + def test_pinned_source_hash_failure_is_not_ignored(self): + artifact = self.root / "input.jar" + artifact.write_bytes(b"not-the-pinned-artifact") + manifest = self.root / "source-profiles.json" + manifest.write_text(json.dumps({"target113": {"seed": ".", "launcher": "forge.jar", + "jars": [{"path": "input.jar", "sha256": "0" * 64}]}})) + with self.assertRaises(AssertionError): + pinned_profile(manifest, "target113") + + def test_native_manifest_rejects_historical_runtime_even_with_no_jars(self): + manifest = self.root / "target.json" + manifest.write_text(json.dumps({"target114": {"seed": ".", "launcher": "forge-1.13.2-25.0.223.jar", "jars": []}})) + with self.assertRaises(AssertionError): + pinned_profile(manifest, "target114") + + def test_native_omitted_empty_chest_lock_is_semantically_unlocked(self): + old = {"id": "Chest", "Lock": "", "Items": []} + new = {"id": "minecraft:chest", "Items": []} + self.assertEqual(native_normalized(old), native_normalized(new)) + self.assertNotIn("Lock", normalized(new)) # Historical default is unchanged. + + def test_missing_or_changed_real_chest_locks_remain_failures(self): + unlocked = native_normalized({"id": "minecraft:chest"}) + locked = native_normalized({"id": "minecraft:chest", "Lock": "secret"}) + self.assertTrue(differences(unlocked, locked)) + self.assertTrue(differences(locked, unlocked)) + self.assertTrue(differences(locked, native_normalized({"id": "minecraft:chest", "Lock": "different"}))) + + def test_custom_machine_empty_key_is_not_dropped_or_defaulted(self): + old = native_normalized({"id": "poweradvantage:machine", "Lock": ""}) + new = native_normalized({"id": "poweradvantage:machine"}) + self.assertTrue(differences(old, new)) + + def test_startup_errors_cannot_hide_behind_a_successful_snapshot(self): + log = self.root / "console.log" + log.write_text("[Server thread/INFO] Done\n[Server thread/ERROR] Failed to reload data packs\n") + with self.assertRaises(AssertionError): + require_clean_log(log) + log.write_text("[Server thread/WARN] No data fixer registered for custom entity\n[Server thread/INFO] Done\n") + require_clean_log(log) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_actors.py b/test-rig/migration/test_native114_actors.py new file mode 100644 index 0000000..cce118b --- /dev/null +++ b/test-rig/migration/test_native114_actors.py @@ -0,0 +1,66 @@ +import unittest +from pathlib import Path +import tempfile +from unittest.mock import Mock, patch + +from native114_actors import baseline, expected_state, native_dimension_state, release + + +class NativeActorOracleTest(unittest.TestCase): + def test_frozen_comparison_retains_positions_and_nested_items(self): + old = {"file": {"Inventory": [{"Count": 17, "tag": {"nested": {"keep": 3}}}]}, + "entities": {"cart": {"Pos": [1, 2, 3], "Items": [{"Count": 4}]}}} + result = expected_state(old, "actors") + self.assertEqual(old, result) + result["entities"]["cart"]["Items"][0]["Count"] = 0 + self.assertEqual(4, old["entities"]["cart"]["Items"][0]["Count"]) + + def test_survival_only_removes_historical_movement_keys(self): + old = {"file": {}, "entities": {"wolf": {"Pos": [1, 2, 3], "Motion": [0, 0, 0], + "Rotation": [0, 0], "OwnerUUID": "owner", "ForgeData": {"keep": 8}}}} + self.assertEqual({"OwnerUUID": "owner", "ForgeData": {"keep": 8}}, + expected_state(old, "actors", survival=True)["entities"]["wolf"]) + + def test_dimension_oracle_keeps_live_relationships_and_quantities(self): + old = {"file": {"Inventory": [{"id": "steamadvantage:musket", "Count": 3}]}, + "entities": {"horse": {"Pos": [1, 2, 3], "Health": 20}}, + "relationships": {"horse": {"passengerUuids": ["wolf"]}}, "adapter": {"Dimension": -1}} + result = expected_state(old, "dimensions", survival=True) + self.assertEqual(old["relationships"], result["relationships"]) + self.assertEqual(old["file"], result["file"]) + + def test_native_leash_byte_is_derived_only_from_actual_saved_compound(self): + state = {"entities": {"horse": {"id": "minecraft:horse", "Leash": {"X": 1, "Y": 2, "Z": 3}}, + "wolf": {"id": "minecraft:wolf"}}, "relationships": {"horse": {"leashHolderType": "minecraft:leash_knot"}}} + result = native_dimension_state(state) + self.assertEqual(1, result["entities"]["horse"]["Leashed"]) + self.assertEqual(0, result["entities"]["wolf"]["Leashed"]) + self.assertNotIn("Leashed", state["entities"]["horse"]) + del state["entities"]["horse"]["Leash"] + self.assertEqual(0, native_dimension_state(state)["entities"]["horse"]["Leashed"]) + + def test_dimension_release_never_saves_before_native_initialization(self): + server = Mock() + release(server, Path("unused"), "dimensions", "released") + server.send.assert_called_once_with("advdimensions operate released") + server.wait.assert_called_once_with("ADVDIMENSIONS COMPLETE released", 120) + + def test_actor_release_requests_real_tickets_not_synthetic_ticks(self): + server = Mock() + with patch("native114_actors.command", return_value="captured") as command: + self.assertEqual("captured", release(server, Path("unused"), "actors", "released")) + self.assertEqual("forceload add 96 -32 223 47", server.send.call_args_list[0].args[0]) + command.assert_called_once_with(server, Path("unused"), "actors", "operate", "released") + + def test_retained_baseline_selection_includes_player_layout_restart(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + for selected in ("source", "restarted", "runtime-restart"): + (root / ("semantic-" + selected + ".json")).write_text("{}") + with patch("native114_actors.actors.capture", return_value={"retain": True}) as capture: + self.assertEqual(({"retain": True}, selected), baseline(root, "actors")) + capture.assert_called_once_with(root, selected) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_broad.py b/test-rig/migration/test_native114_broad.py new file mode 100644 index 0000000..4bd6b42 --- /dev/null +++ b/test-rig/migration/test_native114_broad.py @@ -0,0 +1,185 @@ +"""Negative controls for the repaired broad drill oracle, not Minecraft passes.""" + +from copy import deepcopy +import json +from pathlib import Path +import tempfile +import unittest + +import nbtlib +import native114_broad as broad + + +class BroadOracleTests(unittest.TestCase): + def setUp(self): + definition = broad.read_json(broad.RIG / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json") + self.stations = [s for s in definition["stations"] if s["id"] in broad.DRILL_BAYS] + + def snapshot(self, station, moved=False): + rows = [] + for case in broad.drill_cases(station): + position = case["rear"] if moved else case["origin"] + marker = json.dumps(case["id"]) + data = ('{id:"' + case["block"] + '",x:' + str(position[0]) + ',y:' + str(position[1]) + + ',z:' + str(position[2]) + ',ForgeData:{AdvBayWitness:' + marker + '},Energy:[I;0],' + + 'Items:[{Slot:0b,id:"minecraft:diamond",Count:7b,tag:{AdvBayStock:' + marker + '}}]}') + row = dict(pos=list(position), block=case["block"], state='facing=' + case["facing"], nbt=data) + rows.append(row) + if not moved: + rows.append(dict(pos=list(case["rear"]), block="steamadvantage:steam_track" + if station["id"] == "S-05" else "electricadvantage:electric_track", state="track")) + if moved: + rows.append(dict(pos=list(case["origin"]), block="steamadvantage:steam_pipe" + if station["id"] == "S-05" else "electricadvantage:electric_conduit", state="pipe")) + return dict(snapshot=rows) + + def states(self, moved=False): + states = {} + for station in self.stations: + states.update(broad.drill_state(station, self.snapshot(station, moved), initial=not moved)) + return states + + def test_all_twelve_initial_then_moved_then_restarted(self): + initial, moved = self.states(), self.states(True) + self.assertEqual(12, broad.operating_state(initial, moved)["moved"]) + self.assertTrue(broad.restarted_state(moved, deepcopy(moved))["exactEnergy"]) + + def test_movement_cannot_replace_initial_placement_test(self): + for station in self.stations: + with self.assertRaisesRegex(AssertionError, "initial placement"): + broad.drill_state(station, self.snapshot(station, True), initial=True) + + def test_no_movement_cannot_clear_the_original_failure(self): + with self.assertRaisesRegex(AssertionError, "actual track movement"): + broad.operating_state(self.states(), self.states()) + + def test_initial_track_is_required(self): + station = self.stations[0] + doc = self.snapshot(station) + doc["snapshot"][1]["block"] = "minecraft:air" + with self.assertRaisesRegex(AssertionError, "initial declared rear track"): + broad.drill_state(station, doc, initial=True) + + def test_live_steam_bit_may_replace_the_conduit_left_in_the_old_cell(self): + station = self.stations[0] + doc = self.snapshot(station, True) + doc["snapshot"][1]["block"] = "steamadvantage:drillbit" + self.assertTrue(broad.drill_state(station, doc)["S-05:down-facing"]["moved"]) + + def test_missing_duplicate_wrong_facing_and_off_track_fail(self): + station = self.stations[0] + for mutation in ("missing", "duplicate", "facing", "off-track"): + with self.subTest(mutation=mutation): + doc = self.snapshot(station) + if mutation == "missing": + doc["snapshot"].pop(0) + elif mutation == "duplicate": + clone = deepcopy(doc["snapshot"][0]) + clone["pos"] = list(broad.drill_cases(station)[0]["rear"]) + doc["snapshot"].append(clone) + elif mutation == "facing": + doc["snapshot"][0]["state"] = "facing=west" + else: + doc["snapshot"][0]["pos"][0] += 1 + with self.assertRaises(AssertionError): + broad.drill_state(station, doc) + + def test_witness_and_inventory_and_coordinate_changes_fail(self): + station = self.stations[0] + for mutation in ("marker", "lost-stock", "duplicated-stock", "coordinates", "energy"): + with self.subTest(mutation=mutation): + doc = self.snapshot(station) + data = nbtlib.parse_nbt(doc["snapshot"][0]["nbt"]) + if mutation == "marker": + data["ForgeData"]["AdvBayWitness"] = nbtlib.String("replacement") + elif mutation == "lost-stock": + data["Items"][0]["Count"] = nbtlib.Byte(6) + elif mutation == "duplicated-stock": + data["Items"][0]["Count"] = nbtlib.Byte(8) + elif mutation == "coordinates": + data["x"] = nbtlib.Int(999) + else: + data["Energy"] = nbtlib.IntArray([2139095040]) + doc["snapshot"][0]["nbt"] = data.snbt() + with self.assertRaises(AssertionError): + broad.drill_state(station, doc) + + def test_stock_can_export_to_rear_chest_without_loss(self): + station = self.stations[0] + doc = self.snapshot(station, True) + row = doc["snapshot"][0] + data = nbtlib.parse_nbt(row["nbt"]) + stock = data.pop("Items") + row["nbt"] = data.snbt() + doc["snapshot"].append(dict(pos=[-61, 5, 70], block="minecraft:chest", state="chest", + nbt=nbtlib.Compound({"Items": stock}).snbt())) + self.assertEqual(7, broad.drill_state(station, doc)["S-05:down-facing"]["stock"]) + + def test_energy_loss_during_restart_is_not_accepted(self): + before = self.states(True) + before["E-06:up-facing"]["energyBits"] = [1132068864] + before["E-06:up-facing"]["energy"] = 250 + with self.assertRaisesRegex(AssertionError, "Restart changed"): + broad.restarted_state(before, self.states(True)) + + def test_report_cannot_drop_an_assertion_or_station(self): + station = self.stations[0] + record = dict(station=station["id"], minecraft="1.14.4", status="MANUAL_REQUIRED", + assertions=[dict(id=a["id"], status="PASS") for a in station["assertions"]], + manualChecks=station["manualChecks"]) + self.assertEqual(6, broad.assertions_state({station["id"]: record}, [station])["assertions"]["PASS"]) + with self.assertRaisesRegex(AssertionError, "omitted"): + broad.assertions_state({}, [station]) + record["assertions"].pop() + with self.assertRaisesRegex(AssertionError, "assertion set changed"): + broad.assertions_state({station["id"]: record}, [station]) + + def test_decoupled_modset_cannot_silently_contain_base_metals(self): + expected = dict(minecraft="1.14.4", forge="28.2.26", poweradvantage="2.4.5.114041") + broad.loaded_mods({"S-05": dict(mods=expected)}, expected) + actual = dict(expected, basemetals="3.0.1.114041") + with self.assertRaisesRegex(AssertionError, "loaded mod identities"): + broad.loaded_mods({"S-05": dict(mods=actual)}, expected) + + def test_live_broad_staging_explicitly_disables_frozen_migration_mode(self): + with tempfile.TemporaryDirectory() as directory: + runtime = Path(directory) + (runtime / "advantage-migration.allow").touch() + broad.authorize_broad(runtime) + self.assertTrue((runtime / "advantage-works.allow").is_file()) + self.assertTrue((runtime / "migration-operate.allow").is_file()) + + def test_works_world_retains_original_no_structure_and_seed_settings(self): + with tempfile.TemporaryDirectory() as directory: + runtime = Path(directory) + properties = runtime / "server.properties" + properties.write_text("level-name=AdvantageWorksMigration\nserver-port=25584\ngenerate-structures=true\n") + broad.configure_broad(runtime, 25588) + result = dict(line.split("=", 1) for line in properties.read_text().splitlines()) + self.assertEqual("false", result["generate-structures"]) + self.assertEqual("advantage-works-1.14.4-v1", result["level-seed"]) + self.assertEqual("25588", result["server-port"]) + self.assertEqual("0", result["difficulty"]) + self.assertEqual("true", result["enable-command-block"]) + self.assertEqual("false", result["spawn-npcs"]) + self.assertEqual("AdvantageWorksMigration", result["level-name"]) + + def test_required_integration_skip_is_explained_by_loaded_mods(self): + station = dict(id="W-03", assertions=[dict(id="mineralogy", requiredMods=["mineralogy"])]) + record = dict(station="W-03", minecraft="1.14.4", status="MANUAL_REQUIRED", mods={}, manualChecks=["GUI"], + assertions=[dict(id="mineralogy", status="SKIP")]) + self.assertEqual({"SKIP": 1}, broad.assertions_state({"W-03": record}, [station])["assertions"]) + record["mods"]["mineralogy"] = "6.0.1.114041" + with self.assertRaisesRegex(AssertionError, "unexplained skip"): + broad.assertions_state({"W-03": record}, [station]) + + def test_duplicate_assertions_cannot_be_counted_as_coverage(self): + station = dict(id="W-03", assertions=[dict(id="one")]) + record = dict(station="W-03", minecraft="1.14.4", status="MANUAL_REQUIRED", manualChecks=["GUI"], + assertions=[dict(id="one", status="PASS"), dict(id="one", status="PASS")]) + with self.assertRaisesRegex(AssertionError, "assertion set changed"): + broad.assertions_state({"W-03": record}, [station]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_connected.py b/test-rig/migration/test_native114_connected.py new file mode 100644 index 0000000..a26ce27 --- /dev/null +++ b/test-rig/migration/test_native114_connected.py @@ -0,0 +1,253 @@ +"""Native114 connected destination safety and unchanged-oracle regression tests.""" + +import copy +import json +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +import zipfile +from unittest.mock import Mock, patch + +import connected +from matrix import WORLD, digest, fingerprint, write_json +from native114 import LAUNCHER, RIG +import native114_connected as native +from test_connected import advanced, nbt, sample + + +BOOT_PROOF = ("Starting minecraft server version 1.14.4\n" + "Forge mod loading, version 28.2.26, for MC 1.14.4 with MCP 20190829.143755\n") + + +class NativeConnectedTest(unittest.TestCase): + def setUp(self): + root = RIG / "build/port-evidence" + root.mkdir(parents=True, exist_ok=True) + self.temp = tempfile.TemporaryDirectory(dir=root) + self.root = Path(self.temp.name) + self.definition = connected.fixture("original110") + + def tearDown(self): + self.temp.cleanup() + + def source(self): + source = self.root / "original110" + (source / WORLD).mkdir(parents=True) + (source / WORLD / "level.dat").write_bytes(b"unit-fixture-only-no-minecraft-process") + (source / "mods").mkdir() + jar = source / "mods/old.jar" + jar.write_bytes(b"archived-unit-test-artifact") + write_json(source / "input-jars.json", [{"file": jar.name, "sha256": digest(jar)}]) + manifest = self.root / "profiles.json" + write_json(manifest, {"original110": {"seed": str(source), "launcher": "forge-old.jar", + "jars": [{"path": str(jar), "sha256": digest(jar)}]}}) + write_json(source / "connected-fixture.json", self.definition) + write_json(source / "migration-fixture.json", self.definition["blocks"]) + cold, worked = sample(), advanced(sample()) + write_json(source / "connected-cold.json", cold) + write_json(source / "connected-source.json", worked) + write_json(source / "migration-source.json", worked["snapshot"]) + write_json(source / "fixture-hashes.json", fingerprint(source / WORLD)) + write_json(self.root / "connected-results.json", {"status": "FAIL", + "sources": {"original110": {"status": "PASS"}}, "routes": {"old-bridge": {"status": "FAIL"}}}) + return SimpleNamespace(source=source, source_manifest=manifest, source_key="original110", + source_hashes=source / "fixture-hashes.json", source_checkpoint="source", + target_manifest=self.root / "target.json", output=self.root / "new-output", + port=25576, ticks=64, java8=Path("unused-java8")) + + def test_only_observation_save_and_stop_commands_are_permitted(self): + for command in ("stop", "save-all", "save-all flush", "advconnected snapshot imported", + "advconnected step 1 first", "advconnected step 4000 last"): + native.require_command(command) + + def test_build_reset_refill_and_invalid_tick_commands_are_rejected(self): + for command in ("advconnected build source", "advworks reset all", "setblock 0 80 0 stone", + "data merge block 0 80 0 {}", "advconnected step 0 bad", "advconnected step 4001 bad", + "advconnected snapshot ok\nadvworks build all", "advconnected snapshot ../escape"): + with self.subTest(command=command), self.assertRaises(AssertionError): + native.require_command(command) + + def test_operation_cannot_start_before_preservation_gate(self): + server = native.NativeConnectedServer.__new__(native.NativeConnectedServer) + server.loaded_verified = False + with patch.object(connected.ConnectedServer, "send") as sender, self.assertRaises(AssertionError): + server.send("advconnected step 64 worked") + sender.assert_not_called() + + def test_failed_loaded_callback_never_opens_operation_gate(self): + server = native.NativeConnectedServer.__new__(native.NativeConnectedServer) + server.loaded_verified = False + server.on_loaded = Mock(side_effect=AssertionError("lost saved progress")) + with patch.object(connected.ConnectedServer, "observe"), self.assertRaises(AssertionError): + server.observe("snapshot", "converted-loaded") + self.assertFalse(server.loaded_verified) + + def test_valid_destination_document_uses_existing_state_oracle(self): + self.assertEqual(native.require_document(sample(), self.definition, destination=True), connected.state(sample())) + + def test_missing_duplicate_and_moved_witnesses_fail(self): + for mutation in (lambda rows: rows.pop(), lambda rows: rows.append(copy.deepcopy(rows[0])), + lambda rows: rows[0].update(x=99)): + document = sample() + mutation(document["snapshot"]) + with self.assertRaises(AssertionError): + native.require_document(document, self.definition, destination=True) + + def test_rebuild_or_unfrozen_snapshot_is_not_preservation(self): + for field, value in (("buildCallsThisProcess", 1), ("held", False)): + document = sample() + document[field] = value + with self.assertRaises(AssertionError): + native.require_document(document, self.definition, destination=True) + + def test_unchanged_oracle_rejects_quantity_loss_and_wrong_drill_products(self): + for station, contents in (("C-ELECTRIC", '{Items:[]}'), + ("C-EDRILL", '{Items:[{id:"minecraft:gravel",Count:9b}]}')): + document = advanced(sample()) + nbt(connected.entries(document)[station + ("-drill" if station.endswith("DRILL") else "-furnace")], contents) + with self.assertRaises(AssertionError): + native.require_document(document, self.definition, destination=True) + + def test_idle_and_nonmeasured_intervals_still_fail_existing_operation_oracle(self): + before = sample() + idle = copy.deepcopy(before) + idle.update(requestedTicks=64, advancedTicks=64) + self.assertEqual(connected.operation(before, idle, 64)["status"], "FAIL") + worked = advanced(before) + worked["advancedTicks"] = 0 + self.assertEqual(connected.operation(before, worked, 64)["status"], "FAIL") + + def test_finite_water_creation_remains_failure(self): + before = sample() + worked = advanced(before) + nbt(connected.entries(worked)["C-STEAM-water-0"], '{Tank:{FluidName:"water",Amount:10001}}') + self.assertEqual(connected.operation(before, worked, 64)["status"], "FAIL") + + def test_boot_proof_needs_both_native_version_and_clean_log(self): + log = self.root / "console.log" + log.write_text(BOOT_PROOF) + native.require_native_boot(log) + for text in (BOOT_PROOF.replace("1.14.4", "1.13.2"), BOOT_PROOF + "[main/ERROR] native failure\n", + BOOT_PROOF + "java.lang.NoSuchMethodError: example\n"): + log.write_text(text) + with self.assertRaises(AssertionError): + native.require_native_boot(log) + + def test_explicit_source_pins_and_historical_failures_are_preserved(self): + args = self.source() + before = fingerprint(args.source) + definition, snapshot, verdict, actual, historical = native.verify_source(args) + self.assertEqual(definition, self.definition) + self.assertEqual(verdict["status"], "PASS") + self.assertEqual(historical["status"], "FAIL") + self.assertEqual(actual, before) + self.assertEqual(fingerprint(args.source), before) + + def test_changed_source_world_fails_before_staging(self): + args = self.source() + (args.source / WORLD / "level.dat").write_bytes(b"changed") + with patch.object(native, "prepare_target") as prepare, self.assertRaises(AssertionError): + native.run(args) + prepare.assert_not_called() + self.assertFalse(args.output.exists()) + + def test_changed_source_jar_or_wrong_profile_key_fails(self): + args = self.source() + args.source_key = "maintained110" + with self.assertRaises(ValueError): + native.verify_source(args) + args.source_key = "original110" + (args.source / "mods/old.jar").write_bytes(b"changed") + with self.assertRaises(AssertionError): + native.verify_source(args) + + def test_nonconnected_operation_fixture_cannot_be_reconstructed(self): + args = self.source() + (args.source / "connected-fixture.json").unlink() + with patch.object(native, "prepare_target") as prepare, self.assertRaises(FileNotFoundError): + native.run(args) + prepare.assert_not_called() + + def test_target_prepare_copies_only_fixture_and_has_no_build_authority(self): + args = self.source() + (args.source / "connected-build.allow").touch() + (args.source / "connected-built.json").write_text("{}") + before = fingerprint(args.source) + seed = self.root / "native-seed" + (seed / "libraries").mkdir(parents=True) + (seed / LAUNCHER).write_bytes(b"unit-test-launcher-not-executed") + jar = self.root / "target.jar" + jar.write_bytes(b"unit-test-target-not-executed") + runtime = self.root / "target" + pins = {"launcher_sha256": digest(seed / LAUNCHER), "jars": [{"path": str(jar), "sha256": digest(jar)}]} + self.assertEqual(native.prepare_target(runtime, (seed, LAUNCHER, [jar]), self.definition, 25576, args.source, pins), LAUNCHER) + self.assertEqual(fingerprint(runtime / WORLD), fingerprint(args.source / WORLD)) + self.assertFalse((runtime / "connected-build.allow").exists()) + self.assertFalse((runtime / "connected-built.json").exists()) + self.assertIn("server-port=25576", (runtime / "server.properties").read_text()) + self.assertNotIn("3;minecraft:bedrock", (runtime / "server.properties").read_text()) + self.assertEqual(fingerprint(args.source), before) + + def test_non_native_launcher_is_rejected_without_creating_target(self): + runtime = self.root / "target" + with self.assertRaises(ValueError): + native.prepare_target(runtime, (self.root, "forge-1.13.2.jar", []), self.definition, 25576, self.root, {}) + self.assertFalse(runtime.exists()) + + def target(self, args): + seed = self.root / "seed" + (seed / "libraries").mkdir(parents=True) + (seed / LAUNCHER).write_bytes(b"unit-test-launcher-never-executed") + jars = [] + for identity in ("poweradvantage", "steamadvantage", "electricadvantage", "advantageworkstest", "orespawn"): + jar = seed / (identity + ".jar") + with zipfile.ZipFile(jar, "w") as archive: + archive.writestr("META-INF/mods.toml", f'[[mods]]\nmodId="{identity}"\nversion="1.0.0.114041"\n') + jars.append(dict(path=str(jar), id=identity, version="1.0.0.114041", sha256=digest(jar))) + write_json(args.target_manifest, {"target114": dict(seed=str(seed), launcher=LAUNCHER, + launcher_sha256=digest(seed / LAUNCHER), jars=jars)}) + + def test_runtime_failure_is_retained_and_source_is_still_rechecked(self): + args = self.source() + self.target(args) + before = fingerprint(args.source) + with patch.object(native.socket, "socket"), patch.object(native, "prepare_target", side_effect=RuntimeError("retained native failure")), self.assertRaises(RuntimeError): + native.run(args) + report = native.read_json(args.output / "native114-connected-results.json") + self.assertEqual(report["status"], "FAIL") + self.assertEqual(report["failure"], "retained native failure") + self.assertEqual(report["sourceRuntimeAfter"], before) + self.assertEqual(report["sourcePreservation"]["status"], "PASS") + + def test_source_change_on_failure_is_reported_not_hidden(self): + args = self.source() + self.target(args) + def changed(*unused): + (args.source / WORLD / "level.dat").write_bytes(b"unexpected-source-change") + raise RuntimeError("native failure") + with patch.object(native.socket, "socket"), patch.object(native, "prepare_target", side_effect=changed), self.assertRaisesRegex(AssertionError, "Protected source"): + native.run(args) + report = native.read_json(args.output / "native114-connected-results.json") + self.assertEqual(report["status"], "FAIL") + self.assertEqual(report["sourcePreservation"]["status"], "FAIL") + + def test_candidate_change_after_preflight_cannot_replace_pinned_bytes(self): + args = self.source() + seed = self.root / "seed" + (seed / "libraries").mkdir(parents=True) + (seed / LAUNCHER).write_bytes(b"launcher-not-executed") + jar = self.root / "candidate.jar" + jar.write_bytes(b"pinned") + pins = {"launcher_sha256": digest(seed / LAUNCHER), "jars": [{"path": str(jar), "sha256": digest(jar)}]} + jar.write_bytes(b"evolving-after-preflight") + with self.assertRaises(AssertionError): + native.prepare_target(self.root / "target", (seed, LAUNCHER, [jar]), self.definition, 25576, args.source, pins) + + def test_continued_output_cannot_promote_idle_machinery(self): + self.assertEqual(native.continued_output(sample(), advanced(sample()))["status"], "PASS") + self.assertEqual(native.continued_output(sample(), sample())["status"], "FAIL") + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_endurance.py b/test-rig/migration/test_native114_endurance.py new file mode 100644 index 0000000..fb63bdb --- /dev/null +++ b/test-rig/migration/test_native114_endurance.py @@ -0,0 +1,75 @@ +"""Native transport guards; historical material/energy oracles stay inherited.""" + +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import endurance +from matrix import write_json +from native114_endurance import NativeEnduranceRig, audit + + +class NativeEnduranceTests(unittest.TestCase): + def test_batch_and_conservation_oracles_are_inherited(self): + self.assertIs(NativeEnduranceRig.batch, endurance.Rig.batch) + self.assertIs(NativeEnduranceRig.invariants, endurance.Rig.invariants) + self.assertIs(NativeEnduranceRig.inventory, endurance.Rig.inventory) + self.assertIs(NativeEnduranceRig.seed, endurance.Rig.seed) + self.assertIs(NativeEnduranceRig.structure, endurance.Rig.structure) + + def test_restart_selects_first_tick_without_output_or_inventory_inspection(self): + with tempfile.TemporaryDirectory() as directory: + definitions = {s: dict(origin=[0, 0, 0], boundsMin=[0, 0, 0], boundsMax=[31, 20, 31]) + for s in endurance.CHECKED} + rig = NativeEnduranceRig("test", Path(directory), None, None, definitions) + ticks = iter([200, 200, 201]) + observations, commands = [], [] + def checkpoint(label): + observations.append(label) + rig.documents = {"W-01": {"worldTime": next(ticks)}} + rig.checkpoint = checkpoint + rig.command = lambda command, response=None: commands.append(command) + with patch.object(endurance.Rig, "boot"), patch("native114_endurance.time.sleep"): + rig.boot("endurance-restart") + self.assertEqual(len(observations), 3) + self.assertEqual(len(commands), len(endurance.CHECKED)) + self.assertTrue(all(c == "forceload add 0 0 31 31" for c in commands)) + self.assertTrue((Path(directory) / "native-restart-readiness.json").is_file()) + + def test_restart_without_measured_tick_fails(self): + with tempfile.TemporaryDirectory() as directory: + definitions = {s: dict(origin=[0, 0, 0], boundsMin=[0, 0, 0], boundsMax=[31, 20, 31]) + for s in endurance.CHECKED} + rig = NativeEnduranceRig("test", Path(directory), None, None, definitions) + rig.command = lambda *args: None + rig.checkpoint = lambda label: rig.documents.update({"W-01": {"worldTime": 200}}) + with patch.object(endurance.Rig, "boot"), patch("native114_endurance.time.sleep"): + with self.assertRaisesRegex(TimeoutError, "No actual native tick"): + rig.boot("endurance-restart") + + def test_initial_boot_does_not_restore_or_rebuild_anything(self): + rig = NativeEnduranceRig("test", Path("unused"), None, None, {}) + rig.command = lambda *args: self.fail("Initial boot issued a command") + with patch.object(endurance.Rig, "boot"): + rig.boot("endurance") + + def test_smoke_result_never_qualifies_as_endurance(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + write_json(root / "native114-endurance-results.json", dict(status="SMOKE_PASS_NOT_ENDURANCE")) + with self.assertRaisesRegex(AssertionError, "No completed native endurance"): + audit(root) + + def test_missing_native_profile_fails(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + write_json(root / "native114-endurance-results.json", dict( + status="PASS", duration_seconds=1800, interval_seconds=60, + source_fixture_unchanged=True, helpers_unchanged=True, profiles={})) + with self.assertRaisesRegex(AssertionError, "Missing native environment"): + audit(root) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_holder_networks.py b/test-rig/migration/test_native114_holder_networks.py new file mode 100644 index 0000000..0443f09 --- /dev/null +++ b/test-rig/migration/test_native114_holder_networks.py @@ -0,0 +1,58 @@ +"""The full native holder halo is wider than the FULL-status halo.""" + +import unittest +from native114_holder_networks import (FORCED_LEVEL, HOLDER_RADIUS, LAST_HOLDER_LEVEL, + bridge_fixtures, extended_fixtures, linear_fixtures) +from native114_ticket_networks import fill_commands +import extended_lifecycle + + +class HolderNetworkTests(unittest.TestCase): + def test_exact_native_holder_threshold_is_not_confused_with_full_threshold(self): + self.assertEqual(FORCED_LEVEL, 31) + self.assertEqual(LAST_HOLDER_LEVEL, 33 + 11) + self.assertEqual(HOLDER_RADIUS, 13) + self.assertGreater(FORCED_LEVEL + HOLDER_RADIUS + 1, LAST_HOLDER_LEVEL) + + def test_every_retained_ticket_is_outside_full_holder_dependency_radius(self): + for definition in (linear_fixtures(), bridge_fixtures()): + for f in definition: + all_chunks = {(x, z) for x in range(f["min"][0] >> 4, (f["max"][0] >> 4) + 1) + for z in range(f["min"][2] >> 4, (f["max"][2] >> 4) + 1)} + for group in ("bridge",) if "bridge" in f["groups"] else ("supply", "machine"): + retained = all_chunks - set(map(tuple, f["groups"][group + "halo"])) + self.assertTrue(retained) + self.assertTrue(all(min(max(abs(x-a), abs(z-b)) for a, b in f["groups"][group]) > 13 for x, z in retained)) + + def test_long_paths_and_all_witnesses_stay_inside_separate_bays(self): + for definition in (linear_fixtures(), bridge_fixtures(), extended_fixtures()): + for index, f in enumerate(definition): + for p in f["witnesses"]: + self.assertTrue(all(a <= n <= b for a, n, b in zip(f["min"], p, f["max"]))) + for other in definition[index + 1:]: + self.assertTrue(any(f["max"][a] < other["min"][a] or other["max"][a] < f["min"][a] for a in range(3))) + + def test_paths_are_connected(self): + for f in linear_fixtures() + bridge_fixtures(): + sink = f["witnesses"][1] if "bridge" in f["groups"] else f["witnesses"][4] + path = [f["witnesses"][0], *f["pipes"], sink] + self.assertTrue(all(sum(abs(x-y) for x, y in zip(a,b)) == 1 for a,b in zip(path,path[1:]))) + self.assertEqual(len(path), len(set(map(tuple,path)))) + + def test_drill_geometry_is_not_changed_to_accept_network_results(self): + self.assertEqual(extended_fixtures()[:18], [f for f in extended_lifecycle.fixtures() if "route" in f]) + + def test_native_fill_is_still_bounded_below_vanilla_volume_limit(self): + for f in linear_fixtures() + bridge_fixtures(): + commands = fill_commands([f], "fill " + " ".join(map(str,f["min"]+f["max"])) + " minecraft:air replace") + self.assertEqual(len(commands), 68) + for command in commands: + values = list(map(int, command.split()[1:7])) + volume = 1 + for a,b in zip(values[:3],values[3:]): + volume *= b-a+1 + self.assertLessEqual(volume,32768) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_interop.py b/test-rig/migration/test_native114_interop.py new file mode 100644 index 0000000..4426421 --- /dev/null +++ b/test-rig/migration/test_native114_interop.py @@ -0,0 +1,33 @@ +import unittest + +from native114_interop import require_checks + + +class NativeInteropOracleTests(unittest.TestCase): + def document(self, phase="exercise"): + return dict(phase=phase, status="PASS", checks=[dict(id=str(i), status="PASS") + for i in range(20 if phase == "exercise" else 2)]) + + def test_complete_witness_sets_pass(self): + for phase in ("exercise", "restored"): + require_checks(self.document(phase), phase) + + def test_single_conversion_failure_is_not_hidden_by_overall_status(self): + document = self.document() + document["checks"][2].update(status="FAIL", failure="energy disappeared") + with self.assertRaisesRegex(AssertionError, "energy disappeared"): + require_checks(document, "exercise") + + def test_missing_and_duplicate_witnesses_fail(self): + document = self.document() + document["checks"].pop() + with self.assertRaisesRegex(AssertionError, "Incomplete"): + require_checks(document, "exercise") + document = self.document() + document["checks"][1]["id"] = "0" + with self.assertRaisesRegex(AssertionError, "Duplicate"): + require_checks(document, "exercise") + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_lifecycle.py b/test-rig/migration/test_native114_lifecycle.py new file mode 100644 index 0000000..a8edb9e --- /dev/null +++ b/test-rig/migration/test_native114_lifecycle.py @@ -0,0 +1,434 @@ +"""Native lifecycle adapter regressions; no Minecraft process or retained world writes.""" + +import argparse +import copy +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch +from unittest.mock import Mock + +import lifecycle +import native114_lifecycle as native +from matrix import WORLD, digest, fingerprint, write_json +from native114 import RIG + + +class NativeLifecycleTests(unittest.TestCase): + def setUp(self): + root = RIG / "build/port-evidence" + root.mkdir(parents=True, exist_ok=True) + self.temp = tempfile.TemporaryDirectory(dir=root) + self.root = Path(self.temp.name) + + def tearDown(self): + self.temp.cleanup() + + def source_args(self): + source = self.root / "source" + world = source / WORLD + world.mkdir(parents=True) + (world / "level.dat").write_bytes(b"unit-test-world-hash-only") + mods = source / "mods" + mods.mkdir() + jar = mods / "archived-1.13.2.jar" + jar.write_bytes(b"unit-test-pin-not-a-runtime") + write_json(source / "input-jars.json", [{"file": jar.name, "sha256": digest(jar)}]) + history = {key: {"status": "PASS"} for key in ("conversion", "restart", "idempotence")} + write_json(source / "state-comparison.json", history) + write_json(source / "migration-fixture.json", []) + write_json(source / "migration-restarted.json", []) + manifest = self.root / "archived.json" + write_json(manifest, {"target113": {"seed": str(source), "launcher": "forge-1.13.2-25.0.223.jar", + "jars": [{"path": str(jar), "sha256": digest(jar)}]}}) + hashes = self.root / "source-world-hashes.json" + write_json(hashes, fingerprint(world)) + java = self.root / "java.exe" + java.write_bytes(b"not-launched") + target = self.root / "target.json" + write_json(target, {"target114": {"jars": [{"id": key} for key in + ("steamadvantage", "electricadvantage", "basemetals")]}}) + return argparse.Namespace(source=source, source_manifest=manifest, source_key="target113", + source_hashes=hashes, target_manifest=target, java8=java, + output=self.root / "output", port=25580) + + def test_native_subclass_inherits_original_operations_fixtures_and_oracles(self): + self.assertIs(native.lifecycle.LifecycleRun, lifecycle.LifecycleRun) + self.assertIs(native.lifecycle.fixtures, lifecycle.fixtures) + for method in ("build", "midblock", "save_read_guards", "mining_results"): + self.assertIs(getattr(native.NativeLifecycleRun, method), getattr(lifecycle.LifecycleRun, method)) + definition = native.lifecycle.fixtures() + self.assertEqual(len(definition), 15) + drills = [entry for entry in definition if "from" in entry] + self.assertEqual({entry["id"] for entry in drills}, + {prefix + facing for prefix in ("S-MOVE-", "E-MOVE-") for facing in lifecycle.FACINGS}) + for entry in drills: + self.assertNotEqual((entry["from"][0] >> 4, entry["from"][2] >> 4), + (entry["to"][0] >> 4, entry["to"][2] >> 4)) + + def test_full_seven_groups_cannot_pass_missing_or_failed_drills(self): + definition = lifecycle.fixtures() + group = {entry["id"]: {"status": "PASS"} for entry in definition if "from" in entry} + groups = [copy.deepcopy(group) for _ in range(7)] + self.assertTrue(native.require_groups(definition, groups)) + self.assertFalse(native.require_groups(definition, [])) + groups[0].pop("S-MOVE-down") + self.assertFalse(native.require_groups(definition, groups)) + groups[0] = copy.deepcopy(group) + groups[6]["E-MOVE-up"]["status"] = "FAIL" + self.assertFalse(native.require_groups(definition, groups)) + + def test_optional_load_tracing_precedes_build_and_does_not_replace_runner(self): + runner = Mock(runtime=self.root, definition=lifecycle.fixtures()) + runner.build.side_effect = RuntimeError("stop before any real fixture") + with patch.object(native, "require_native_boot"): + with self.assertRaisesRegex(RuntimeError, "stop before"): + native.exercise(runner, "unit", lambda result: None, trace_loads=True) + runner.boot.assert_called_once_with("lifecycle") + runner.probe.assert_called_once_with("trace-loads", "tracing-loads") + self.assertEqual([call[0] for call in runner.mock_calls], ["boot", "probe", "build"]) + + def test_source_preflight_is_read_only(self): + args = self.source_args() + before = fingerprint(args.source) + observed, history = native.verify_source(args) + self.assertEqual(before, observed) + self.assertEqual(before, fingerprint(args.source)) + self.assertEqual(history["restart"]["status"], "PASS") + + def test_changed_source_world_is_rejected(self): + args = self.source_args() + (args.source / WORLD / "level.dat").write_bytes(b"tampered") + with self.assertRaisesRegex(AssertionError, "fingerprint mismatch"): + native.verify_source(args) + + def test_source_mods_must_match_archived_manifest_and_recorded_inputs(self): + args = self.source_args() + (args.source / "mods" / "unexpected.jar").write_bytes(b"extra") + with self.assertRaisesRegex(AssertionError, "source jars differ"): + native.verify_source(args) + + def test_failed_source_state_is_not_reclassified(self): + args = self.source_args() + history = native.read_json(args.source / "state-comparison.json") + history["restart"]["status"] = "FAIL" + write_json(args.source / "state-comparison.json", history) + with self.assertRaisesRegex(AssertionError, "state gate did not pass"): + native.verify_source(args) + + def test_world_hash_keys_are_portable_without_ignoring_files(self): + native.require_equal_hashes({"region/r.0.0.mca": "abc"}, {"region\\r.0.0.mca": "ABC"}, "mismatch") + for actual in ({}, {"region/r.0.0.mca": "DEF"}, {"region/r.0.0.mca": "ABC", "extra": "ABC"}): + with self.assertRaisesRegex(AssertionError, "mismatch"): + native.require_equal_hashes(actual, {"region/r.0.0.mca": "ABC"}, "mismatch") + + def test_wrong_runtime_and_errors_fail_native_boot_check(self): + log = self.root / "console.log" + good = ("Starting minecraft server version 1.14.4\n" + "Forge mod loading, version 28.2.26, for MC 1.14.4 with MCP 20190829.143755\nDone\n") + log.write_text(good) + native.require_native_boot(log) + notice = "[Thread-1/FATAL] [ne.mi.co.ForgeConfig/CORE]: Forge config just got changed on the file system!\n" + log.write_text(good + notice) + native.require_native_boot(log) + for text in (good.replace("1.14.4", "1.13.2"), good + "[Server thread/ERROR] failure\n", + good + "[Server thread/FATAL] failure\n", good + notice.replace("ForgeConfig/CORE", "Other/CORE")): + log.write_text(text) + with self.assertRaises(AssertionError): + native.require_native_boot(log) + + def test_staged_extra_or_changed_jars_fail(self): + runtime = self.root / "runtime" + (runtime / "mods").mkdir(parents=True) + seed = self.root / "seed" + seed.mkdir() + (seed / native.LAUNCHER).write_bytes(b"launcher") + (runtime / native.LAUNCHER).write_bytes(b"launcher") + jar = self.root / "candidate.jar" + jar.write_bytes(b"candidate") + (runtime / "mods" / jar.name).write_bytes(jar.read_bytes()) + write_json(runtime / "input-jars.json", [{"file": jar.name, "sha256": digest(jar)}]) + profile = (seed, native.LAUNCHER, [jar]) + native.verify_staged(runtime, profile) + (runtime / "mods" / "extra.jar").write_bytes(b"unexpected") + with self.assertRaisesRegex(AssertionError, "jar set changed"): + native.verify_staged(runtime, profile) + + def test_failures_are_retained_both_profiles_attempted_and_source_rechecked(self): + args = self.source_args() + source_profile = native.pinned_profile(args.source_manifest, args.source_key) + native_profile = (self.root / "native-seed", native.LAUNCHER, []) + native_profile[0].mkdir() + before = fingerprint(args.source) + + def profile(path, key): + return native_profile if key == "target114" else source_profile + + with patch.object(native, "pinned_profile", side_effect=profile), \ + patch.object(native, "prepare", side_effect=RuntimeError("deliberate unit-test failure")) as prepare: + self.assertEqual(native.run(args), 1) + result = native.read_json(args.output / "native114-lifecycle-results.json") + self.assertEqual(prepare.call_count, 2) + self.assertEqual(result["status"], "FAIL") + self.assertTrue(result["source_fixture_unchanged"]) + self.assertTrue(result["helpers_unchanged"]) + self.assertEqual(result["sourceRuntimeBefore"], result["sourceRuntimeAfter"]) + self.assertEqual(fingerprint(args.source), before) + for row in result["profiles"].values(): + self.assertIn("deliberate unit-test failure", row["failure"]) + + +class NativeReadinessTests(unittest.TestCase): + def setUp(self): + root = RIG / "build/port-evidence" + root.mkdir(parents=True, exist_ok=True) + self.temp = tempfile.TemporaryDirectory(dir=root) + self.root = Path(self.temp.name) + self.definition = [dict(id="drill", min=[0, 0, 0], max=[15, 100, 15], + witnesses=[[1, 80, 1]], to=[1, 80, 1], block="steamadvantage:steam_drill")] + + def tearDown(self): + self.temp.cleanup() + + def snapshot(self, tick=100, loaded=True, loads=1, nbt="{}"): + return dict(worldTime=tick, chunks=[dict(pos=[0, 0], loaded=loaded, loads=loads, unloads=0)], + chunkEvents=[dict(operation="load", pos=[0, 0], worldTime=99)] if loads else [], + cases={"drill": [dict(pos=[1, 80, 1], loaded=loaded, nbt=nbt)]}) + + def runner(self, snapshots): + runner = native.NativeLifecycleRun(self.root, None, None, self.definition) + pending = iter(snapshots) + + def probe(operation, label): + value = copy.deepcopy(next(pending)) + path = self.root / "advantage-works-results" / f"lifecycle-{label}.json" + self.assertFalse(path.exists()) + write_json(path, value) + return value + + runner._raw_probe = Mock(side_effect=probe) + return runner + + def test_readiness_does_not_inspect_machine_nbt_block_or_progress(self): + first, ready = self.snapshot(), self.snapshot(101, nbt="not even parsed") + runner = self.runner([ready]) + with patch.object(native.time, "sleep"): + selected = runner._wait_ready("test", first) + self.assertEqual(selected, ready) + self.assertEqual(runner.observations[-1]["first_ready"]["world_time"], 101) + self.assertEqual(native.loaded_issues(ready, self.definition), []) + + def unloaded(self, tick=101, callbacks=0, events=False): + value = self.snapshot(tick, loaded=False) + value["chunks"][0]["unloads"] = callbacks + if events: + value["chunkEvents"].append(dict(operation="unload", pos=[0, 0], worldTime=tick)) + return value + + def unload_runner(self, snapshots, deadline=45): + runner = self.runner(snapshots) + runner._unload_context = dict(name="cycle", before=self.snapshot(), deadline=deadline) + return runner + + def test_absent_holder_waits_for_queued_real_unload_callback(self): + pending, complete = self.unloaded(), self.unloaded(102, callbacks=1, events=True) + runner = self.unload_runner([pending, complete]) + with patch.object(native.time, "sleep"), patch.object(native.time, "monotonic", return_value=0): + selected = runner.probe("snapshot", "cycle-absent-00") + self.assertEqual(selected, complete) + self.assertEqual(runner._raw_probe.call_count, 2) + lifecycle.require_unloaded(self.snapshot(), selected) + self.assertEqual(native.read_json(self.root / "advantage-works-results/lifecycle-cycle-absent-00.json"), complete) + self.assertTrue((self.root / "advantage-works-results/lifecycle-cycle-absent-00-native-initial.json").exists()) + + def test_queued_callback_wait_never_extends_original_unload_budget(self): + pending = self.unloaded() + runner = self.unload_runner([pending], deadline=1) + with patch.object(native.time, "sleep") as sleep, patch.object(native.time, "monotonic", return_value=1): + selected = runner.probe("snapshot", "cycle-absent-44") + self.assertEqual(selected, pending) + sleep.assert_not_called() + with self.assertRaisesRegex(AssertionError, "No real unload event"): + lifecycle.require_unloaded(self.snapshot(), selected) + + def test_still_loaded_snapshot_returns_to_inherited_polling(self): + loaded = self.snapshot(101) + runner = self.unload_runner([loaded]) + with patch.object(native.time, "sleep") as sleep: + selected = runner.probe("snapshot", "cycle-absent-00") + self.assertEqual(selected, loaded) + sleep.assert_not_called() + + def test_unload_counter_without_an_actual_event_is_not_ready(self): + pending = self.unloaded(callbacks=1) + runner = self.unload_runner([pending], deadline=0) + with patch.object(native.time, "monotonic", return_value=0): + runner.probe("snapshot", "cycle-absent-00") + self.assertEqual(runner.observations[-1]["status"], "PENDING") + self.assertTrue(native.unload_event_issues(self.snapshot(), pending)) + + def test_unload_context_cannot_leak_to_another_cycle_after_failure(self): + runner = self.runner([]) + with patch.object(lifecycle.LifecycleRun, "unload_reload", side_effect=RuntimeError("probe failure")): + with self.assertRaisesRegex(RuntimeError, "probe failure"): + runner.unload_reload("cycle") + self.assertIsNone(runner._unload_context) + + def test_unload_wrapper_delegates_the_original_protocol_exactly_once(self): + runner = self.runner([]) + result = object() + + def original(instance, name): + self.assertIs(instance, runner) + self.assertEqual(name, "cycle") + self.assertEqual(instance._unload_context, dict(name="cycle", before=None, deadline=None)) + return result + + with patch.object(lifecycle.LifecycleRun, "unload_reload", autospec=True, side_effect=original) as call: + self.assertIs(runner.unload_reload("cycle"), result) + call.assert_called_once_with(runner, "cycle") + self.assertIsNone(runner._unload_context) + + def test_unload_budget_is_anchored_to_release_and_not_extended_by_samples(self): + runner = self.unload_runner([self.unloaded(), self.unloaded()]) + with patch.object(native.time, "monotonic", return_value=10): + runner.probe("release", "cycle-released") + self.assertEqual(runner._unload_context["deadline"], 55) + with patch.object(native.time, "monotonic", return_value=55): + runner.probe("snapshot", "cycle-absent-44") + self.assertEqual(runner._unload_context["deadline"], 55) + + def test_reload_waits_for_genuine_load_then_first_tick_without_reading_values(self): + absent = self.unloaded(callbacks=1, events=True) + initial, ready = self.snapshot(102, loads=2), self.snapshot(103, loads=2, nbt="{progress:0s}") + runner = self.unload_runner([initial, ready]) + runner._unload_context["absent"] = absent + with patch.object(native.time, "sleep"): + selected = runner.probe("load", "cycle-loaded") + self.assertEqual(selected, ready) + self.assertEqual(runner._raw_probe.call_args_list[0].args[0], "load") + self.assertEqual(native.read_json(self.root / "advantage-works-results/lifecycle-cycle-loaded.json"), ready) + + def test_wait_uses_first_real_tick_even_when_desired_values_arrive_later(self): + before = self.snapshot(nbt="{progress:12s}") + immediate = self.snapshot(nbt="{progress:12s}") + same_tick = self.snapshot(nbt="{progress:12s}") + first_tick = self.snapshot(101, nbt="{progress:0s}") + later_pass = self.snapshot(102, nbt="{progress:13s}") + runner = self.runner([before, immediate, same_tick, first_tick, later_pass]) + with patch.object(native.time, "sleep"), \ + patch.object(lifecycle, "progress_results", side_effect=AssertionError("oracle inside wait")), \ + patch.object(lifecycle, "changed_target_results", side_effect=AssertionError("oracle inside wait")): + selected = runner.probe("legacy-read", "legacy") + self.assertEqual(selected, first_tick) + self.assertEqual(runner._raw_probe.call_count, 4) + canonical = native.read_json(self.root / "advantage-works-results/lifecycle-legacy.json") + self.assertEqual(canonical, first_tick) + self.assertNotEqual(canonical, later_pass) + record = runner.observations[-1] + source = self.root / "advantage-works-results" / ("lifecycle-" + record["selected_label"] + ".json") + self.assertEqual(source.read_bytes(), (self.root / "advantage-works-results/lifecycle-legacy.json").read_bytes()) + self.assertEqual([sample["world_time"] for sample in record["samples"]], [100, 100, 101]) + + def test_changed_read_is_not_reissued_or_retried_until_target_clears(self): + before, immediate = self.snapshot(nbt="{progress:12s}"), self.snapshot(nbt="{progress:12s}") + retargeted = self.snapshot(101, nbt="{progress:1s,targetX:2,targetY:80,targetZ:1}") + runner = self.runner([before, immediate, retargeted, self.snapshot(102, nbt="{progress:0s}")]) + with patch.object(native.time, "sleep"): + self.assertEqual(runner.probe("changed-target-read", "changed"), retargeted) + self.assertEqual([call.args[0] for call in runner._raw_probe.call_args_list], + ["snapshot", "changed-target-read", "snapshot"]) + + def guard_samples(self, changed_items="[]"): + old = "{progress:12s,targetX:2,targetY:80,targetZ:1,Items:[]}" + advanced = old.replace("12s", "13s") + cleared = "{progress:0s,Items:" + changed_items + "}" + return [self.snapshot(nbt=old), self.snapshot(nbt=old), self.snapshot(nbt=old), + self.snapshot(101, nbt=advanced), self.snapshot(101, nbt=advanced), + self.snapshot(101, nbt=cleared), self.snapshot(102, nbt=cleared)] + + def test_inherited_guard_calls_each_unchanged_oracle_once_after_selection(self): + runner = self.runner(self.guard_samples()) + with patch.object(native.time, "sleep"), \ + patch.object(lifecycle, "progress_results", wraps=lifecycle.progress_results) as progress, \ + patch.object(lifecycle, "changed_target_results", wraps=lifecycle.changed_target_results) as changed: + results = runner.save_read_guards() + self.assertEqual(progress.call_count, 1) + self.assertEqual(changed.call_count, 1) + self.assertEqual(results["legacy_read"]["drill"]["status"], "PASS") + self.assertEqual(results["changed_target_read"]["drill"]["status"], "PASS") + + def test_inherited_unchanged_inventory_guard_still_fails(self): + runner = self.runner(self.guard_samples('[{id:"minecraft:dirt",Count:1b}]')) + with patch.object(native.time, "sleep"): + with self.assertRaisesRegex(AssertionError, "changed drill inventory"): + runner.save_read_guards() + + def test_loaded_chunks_need_actual_load_events_and_counter_increase(self): + before = self.snapshot(loaded=False, loads=1) + loaded = self.snapshot(101, loads=1) + self.assertTrue(native.load_event_issues(before, loaded)) + loaded["chunks"][0]["loads"] = 2 + self.assertEqual(native.load_event_issues(before, loaded), []) + loaded["chunkEvents"] = [] + self.assertTrue(native.load_event_issues(before, loaded)) + + def test_tick_gate_starts_only_once_all_witnesses_are_loaded(self): + first = self.snapshot(loaded=False, loads=0) + loaded, ready = self.snapshot(101), self.snapshot(102) + runner = self.runner([loaded, ready]) + with patch.object(native.time, "sleep"): + self.assertEqual(runner._wait_ready("test", first, reload_before=first), ready) + samples = runner.observations[-1]["samples"] + self.assertIsNone(samples[0]["loaded_since_world_time"]) + self.assertEqual(samples[1]["loaded_since_world_time"], 101) + + def test_missing_witness_and_missing_chunk_are_never_ready(self): + for key in ("cases", "chunks"): + value = self.snapshot() + value[key] = {} if key == "cases" else [] + self.assertTrue(native.loaded_issues(value, self.definition)) + + def test_stalled_native_time_fails_bounded_without_synthetic_advancement(self): + runner = self.runner([self.snapshot()] * 4) + with patch.object(native, "READINESS_SAMPLES", 3), patch.object(native.time, "sleep"): + with self.assertRaisesRegex(TimeoutError, "actual ticks"): + runner._wait_ready("test", self.snapshot()) + self.assertEqual(runner.observations[-1]["status"], "FAIL") + self.assertTrue(all(row["world_time"] == 100 for row in runner.observations[-1]["samples"])) + + def test_wall_guard_cannot_be_passed_by_late_tick(self): + runner = self.runner([self.snapshot(101)]) + with patch.object(native.time, "monotonic", side_effect=[0, 0, 21]), patch.object(native.time, "sleep"): + with self.assertRaises(TimeoutError): + runner._wait_ready("test", self.snapshot()) + + def test_native_restart_only_restores_tickets_not_fixtures(self): + absent, loaded, ready = self.snapshot(loaded=False, loads=0), self.snapshot(), self.snapshot(101) + runner = self.runner([absent, loaded, ready]) + with patch.object(lifecycle.LifecycleRun, "boot") as boot, patch.object(native, "require_native_boot"), \ + patch.object(native.time, "sleep"): + runner.boot("midblock-restart") + boot.assert_called_once_with("midblock-restart") + self.assertEqual([call.args[0] for call in runner._raw_probe.call_args_list], ["snapshot", "load", "snapshot"]) + self.assertEqual(runner.observations[-1]["reload_before_label"], "midblock-restart-native-unrestored") + + def test_initial_and_frozen_boot_never_rebuild_or_load_lifecycle_fixture(self): + runner = self.runner([]) + with patch.object(lifecycle.LifecycleRun, "boot"): + runner.boot("lifecycle") + runner.boot("retained13-import") + runner._raw_probe.assert_not_called() + + def test_raw_snapshot_overwrite_rejected(self): + runner = native.NativeLifecycleRun(self.root, None, None, self.definition) + write_json(self.root / "advantage-works-results/lifecycle-original.json", self.snapshot()) + with patch.object(lifecycle.LifecycleRun, "probe") as original: + with self.assertRaises(FileExistsError): + runner._raw_probe("snapshot", "original") + original.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_oil.py b/test-rig/migration/test_native114_oil.py new file mode 100644 index 0000000..b0739d9 --- /dev/null +++ b/test-rig/migration/test_native114_oil.py @@ -0,0 +1,430 @@ +"""Focused native oil safety, protocol and unchanged-oracle negative controls.""" + +from copy import deepcopy +import json +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch +import zipfile + +import lifecycle +from matrix import WORLD, digest, fingerprint, write_json +from native114 import LAUNCHER, RIG +import native114_oil as native +import oil_transfer + + +BOOT_PROOF = ("Starting minecraft server version 1.14.4\n" + "Forge mod loading, version 28.2.26, for MC 1.14.4 with MCP 20190829.143755\n") + + +def snapshot(definition, amount, identity=0): + cases = {} + for index, lane in enumerate(definition): + cases[lane["id"]] = [ + dict(pos=lane["source"], block="minecraft:air"), + dict(pos=lane["drain"], block="poweradvantage:fluid_drain", tileIdentity=identity + index * 2 + 1, + nbt="{Tank:{Empty:1b}}"), + *[dict(pos=pos, block="poweradvantage:fluid_pipe") for pos in lane["pipes"]], + dict(pos=lane["output"], block=lane["tank"], tileIdentity=identity + index * 2 + 2, + nbt=('{Tank:{Amount:' + str(amount) + ',FluidName:"' + lane["fluid"] + '"}}') + if amount else "{Tank:{Empty:1b}}")] + return dict(cases=cases) + + +class NativeOilTests(unittest.TestCase): + def setUp(self): + root = RIG / "build/port-evidence" + root.mkdir(parents=True, exist_ok=True) + self.temp = tempfile.TemporaryDirectory(dir=root) + self.root = Path(self.temp.name) + + def tearDown(self): + self.temp.cleanup() + + def profile(self): + seed = self.root / "installed-seed" + (seed / "libraries/subdir").mkdir(parents=True) + (seed / "libraries/subdir/runtime.jar").write_bytes(b"unit-test-library-not-executed") + (seed / LAUNCHER).write_bytes(b"unit-test-launcher-not-executed") + (seed / "minecraft_server.1.14.4.jar").write_bytes(b"unit-test-vanilla-not-executed") + (seed / WORLD).mkdir() + (seed / WORLD / "level.dat").write_bytes(b"protected-unit-test-world-never-copied") + (seed / "mods").mkdir() + (seed / "mods/unpinned.jar").write_bytes(b"never-copy-seed-mods") + (seed / "server.properties").write_text("protected-seed-settings\n") + jars = [] + for mod in ("poweradvantage", "advantageworkstest", "orespawn"): + jar = self.root / (mod + "-1.0.0.114041.jar") + with zipfile.ZipFile(jar, "w") as archive: + archive.writestr("META-INF/mods.toml", '[[mods]]\nmodId="' + mod + '"\nversion="1.0.0.114041"\n') + jars.append(dict(path=str(jar), sha256=digest(jar), id=mod, version="1.0.0.114041")) + pins = dict(seed=str(seed), launcher=LAUNCHER, launcher_sha256=digest(seed / LAUNCHER), jars=jars) + manifest = self.root / "target.json" + write_json(manifest, dict(target114=pins)) + java = self.root / "unit-java-not-executed.exe" + java.write_bytes(b"unit test executable placeholder, process factory is mocked") + args = SimpleNamespace(target_manifest=manifest, output=self.root / "native114-oil-unit", + java8=java, port=25576, seconds=2) + return args, (seed, LAUNCHER, [Path(jar["path"]) for jar in jars]), pins + + def server_factory(self, mutate=None, transform=None, failure=None, shutdown=False): + owner = self + self.processes = [] + self.commands = [] + + class ProtocolServer: + """Unit protocol double; no Java or Minecraft process is started.""" + def __init__(self, runtime, java, launcher, label): + self.runtime, self.label = runtime, label + self.closed = False + self.identity = len(owner.processes) * 100 + owner.processes.append(self) + self.definition = native.read_json(runtime / "lifecycle-fixture.json") + (runtime / f"console-{label}.log").write_text(BOOT_PROOF) + + def send(self, command): + owner.commands.append((self.label, command)) + if mutate: + mutate(self, command) + if not command.startswith("advlifecycle "): + return + _, operation, label = command.split() + if label == failure: + raise AssertionError("unit-injected probe failure") + amount = {"oil-empty": 0, "oil-first-fill": 1000, "oil-restored": 1000, + "oil-refilled": 2000, "oil-retained": 2000}.get(label, 0) + value = snapshot(self.definition, amount, self.identity) + if transform: + transform(label, value) + write_json(self.runtime / "advantage-works-results" / f"lifecycle-{label}.json", value) + + def wait(self, token, timeout): + pass + + def close(self): + self.closed = True + if shutdown: + raise AssertionError("unit-injected shutdown failure") + + return ProtocolServer + + def report(self, args): + return native.read_json(args.output / "native114-oil-results.json") + + def test_power_definitions_are_exact_unchanged_copies(self): + original = oil_transfer.fixtures() + lanes = native.fixtures(dict(jars=[])) + self.assertEqual(lanes, original) + lanes[0]["source"][0] += 1 + self.assertEqual(oil_transfer.fixtures(), original) + + def test_optional_mineralogy_has_two_distinct_native_lanes_without_mutating_power(self): + pins = dict(jars=[dict(id="mineralogy", version=native.MINERALOGY_VERSION, sha256=native.MINERALOGY_SHA256)]) + lanes = native.fixtures(pins) + self.assertEqual(lanes[:4], oil_transfer.fixtures()) + self.assertEqual(len(lanes), 6) + for lane, template in zip(lanes[4:], lanes[:2]): + self.assertEqual(lane["fluid"], "mineralogy:crude_oil") + self.assertEqual(lane["bucket"], "mineralogy:crude_oil_bucket") + self.assertEqual(lane["tank"], template["tank"]) + for point, old in zip(lane["witnesses"], template["witnesses"]): + self.assertEqual(point, [old[0] + 256, *old[1:]]) + native.require_document(snapshot(lanes, 1000), lanes) + self.assertEqual(len(oil_transfer.inspect(snapshot(lanes, 1000), lanes, 1000)), 6) + + def test_mineralogy_wrong_version_hash_or_duplicate_is_not_silently_skipped(self): + good = dict(id="mineralogy", version=native.MINERALOGY_VERSION, sha256=native.MINERALOGY_SHA256) + for jars in ([dict(good, version="6.0.0.113021")], [dict(good, sha256="0" * 64)], [good, good]): + with self.subTest(jars=jars), self.assertRaises(ValueError): + native.fixtures(dict(jars=jars)) + + def test_bounds_headroom_connectivity_overlap_and_duplicate_ids_are_strict(self): + def moved(lanes): + lanes[0]["source"][0] += 1 + mutations = [lambda lanes: lanes[0]["min"].__setitem__(0, 0), + lambda lanes: lanes[0]["max"].__setitem__(0, lanes[0]["min"][0] + 32), + lambda lanes: lanes[0]["source"].__setitem__(1, lanes[0]["max"][1]), + moved, lambda lanes: lanes.append(deepcopy(lanes[0]))] + for mutate in mutations: + lanes = oil_transfer.fixtures() + mutate(lanes) + with self.assertRaises(AssertionError): + native.require_bounds(lanes) + + def test_missing_extra_duplicate_and_moved_snapshot_witnesses_fail(self): + lanes = oil_transfer.fixtures() + for mutate in (lambda s: s["cases"].pop(lanes[0]["id"]), + lambda s: s["cases"].update(extra=[]), + lambda s: s["cases"][lanes[0]["id"]].pop(), + lambda s: s["cases"][lanes[0]["id"]].append(s["cases"][lanes[0]["id"]][0]), + lambda s: s["cases"][lanes[0]["id"]][0].update(pos=[0, 80, 0])): + document = snapshot(lanes, 1000) + mutate(document) + with self.assertRaises(AssertionError): + native.require_document(document, lanes) + + def test_existing_quantity_fluid_and_tile_identity_oracles_remain_strict(self): + lanes = native.fixtures(dict(jars=[])) + for amount in (0, 999, 1001, 2000): + with self.assertRaises(AssertionError): + oil_transfer.inspect(snapshot(lanes, amount), lanes, 1000) + wrong = snapshot(lanes, 1000) + wrong["cases"][lanes[0]["id"]][-1]["nbt"] = '{Tank:{Amount:1000,FluidName:"minecraft:water"}}' + with self.assertRaises(AssertionError): + oil_transfer.inspect(wrong, lanes, 1000) + with self.assertRaises(AssertionError): + oil_transfer.require_same_machines(snapshot(lanes, 1000), snapshot(lanes, 2000, 100), lanes) + + def test_native_log_requires_both_versions_and_rejects_runtime_command_and_probe_errors(self): + log = self.root / "console.log" + log.write_text(BOOT_PROOF + "[watcher/FATAL] " + native.CONFIG_NOTICE + "\n") + native.require_native_boot(log) + for text in (BOOT_PROOF.replace("1.14.4", "1.13.2"), BOOT_PROOF.replace("28.2.26", "25.0.223"), + *[BOOT_PROOF + failure + "\n" for failure in ( + "[main/ERROR] bad", "[main/FATAL] actual fatal", "NoSuchFieldError: bad", + "NoSuchMethodError: bad", "Failed to reload data packs", "Lifecycle probe failed", + "ADVLIFECYCLE FAIL", "Unknown or incomplete command", + "Incorrect argument for command", "An unexpected error occurred trying to execute that command")]): + log.write_text(text) + with self.subTest(text=text), self.assertRaises(AssertionError): + native.require_native_boot(log) + + def test_prepare_copies_pinned_libraries_only_never_seed_world_or_settings(self): + args, profile, pins = self.profile() + before = fingerprint(profile[0]) + runtime = self.root / "target" + definition = native.fixtures(pins) + self.assertEqual(native.prepare(runtime, profile, definition, args.port, pins, before), LAUNCHER) + self.assertFalse((runtime / WORLD).exists()) + self.assertFalse((runtime / "mods/unpinned.jar").exists()) + self.assertIn("server-port=25576", (runtime / "server.properties").read_text()) + self.assertNotIn("3;minecraft:bedrock", (runtime / "server.properties").read_text()) + self.assertEqual(fingerprint(profile[0]), before) + self.assertEqual(native.read_json(runtime / "lifecycle-fixture.json"), definition) + + def test_staged_jars_cannot_follow_a_changed_original_instead_of_captured_pins(self): + args, profile, pins = self.profile() + before = fingerprint(profile[0]) + runtime = self.root / "target" + native.prepare(runtime, profile, native.fixtures(pins), args.port, pins, before) + jar = profile[2][0] + jar.write_bytes(b"changed after pin") + (runtime / "mods" / jar.name).write_bytes(jar.read_bytes()) + write_json(runtime / "input-jars.json", [dict(file=p.name, sha256=digest(p)) for p in profile[2]]) + with self.assertRaises(AssertionError): + native.verify_staged(runtime, pins, before) + + def test_staged_launcher_and_library_tampering_fail(self): + args, profile, pins = self.profile() + before = fingerprint(profile[0]) + runtime = self.root / "target" + native.prepare(runtime, profile, native.fixtures(pins), args.port, pins, before) + for path in (runtime / LAUNCHER, runtime / "libraries/subdir/runtime.jar"): + old = path.read_bytes() + path.write_bytes(b"changed") + with self.assertRaises(AssertionError): + native.verify_staged(runtime, pins, before) + path.write_bytes(old) + + def test_only_ordered_initial_construction_is_allowed_and_never_after_baseline(self): + args, profile, pins = self.profile() + lanes = native.fixtures(pins) + runner = native.NativeOilRun(self.root, args.java8, LAUNCHER, lanes, pins, {}) + runner.boots = [native.BOOT_LABELS[0]] + runner.server = SimpleNamespace(send=lambda command: None) + for command in ("advlifecycle offer illicit", "data merge block 11272 80 2056 {}", + "setblock 11272 81 2056 poweradvantage:crude_oil", + "advworks reset all", "advlifecycle snapshot ok\nstop"): + with self.subTest(command=command), self.assertRaises(AssertionError): + runner.command(command) + with self.assertRaises(AssertionError): + runner.seal_construction() + initial = native.construction_commands(lanes) + for command in initial: + runner.command(command) + runner.seal_construction() + for command in initial: + with self.assertRaises(AssertionError): + runner.command(command) + + def test_buckets_require_empty_or_restored_gate_and_at_most_two_placements(self): + args, profile, pins = self.profile() + runner = native.NativeOilRun(self.root, args.java8, LAUNCHER, native.fixtures(pins), pins, {}) + runner.server = SimpleNamespace(send=lambda command: None) + runner.boots = [native.BOOT_LABELS[0]] + with self.assertRaises(AssertionError): + runner.command("advlifecycle place-buckets too-soon") + with self.assertRaises(AssertionError): + runner.permit_buckets() + runner.pending_construction = [] + runner.seal_construction() + for index in range(2): + runner.boots = list(native.BOOT_LABELS[:index + 1]) + runner.permit_buckets() + runner.command("advlifecycle place-buckets permitted") + with self.assertRaises(AssertionError): + runner.command("advlifecycle place-buckets duplicate") + runner.boots = list(native.BOOT_LABELS) + with self.assertRaises(AssertionError): + runner.permit_buckets() + + def test_complete_protocol_three_boots_two_source_only_placements_and_all_evidence(self): + args, profile, pins = self.profile() + before = fingerprint(profile[0]) + with patch.object(lifecycle, "Server", self.server_factory()), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 0) + report = self.report(args) + self.assertEqual(report["status"], "PASS") + self.assertEqual(report["full_process_restarts"], 2) + self.assertEqual(report["bucket_placements"], 2) + self.assertEqual([phase["phase"] for phase in report["phases"]], ["first-fill", "restored", "refilled", "retained"]) + self.assertEqual([p["cases"][0]["total_mB"] for p in report["phases"]], [1000, 1000, 2000, 2000]) + self.assertEqual(report["laneCount"], 4) + self.assertEqual(report["seedFingerprintBefore"], before) + self.assertEqual(report["seedFingerprintAfter"], before) + self.assertTrue(report["helpers_unchanged"] and report["seed_unchanged"] and report["manifest_unchanged"]) + self.assertEqual([p.label for p in self.processes], list(native.BOOT_LABELS)) + self.assertTrue(all(p.closed for p in self.processes)) + later = [command for label, command in self.commands if label != native.BOOT_LABELS[0]] + self.assertFalse(any(command.startswith(("fill ", "setblock ", "data ")) for command in later)) + self.assertEqual(sum("place-buckets" in command for command in later), 1) + self.assertEqual((args.output / "target-manifest.json").read_bytes(), args.target_manifest.read_bytes()) + + def test_probe_failure_is_retained_with_seed_fingerprints_and_process_closed(self): + args, profile, _ = self.profile() + before = fingerprint(profile[0]) + with patch.object(lifecycle, "Server", self.server_factory(failure="oil-refilled")), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + report = self.report(args) + self.assertEqual(report["status"], "FAIL") + self.assertIn("probe failure", report["failure"]) + self.assertEqual(report["seedFingerprintAfter"], before) + self.assertTrue(all(p.closed for p in self.processes)) + + def test_shutdown_failure_cannot_be_success(self): + args, _, _ = self.profile() + with patch.object(lifecycle, "Server", self.server_factory(shutdown=True)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + self.assertIn("shutdown failure", self.report(args)["failure"]) + + def test_restart_quantity_loss_blocks_refill_not_repaired_by_new_sources(self): + args, _, _ = self.profile() + def transform(label, value): + if label == "oil-restored": + lane = next(iter(value["cases"].values())) + lane[-1]["nbt"] = lane[-1]["nbt"].replace("Amount:1000", "Amount:999") + with patch.object(lifecycle, "Server", self.server_factory(transform=transform)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + self.assertIn("Lost/duplicated oil", self.report(args)["failure"]) + self.assertEqual(sum("place-buckets" in command for _, command in self.commands), 1) + self.assertEqual(len(self.processes), 2) + + def test_nonempty_baseline_cannot_place_any_bucket(self): + args, _, _ = self.profile() + def transform(label, value): + if label == "oil-empty": + next(iter(value["cases"].values()))[-1]["nbt"] = '{Tank:{Amount:1,FluidName:"poweradvantage:crude_oil"}}' + with patch.object(lifecycle, "Server", self.server_factory(transform=transform)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + self.assertIn("Fixture was not empty", self.report(args)["failure"]) + self.assertFalse(any("place-buckets" in command for _, command in self.commands)) + + def test_refill_cannot_replace_a_machine_even_with_exact_amount(self): + args, _, _ = self.profile() + def transform(label, value): + if label == "oil-refilled": + next(iter(value["cases"].values()))[-1]["tileIdentity"] += 1 + with patch.object(lifecycle, "Server", self.server_factory(transform=transform)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + self.assertIn("Refill replaced a machine", self.report(args)["failure"]) + self.assertEqual(len(self.processes), 2) + + def test_missing_initial_identity_cannot_qualify(self): + args, _, _ = self.profile() + def transform(label, value): + if label == "oil-empty": + next(iter(value["cases"].values()))[1].pop("tileIdentity") + with patch.object(lifecycle, "Server", self.server_factory(transform=transform)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + self.assertFalse(any("place-buckets" in command for _, command in self.commands)) + + def test_staging_failure_still_retains_full_seed_fingerprints(self): + args, profile, _ = self.profile() + before = fingerprint(profile[0]) + with patch.object(native, "prepare", side_effect=AssertionError("unit-injected staging failure")): + self.assertEqual(native.run(args), 1) + report = self.report(args) + self.assertIn("staging failure", report["failure"]) + self.assertEqual(report["seedFingerprintBefore"], before) + self.assertEqual(report["seedFingerprintAfter"], before) + + def test_wrong_launcher_and_out_of_order_boots_are_rejected_before_process(self): + args, _, pins = self.profile() + runner = native.NativeOilRun(self.root, args.java8, "forge-1.13.2-25.0.223.jar", native.fixtures(pins), pins, {}) + with self.assertRaises(AssertionError): + runner.boot(native.BOOT_LABELS[0]) + runner.launcher = LAUNCHER + for label in (native.BOOT_LABELS[1], "invented-boot"): + with self.assertRaises(AssertionError): + runner.boot(label) + + def test_late_log_errors_are_audited_after_stop(self): + args, _, _ = self.profile() + def mutate(server, command): + if command == "save-all flush": + with (server.runtime / f"console-{server.label}.log").open("a") as stream: + stream.write("[main/ERROR] unit-injected late failure\n") + with patch.object(lifecycle, "Server", self.server_factory(mutate=mutate)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + self.assertIn("runtime_audit_failure", self.report(args)) + + def test_full_seed_fingerprint_catches_changes_outside_world(self): + args, profile, _ = self.profile() + def mutate(server, command): + (profile[0] / "server.properties").write_text("unit-injected concurrent seed change") + with patch.object(lifecycle, "Server", self.server_factory(mutate=mutate)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + report = self.report(args) + self.assertFalse(report["seed_unchanged"]) + self.assertIn("server.properties", report["seedFingerprintAfter"]) + + def test_mutable_manifest_is_detected_and_captured_manifest_retained(self): + args, _, _ = self.profile() + original = args.target_manifest.read_bytes() + def mutate(server, command): + args.target_manifest.write_bytes(original + b"\n") + with patch.object(lifecycle, "Server", self.server_factory(mutate=mutate)), patch.object(native.time, "sleep"): + self.assertEqual(native.run(args), 1) + self.assertFalse(self.report(args)["manifest_unchanged"]) + self.assertEqual((args.output / "target-manifest.json").read_bytes(), original) + + def test_stale_input_manifest_fails_before_staging_or_process(self): + args, profile, _ = self.profile() + profile[2][0].write_bytes(b"not the pinned jar") + with patch.object(native, "prepare") as prepare, self.assertRaises(AssertionError): + native.run(args) + prepare.assert_not_called() + self.assertFalse(args.output.exists()) + + def test_output_cannot_overlap_seed_manifest_or_leave_scoped_build(self): + args, profile, _ = self.profile() + for output in (profile[0], profile[0] / "native114-oil-unsafe", args.target_manifest, + RIG / "migration/native114-oil-unsafe", self.root / "wrong-prefix"): + args.output = output + with self.subTest(output=output), self.assertRaises((ValueError, FileExistsError)): + native.run(args) + + def test_invalid_port_or_interval_fails_before_staging(self): + args, _, _ = self.profile() + for port, seconds in ((0, 8), (65536, 8), (25576, 1), (25576, 121)): + args.port, args.seconds = port, seconds + with self.assertRaises(ValueError): + native.run(args) + self.assertFalse(args.output.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_partial.py b/test-rig/migration/test_native114_partial.py new file mode 100644 index 0000000..d391d87 --- /dev/null +++ b/test-rig/migration/test_native114_partial.py @@ -0,0 +1,106 @@ +"""Native transport controls do not change historical fixture/oracle semantics.""" + +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import extended_lifecycle +from native114_partial import (NativeExtendedRun, NativePartialRun, selected_chunks, + subset_load_issues, subset_unload_pending) +from native114_lifecycle import NativeLifecycleRun + + +def snapshot(tick, loaded=True, loads=1, unloads=0, events=None): + return dict(worldTime=tick, chunks=[dict(pos=[1, 2], loaded=loaded, loads=loads, unloads=unloads)], + chunkEvents=events if events is not None else [dict(pos=[1, 2], operation="load")], + cases={"case": [dict(pos=[16, 80, 32], loaded=loaded)]}) + + +class NativePartialTest(unittest.TestCase): + def test_extended_keeps_historical_operations(self): + self.assertIs(NativeExtendedRun.build, extended_lifecycle.ExtendedRun.build) + self.assertIs(NativeExtendedRun.partial, extended_lifecycle.ExtendedRun.partial) + self.assertIs(NativeExtendedRun.wait_step, extended_lifecycle.ExtendedRun.wait_step) + self.assertIs(NativeExtendedRun.save_read_guards, NativeLifecycleRun.save_read_guards) + + def test_group_selection_does_not_acquire_unrequested_group(self): + definition = [dict(groups=dict(machine=[[1, 2]], supply=[[2, 2]]))] + self.assertEqual(selected_chunks(definition, "machine"), {(1, 2)}) + + def test_load_requires_actual_event_not_loaded_flag_alone(self): + self.assertTrue(subset_load_issues(snapshot(1, False), snapshot(2, events=[]), {(1, 2)})) + + def test_load_requires_increment_after_absence(self): + self.assertTrue(subset_load_issues(snapshot(1, False), snapshot(2), {(1, 2)})) + self.assertFalse(subset_load_issues(snapshot(1, False), snapshot(2, loads=2), {(1, 2)})) + + def test_load_rejects_unloaded_witness(self): + current = snapshot(2, loads=2) + current["cases"]["case"][0]["loaded"] = False + self.assertTrue(subset_load_issues(snapshot(1, False), current, {(1, 2)})) + + def test_unload_requires_counter_and_callback(self): + before = snapshot(1) + current = snapshot(2, loaded=False, unloads=1) + self.assertEqual(subset_unload_pending(before, current, {(1, 2)}), (True, ["(1, 2)"])) + current["chunkEvents"].append(dict(pos=[1, 2], operation="unload")) + self.assertEqual(subset_unload_pending(before, current, {(1, 2)}), (True, [])) + + def test_unload_does_not_accept_callback_without_absence(self): + current = snapshot(2, unloads=1, events=[dict(pos=[1, 2], operation="unload")]) + self.assertEqual(subset_unload_pending(snapshot(1), current, {(1, 2)}), (False, [])) + + def test_unknown_load_and_release_groups_fail_before_commands(self): + runner = NativePartialRun(Path("unused"), None, None, []) + with patch.object(runner, "_raw_probe") as raw: + for operation in ("load-foreign", "release-foreign"): + with self.assertRaises(ValueError): + runner.probe(operation, "unknown") + raw.assert_not_called() + + def test_unload_callback_wait_preserves_actual_raw_event_and_tick(self): + with tempfile.TemporaryDirectory() as temp: + runner = NativePartialRun(Path(temp), None, None, []) + runner._released_groups["machine"] = dict(before=snapshot(1), selected={(1, 2)}, deadline=1000) + pending = snapshot(2, False, unloads=0) + actual = snapshot(3, False, unloads=1, events=[dict(pos=[1, 2], operation="unload")]) + with patch.object(runner, "_raw_probe", side_effect=[pending, actual]) as raw, \ + patch.object(runner, "_canonical") as canonical, patch.object(runner, "_record"), \ + patch("native114_partial.time.monotonic", return_value=100), patch("native114_partial.time.sleep"): + selected = runner._snapshot_ready("snapshot", "absent") + self.assertIs(selected, actual) + self.assertEqual(raw.call_count, 2) + self.assertEqual(selected["worldTime"], 3) + canonical.assert_called_once_with("absent-native-pending-01", "absent") + + def test_load_selects_first_real_tick_even_if_gameplay_wrong(self): + definition = [dict(min=[16, 0, 32], max=[31, 100, 47])] + with tempfile.TemporaryDirectory() as temp: + runner = NativePartialRun(Path(temp), None, None, definition) + rows = [snapshot(1, False, loads=0), snapshot(2, loads=1), snapshot(2, loads=1), snapshot(3, loads=1)] + rows[-1]["cases"]["case"][0]["nbt"] = '{progress:0,WrongGameplay:1}' + with patch.object(runner, "_raw_probe", side_effect=rows) as raw, \ + patch.object(runner, "_canonical") as canonical, patch.object(runner, "_record"), \ + patch("native114_partial.time.sleep"): + selected = runner._load_ready("load", "reload") + self.assertIs(selected, rows[-1]) + self.assertEqual(raw.call_count, 4) + canonical.assert_called_once_with("reload-native-pending-002", "reload") + + def test_canonical_is_exact_raw_bytes_and_never_overwritten(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) / "advantage-works-results" + root.mkdir() + raw = root / "lifecycle-selected.json" + raw.write_bytes(b'{"worldTime":5,"raw":true}\n') + runner = NativePartialRun(Path(temp), None, None, []) + runner._canonical("selected", "canonical") + self.assertEqual(raw.read_bytes(), (root / "lifecycle-canonical.json").read_bytes()) + with self.assertRaises(FileExistsError): + runner._canonical("selected", "canonical") + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_player_layout.py b/test-rig/migration/test_native114_player_layout.py new file mode 100644 index 0000000..ff60408 --- /dev/null +++ b/test-rig/migration/test_native114_player_layout.py @@ -0,0 +1,21 @@ +import unittest + +from native114_player_layout import verdict + + +class NativeLayoutOracleTest(unittest.TestCase): + def test_scope_is_exact_player_data_not_unparented_fixture_turbine_operation(self): + player = {"Inventory": [{"id": "steamadvantage:musket", "Count": 1, "Slot": 35, + "tag": {"Nested": {"Items": [{"id": "electricadvantage:lithium_battery", "Count": 3, "tag": {"energy": 123}}]}}}], + "EnderItems": [{"id": "poweradvantage:crude_oil_bucket", "Count": 1, "Slot": 26}]} + expected = {"file": player, "adapter": player, "entities": {"unparented-turbine": {"id": "electricadvantage:hydroturbine"}}} + self.assertEqual("PASS", verdict(expected, {"file": player, "adapter": player})["status"]) + + def test_extra_nested_item_tag_is_rejected(self): + expected = {"file": {"Inventory": [{"id": "x:y", "Count": 3, "tag": {"energy": 123}}]}, "adapter": {}} + actual = {"file": {"Inventory": [{"id": "x:y", "Count": 3, "tag": {"energy": 123, "extra": "changed"}}]}, "adapter": {}} + self.assertEqual("FAIL", verdict(expected, actual)["status"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_recipes.py b/test-rig/migration/test_native114_recipes.py new file mode 100644 index 0000000..95c6163 --- /dev/null +++ b/test-rig/migration/test_native114_recipes.py @@ -0,0 +1,210 @@ +"""Native recipe runner safety/oracle tests. No Minecraft or protected input writes.""" + +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch +import zipfile + +import native114_recipes as recipes + + +class NativeRecipeTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + + def packaged(self, counts=None, bad_predicate=False): + jars = [] + for mod, count in (counts or recipes.MOD_COUNTS).items(): + path = self.root / (mod + ".jar") + with zipfile.ZipFile(path, "w") as jar: + for index in range(count): + stem = f"case_{index}" + jar.writestr(f"data/{mod}/recipes/{stem}.json", json.dumps({"type": "minecraft:crafting_shapeless"})) + predicate = {"item": "minecraft:iron_ingot"} + if bad_predicate: + predicate["count"] = {"min": 1} + jar.writestr(f"data/{mod}/advancements/recipes/{stem}.json", json.dumps({"criteria": { + "has_ingredient": {"trigger": "minecraft:inventory_changed", "conditions": {"items": [predicate]}} + }})) + jar.writestr(f"data/{mod}/recipes/crushing_test.json", json.dumps({"type": "basemetals:crushing"})) + jars.append(path) + return jars + + def test_fixture_uses_packaged_advancements_and_preserves_109_inventory(self): + definition, sources = recipes.recipe_fixture(self.packaged()) + self.assertEqual(109, len(definition["recipes"])) + self.assertEqual(109, len(sources)) + self.assertEqual(27, len(definition["materials"])) + self.assertTrue(all(row["ingredients"] == {"has_ingredient": {"item": "minecraft:iron_ingot"}} + for row in definition["recipes"])) + self.assertTrue(all("crushing" not in identity for identity in sources)) + + def test_fixture_inventory_cannot_silently_shrink(self): + counts = dict(recipes.MOD_COUNTS, poweradvantage=34) + with self.assertRaises(AssertionError): + recipes.recipe_fixture(self.packaged(counts)) + + def test_fixture_rejects_unsupported_predicates(self): + with self.assertRaises(AssertionError): + recipes.recipe_fixture(self.packaged(bad_predicate=True)) + + def test_fixture_rejects_duplicate_packaged_recipes(self): + jars = self.packaged() + with self.assertRaises(AssertionError): + recipes.recipe_fixture(jars + jars[:1]) + + def test_unlock_schedule_preserves_modes_and_full_process_persistence(self): + self.assertEqual(5, len(recipes.UNLOCK_BOOTS)) + self.assertEqual(["NORMAL", "NORMAL", "TECH_PROGRESSION", "APOCALYPTIC", "NORMAL"], + [row[0] for row in recipes.UNLOCK_BOOTS]) + self.assertEqual(3, sum("sweep" in row[2] for row in recipes.UNLOCK_BOOTS)) + self.assertEqual(3, sum("persist" in row[2] for row in recipes.UNLOCK_BOOTS)) + self.assertEqual(4, sum("restore" in row[2] for row in recipes.UNLOCK_BOOTS)) + + def unlock_report(self): + definition = {"recipes": [{"id": "poweradvantage:case"}]} + report = {"status": "PASS", "potatoCraftingCases": 59, "bookRecipes": [{"id": "poweradvantage:case"}], + "cases": [{"id": "poweradvantage:case", "recipeActive": True, "status": "PASS"}], + "additionalMaterials": []} + return definition, report + + def test_unlock_requires_every_case_and_all_59_native_potato_checks(self): + definition, report = self.unlock_report() + self.assertEqual(1, recipes.validate_unlock(report, definition, "sweep")["passed"]) + for key, value in (("cases", []), ("potatoCraftingCases", 58), ("bookRecipes", []), ("status", "FAIL")): + changed = dict(report, **{key: value}) + with self.subTest(key=key), self.assertRaises(AssertionError): + recipes.validate_unlock(changed, definition, "sweep") + + def test_unlock_cannot_replace_missing_case_with_duplicate(self): + definition, report = self.unlock_report() + definition["recipes"].append({"id": "poweradvantage:other"}) + report["cases"] *= 2 + with self.assertRaises(AssertionError): + recipes.validate_unlock(report, definition, "sweep") + + def test_human_materials_gaps_remain_failures(self): + definition, report = self.unlock_report() + report["additionalMaterials"] = ["electricadvantage:control_circuit"] + with self.assertRaises(AssertionError): + recipes.validate_unlock(report, definition, "persist") + + def test_unavailable_ingredient_is_reported_as_skip_not_pass(self): + definition, report = self.unlock_report() + report["cases"][0]["status"] = "SKIP_NO_INGREDIENT_PROVIDER" + counts = recipes.validate_unlock(report, definition, "sweep") + self.assertEqual((0, 1), (counts["passed"], counts["skipped"])) + + def test_progression_rejects_empty_evidence_and_ignored_configuration(self): + report = {"status": "PASS", "mode": "NORMAL", "plasticEqualsRubber": True, "blocked": 0, + "blockedRecipes": [], "targets": [{"id": "poweradvantage:case", "status": "PASS"}], + "reachable": {"minecraft:iron_ingot": {}}, "executedRecipes": ["poweradvantage:case"], + "processWitnesses": [{"route": "live_furnace"}], "lootDataPackOverrideVerified": True} + with patch.object(recipes, "validate_tags"): + recipes.validate_progression(report, "NORMAL", True) + for key, value in (("mode", "APOCALYPTIC"), ("plasticEqualsRubber", False), ("blocked", 1), + ("blockedRecipes", ["missing"]), ("targets", []), ("processWitnesses", []), + ("lootDataPackOverrideVerified", False)): + with self.subTest(key=key), self.assertRaises(AssertionError): + recipes.validate_progression(dict(report, **{key: value}), "NORMAL", True) + + def test_original_natural_basis_and_exclusions_are_not_expanded(self): + from progression import fixture + original = fixture() + self.assertEqual(original, recipes.progression_fixture()) + self.assertNotIn("electricadvantage:psu", original["exclusions"]) + self.assertEqual(["electricadvantage:li_ore", "electricadvantage:sulfur_ore"], + [item for item in original["naturalResources"] if item.startswith("electricadvantage:")]) + + def test_target_requires_all_five_mods_and_native_harness(self): + manifest = self.root / "manifest.json" + manifest.write_text(json.dumps({"target114": {"jars": [{"id": "poweradvantage"}]}})) + with self.assertRaises(ValueError), patch.object(recipes, "pinned_profile") as resolve: + recipes.native_target(manifest) + resolve.assert_not_called() + + def test_missing_and_historical_packet_exports_cannot_claim_native_level(self): + runtime = self.root / "runtime" + with self.assertRaises(FileNotFoundError): + recipes.verify_level(runtime, {"jars": []}) + old_level = {"Data": {"Version": {"Name": "1.13.2"}}} + with patch.object(recipes.nbtlib, "load", return_value=old_level), self.assertRaises(AssertionError): + recipes.verify_level(runtime, {"jars": []}) + + def test_native_level_requires_nonempty_singular_item_and_block_registries(self): + level = {"Data": {"Version": {"Name": "1.14.4"}}, "fml": { + "LoadingModList": [{"ModId": "forge", "ModVersion": "28.2.26"}], + "Registries": {"minecraft:item": {"ids": [1]}, "minecraft:block": {"ids": [1]}}}} + with patch.object(recipes.nbtlib, "load", return_value=level), patch.object(recipes, "digest", return_value="test-hash"): + self.assertEqual("1.14.4", recipes.verify_level(self.root, {"jars": []})["minecraft"]) + for registry in ({"minecraft:items": {"ids": [1]}, "minecraft:blocks": {"ids": [1]}}, + {"minecraft:item": {"ids": []}, "minecraft:block": {"ids": [1]}}): + level["fml"]["Registries"] = registry + with self.subTest(registry=registry), patch.object(recipes.nbtlib, "load", return_value=level), self.assertRaises(AssertionError): + recipes.verify_level(self.root, {"jars": []}) + + def test_native_level_rejects_wrong_forge_or_mod_version(self): + level = {"Data": {"Version": {"Name": "1.14.4"}}, "fml": { + "LoadingModList": [{"ModId": "forge", "ModVersion": "25.0.223"}], "Registries": {}}} + with patch.object(recipes.nbtlib, "load", return_value=level), self.assertRaises(AssertionError): + recipes.verify_level(self.root, {"jars": []}) + level["fml"]["LoadingModList"][0]["ModVersion"] = "28.2.26" + with patch.object(recipes.nbtlib, "load", return_value=level), self.assertRaises(AssertionError): + recipes.verify_level(self.root, {"jars": [{"id": "poweradvantage", "version": "2.4.5.114041"}]}) + + def test_staging_only_copies_binaries_and_replaces_legacy_generator(self): + seed = self.root / "seed" + seed.mkdir() + (seed / "libraries").mkdir() + (seed / "forge-1.14.4-28.2.26.jar").write_bytes(b"test-launcher") + (seed / WORLD_NAME).mkdir() + protected = seed / WORLD_NAME / "untouched.dat" + protected.write_bytes(b"protected-existing-world") + runtime = self.root / "new-runtime" + target = seed, "forge-1.14.4-28.2.26.jar", [] + before = recipes.fingerprint(seed) + with patch.object(recipes, "require_free_port"): + recipes.stage_native(runtime, target, 25577, "unlock") + self.assertEqual(before, recipes.fingerprint(seed)) + self.assertFalse((runtime / WORLD_NAME).exists()) + self.assertTrue((runtime / "advantage-recipes.allow").exists()) + props = (runtime / "server.properties").read_text() + self.assertIn("server-port=25577\n", props) + self.assertIn("server-ip=127.0.0.1\n", props) + self.assertIn("generator-settings=\n", props) + self.assertNotIn("3;minecraft:bedrock", props) + with patch.object(recipes, "require_free_port"), self.assertRaises(FileExistsError): + recipes.stage_native(runtime, target, 25577, "unlock") + + def test_mode_pruning_requires_exact_native_packet_difference(self): + log = self.root / "console-tech.log" + event = "[Server thread/ERROR] [minecraft/ServerRecipeBook]: Tried to load unrecognized recipe: poweradvantage:normal_only removed now.\n" + log.write_text(event * 2) + result = recipes.audit_unlock_log(log, {"poweradvantage:normal_only", "poweradvantage:common"}, {"poweradvantage:common"}) + self.assertEqual(["poweradvantage:normal_only"], result["observed_removed_ids"]) + self.assertEqual(2, len(result["events"])) + with self.assertRaises(AssertionError): + recipes.audit_unlock_log(log, {"poweradvantage:normal_only"}, {"poweradvantage:normal_only"}) + log.write_text("") + with self.assertRaises(AssertionError): + recipes.audit_unlock_log(log, {"poweradvantage:normal_only"}, set()) + + def test_mode_pruning_never_accepts_unrelated_errors_or_bad_recipe_ids(self): + log = self.root / "console.log" + for message in ("[Server thread/ERROR] [minecraft/ServerRecipeBook]: Other failure", + "[Server thread/ERROR] [minecraft/ServerRecipeBook]: Tried to load improperly formatted recipe: bad removed now.", + "[Server thread/ERROR] [minecraft/LootTableManager]: Failed to reload data packs", + "NoSuchMethodError: native failure"): + log.write_text(message) + with self.subTest(message=message), self.assertRaises(AssertionError): + recipes.audit_unlock_log(log, set(), set()) + + +WORLD_NAME = recipes.WORLD + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_refresh.py b/test-rig/migration/test_native114_refresh.py new file mode 100644 index 0000000..1745720 --- /dev/null +++ b/test-rig/migration/test_native114_refresh.py @@ -0,0 +1,25 @@ +from pathlib import Path +import tempfile +import unittest + +from matrix import digest +from native114_refresh import snapshot_name + + +class RefreshProvenanceTests(unittest.TestCase): + def test_only_exact_archived_snapshot_is_selected(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + first = root / "migration-source.json" + second = root / "migration-restarted.json" + first.write_text("source") + second.write_text("different") + self.assertEqual(snapshot_name(root, digest(first)), first.name) + with self.assertRaisesRegex(AssertionError, "No retained"): + snapshot_name(root, "0" * 64) + second.write_text("source") + self.assertEqual(snapshot_name(root, digest(first)), second.name) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_respawn.py b/test-rig/migration/test_native114_respawn.py new file mode 100644 index 0000000..a933efe --- /dev/null +++ b/test-rig/migration/test_native114_respawn.py @@ -0,0 +1,24 @@ +import copy +import unittest +from unittest.mock import patch + +from native114_respawn import native_respawn_verdict + + +class NativeRespawnAdapterTest(unittest.TestCase): + def test_only_exact_native_handler_is_translated(self): + for handler, expected in (("ServerPlayNetHandler.PERFORM_RESPAWN", "NetHandlerPlayServer.PERFORM_RESPAWN"), + ("Other.PERFORM_RESPAWN", "Other.PERFORM_RESPAWN"), + ("NetHandlerPlayServer.PERFORM_RESPAWN", "NetHandlerPlayServer.PERFORM_RESPAWN")): + original = {"respawn": {"handler": handler}, "retain": {"Inventory": [17], "bed": "missing"}} + before = copy.deepcopy(original) + with patch("native114_respawn.respawn_verdict", return_value={"status": "TEST"}) as oracle: + self.assertEqual({"status": "TEST"}, native_respawn_verdict(original, {1: "enchantment"}, {"radius": 10})) + adapted = copy.deepcopy(before) + adapted["respawn"]["handler"] = expected + oracle.assert_called_once_with(adapted, {1: "enchantment"}, {"radius": 10}) + self.assertEqual(before, original) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_ticket_networks.py b/test-rig/migration/test_native114_ticket_networks.py new file mode 100644 index 0000000..19fac78 --- /dev/null +++ b/test-rig/migration/test_native114_ticket_networks.py @@ -0,0 +1,95 @@ +"""Native layout safety and ticket-halo geometry, without accepting oracle failures.""" + +import copy +import json +import unittest +from unittest.mock import patch + +import extended_lifecycle +from native114_partial import NativePartialRun +from native114_ticket_networks import (NativeTicketNetworkRun, bridge_fixtures, extended_fixtures, + fill_commands, halo_chunks, linear_fixtures) + + +class NativeTicketFixtureTests(unittest.TestCase): + def test_historical_drill_fixtures_are_identical(self): + self.assertEqual([f for f in extended_lifecycle.fixtures() if "route" in f], extended_fixtures()[:18]) + + def test_all_native_bounds_and_paths_are_contained_and_do_not_overlap(self): + for definition in (linear_fixtures(), bridge_fixtures(), extended_fixtures()): + for index, f in enumerate(definition): + for p in f["witnesses"]: + self.assertTrue(all(a <= n <= b for a, n, b in zip(f["min"], p, f["max"]))) + for other in definition[index + 1:]: + self.assertTrue(any(f["max"][a] < other["min"][a] or other["max"][a] < f["min"][a] for a in range(3))) + + def test_retained_ticket_is_outside_selected_halo(self): + for f in linear_fixtures(): + for group, other in (("supply", "machine"), ("machine", "supply")): + self.assertTrue(set(map(tuple, f["groups"][group + "halo"])).isdisjoint(map(tuple, f["groups"][other]))) + core = f["groups"][group][0] + all_chunks = {(x, z) for x in range(f["min"][0] >> 4, (f["max"][0] >> 4) + 1) + for z in range(f["min"][2] >> 4, (f["max"][2] >> 4) + 1)} + retained = all_chunks - set(map(tuple, f["groups"][group + "halo"])) + self.assertTrue(all(max(abs(x-core[0]), abs(z-core[1])) > 2 for x, z in retained)) + + def test_bridge_halo_does_not_remove_endpoint_ticket(self): + for f in bridge_fixtures(): + self.assertTrue(set(map(tuple, f["groups"]["bridgehalo"])).isdisjoint(map(tuple, f["groups"]["endpoints"]))) + self.assertEqual(f["groups"]["bridgehalo"], halo_chunks(f, "bridge")) + + def test_linear_path_is_native_neighbor_connected_and_128_blocks_long(self): + for f in linear_fixtures(): + path = [f["witnesses"][0], *f["pipes"], f["witnesses"][4]] + self.assertEqual(len(path), 129) + self.assertTrue(all(sum(abs(x-y) for x, y in zip(a, b)) == 1 for a, b in zip(path, path[1:]))) + self.assertEqual(len(set(map(tuple, f["witnesses"]))), len(f["witnesses"])) + + def test_bridge_is_one_connected_u_without_shortcut(self): + for f in bridge_fixtures(): + path = [f["witnesses"][0], *f["pipes"], f["witnesses"][1]] + self.assertTrue(all(sum(abs(x-y) for x, y in zip(a, b)) == 1 for a, b in zip(path, path[1:]))) + self.assertEqual(len(set(map(tuple, path))), len(path)) + self.assertEqual(len(f["pipes"]), 259) + + def test_clearing_is_split_and_exactly_confined_to_declared_bounds(self): + for f in linear_fixtures() + bridge_fixtures(): + command = "fill " + " ".join(map(str, f["min"] + f["max"])) + " minecraft:air replace" + covered = set() + for piece in fill_commands([f], command): + p = piece.split() + low, high = list(map(int, p[1:4])), list(map(int, p[4:7])) + volume = 1 + for a, b in zip(low, high): + volume *= b - a + 1 + self.assertLessEqual(volume, 32768) + cells = {(x, z) for x in range(low[0], high[0] + 1) for z in range(low[2], high[2] + 1)} + self.assertTrue(covered.isdisjoint(cells)) + covered |= cells + self.assertEqual(len(covered), 160 * 32) + outside = command.replace(str(f["min"][0]), "0", 1) + self.assertIsNone(fill_commands([f], outside)) + + def test_release_observes_before_releasing_halo_then_core(self): + runner = NativeTicketNetworkRun(None, None, None, linear_fixtures()) + before = {"raw": "before"} + with patch.object(runner, "_raw_probe", side_effect=[before, {}, {"raw": "after"}]) as raw: + runner.probe("release-supply", "released") + self.assertEqual([call.args[0] for call in raw.call_args_list], + ["snapshot", "release-supplyhalo", "release-supply"]) + self.assertIs(runner._released_groups["supply"]["before"], before) + + def test_load_restores_only_requested_halo_before_core_readiness(self): + runner = NativeTicketNetworkRun(None, None, None, linear_fixtures()) + runner._released_groups["supply"] = {} + runner._released_groups["machine"] = {} + with patch.object(runner, "_raw_probe") as raw, patch.object(runner, "_load_ready", return_value={}) as ready: + runner.probe("load-supply", "loaded") + self.assertEqual(raw.call_args.args[0], "load-supplyhalo") + ready.assert_called_once_with("load-supply", "loaded") + self.assertNotIn("supply", runner._released_groups) + self.assertIn("machine", runner._released_groups) + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/migration/test_native114_transformer_order.py b/test-rig/migration/test_native114_transformer_order.py new file mode 100644 index 0000000..f6a31b0 --- /dev/null +++ b/test-rig/migration/test_native114_transformer_order.py @@ -0,0 +1,33 @@ +import unittest +import json + +import transformer_order as historical +import test_transformer_order as historical_tests +from native114_transformer_order import COREMODS, SUFFIXES, native_order_evidence + + +class NativeTransformerOrderAdapterTest(unittest.TestCase): + def test_oracle_code_is_unchanged_with_only_platform_targets_bound(self): + self.assertIs(historical.order_evidence.__code__, native_order_evidence.__code__) + self.assertEqual("net/minecraft/world/storage/SaveFormat", SUFFIXES["world_info"][0]) + self.assertEqual("net/minecraft/world/chunk/storage/ChunkLoader", SUFFIXES["chunk_status"][0]) + self.assertEqual(historical.SUFFIXES["worldgen_guard"], SUFFIXES["worldgen_guard"]) + self.assertNotEqual(historical.SUFFIXES, SUFFIXES) + self.assertEqual("coremods-1.0.0.jar", COREMODS.name) + + +class NativeOrderEvidenceNegativeControls(historical_tests.OrderEvidenceTests): + def setUp(self): + super().setUp() + mapping = {old[0]: SUFFIXES[suffix][0] for suffix, old in historical.SUFFIXES.items()} + for event in self.events: + if "class" in event: + event["class"] = mapping[event["class"]] + + def prove(self): + (self.root / "transformer-order-events.jsonl").write_text("\n".join(json.dumps(event) for event in self.events)) + return native_order_evidence(self.root, "bm-first") + + +if __name__ == "__main__": + unittest.main() diff --git a/test-rig/profiles/native114/dev-core-apocalyptic.json b/test-rig/profiles/native114/dev-core-apocalyptic.json new file mode 100644 index 0000000..55b7a27 --- /dev/null +++ b/test-rig/profiles/native114/dev-core-apocalyptic.json @@ -0,0 +1,50 @@ +{ + "name": "dev-core-apocalyptic", + "mode": "development", + "runtimeBase": "build/works-native114", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "shutdownTimeoutSeconds": 60, + "stationRunSeconds": 5, + "recipeMode": "APOCALYPTIC", + "sourceCommit": "working-tree-unqualified", + "sourceVersion": "2.4.5.114041", + "lifecycleStations": [], + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.0.114041", + "file": "AdvantageWorksTestHarness-1.1.0.114041-deobf.jar", + "path": "build/libs-dev/AdvantageWorksTestHarness-1.1.0.114041-deobf.jar", + "workspaceProduced": true, + "sha256": "690C49EAA9706AF2376789B44FE86C9756DBB101A94B7D0652EF800DAA766769" + }, + { + "id": "steamadvantage", + "version": "2.2.6.114041", + "file": "SteamAdvantage-2.2.6.114041-deobf.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs-dev/SteamAdvantage-2.2.6.114041-deobf.jar", + "workspaceProduced": true, + "sha256": "7CD599946DE35B189D9363693C67AAC7897D7EB763AEF8C383F9E6CFCA3D8C7C" + }, + { + "id": "electricadvantage", + "version": "2.2.6.114041", + "file": "ElectricAdvantage-2.2.6.114041-deobf.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs-dev/ElectricAdvantage-2.2.6.114041-deobf.jar", + "workspaceProduced": true, + "sha256": "2B32B667092D3BFBC4CF905AE0C630C2767B3C6E191B62035FF2ED0CDEEBF05E" + } + ], + "minecraftVersion": "1.14.4", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/dev-core-apocalyptic.json", + "forge": { + "version": "1.14.4-28.2.26", + "installerPath": ".cache/forge-1.14.4-28.2.26-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.14.4-28.2.26/forge-1.14.4-28.2.26-installer.jar", + "sha256": "E68A0E2D589B2954C5D109A04B80747AC8C061ACABE074DB1607F9C2FD7D2CE1" + }, + "generatorSettings": "" +} diff --git a/test-rig/profiles/native114/dev-core-tech-progression.json b/test-rig/profiles/native114/dev-core-tech-progression.json new file mode 100644 index 0000000..ddb3527 --- /dev/null +++ b/test-rig/profiles/native114/dev-core-tech-progression.json @@ -0,0 +1,50 @@ +{ + "name": "dev-core-tech-progression", + "mode": "development", + "runtimeBase": "build/works-native114", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "shutdownTimeoutSeconds": 60, + "stationRunSeconds": 5, + "recipeMode": "TECH_PROGRESSION", + "sourceCommit": "working-tree-unqualified", + "sourceVersion": "2.4.5.114041", + "lifecycleStations": [], + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.0.114041", + "file": "AdvantageWorksTestHarness-1.1.0.114041-deobf.jar", + "path": "build/libs-dev/AdvantageWorksTestHarness-1.1.0.114041-deobf.jar", + "workspaceProduced": true, + "sha256": "690C49EAA9706AF2376789B44FE86C9756DBB101A94B7D0652EF800DAA766769" + }, + { + "id": "steamadvantage", + "version": "2.2.6.114041", + "file": "SteamAdvantage-2.2.6.114041-deobf.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs-dev/SteamAdvantage-2.2.6.114041-deobf.jar", + "workspaceProduced": true, + "sha256": "7CD599946DE35B189D9363693C67AAC7897D7EB763AEF8C383F9E6CFCA3D8C7C" + }, + { + "id": "electricadvantage", + "version": "2.2.6.114041", + "file": "ElectricAdvantage-2.2.6.114041-deobf.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs-dev/ElectricAdvantage-2.2.6.114041-deobf.jar", + "workspaceProduced": true, + "sha256": "2B32B667092D3BFBC4CF905AE0C630C2767B3C6E191B62035FF2ED0CDEEBF05E" + } + ], + "minecraftVersion": "1.14.4", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/dev-core-tech-progression.json", + "forge": { + "version": "1.14.4-28.2.26", + "installerPath": ".cache/forge-1.14.4-28.2.26-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.14.4-28.2.26/forge-1.14.4-28.2.26-installer.jar", + "sha256": "E68A0E2D589B2954C5D109A04B80747AC8C061ACABE074DB1607F9C2FD7D2CE1" + }, + "generatorSettings": "" +} diff --git a/test-rig/profiles/native114/dev-core.json b/test-rig/profiles/native114/dev-core.json new file mode 100644 index 0000000..34c3a63 --- /dev/null +++ b/test-rig/profiles/native114/dev-core.json @@ -0,0 +1,49 @@ +{ + "name": "dev-core", + "mode": "development", + "runtimeBase": "build/works-native114", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "shutdownTimeoutSeconds": 60, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "working-tree-unqualified", + "sourceVersion": "2.4.5.114041", + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.0.114041", + "file": "AdvantageWorksTestHarness-1.1.0.114041-deobf.jar", + "path": "build/libs-dev/AdvantageWorksTestHarness-1.1.0.114041-deobf.jar", + "workspaceProduced": true, + "sha256": "690C49EAA9706AF2376789B44FE86C9756DBB101A94B7D0652EF800DAA766769" + }, + { + "id": "steamadvantage", + "version": "2.2.6.114041", + "file": "SteamAdvantage-2.2.6.114041-deobf.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs-dev/SteamAdvantage-2.2.6.114041-deobf.jar", + "workspaceProduced": true, + "sha256": "7CD599946DE35B189D9363693C67AAC7897D7EB763AEF8C383F9E6CFCA3D8C7C" + }, + { + "id": "electricadvantage", + "version": "2.2.6.114041", + "file": "ElectricAdvantage-2.2.6.114041-deobf.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs-dev/ElectricAdvantage-2.2.6.114041-deobf.jar", + "workspaceProduced": true, + "sha256": "2B32B667092D3BFBC4CF905AE0C630C2767B3C6E191B62035FF2ED0CDEEBF05E" + } + ], + "minecraftVersion": "1.14.4", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/dev-core.json", + "forge": { + "version": "1.14.4-28.2.26", + "installerPath": ".cache/forge-1.14.4-28.2.26-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.14.4-28.2.26/forge-1.14.4-28.2.26-installer.jar", + "sha256": "E68A0E2D589B2954C5D109A04B80747AC8C061ACABE074DB1607F9C2FD7D2CE1" + }, + "generatorSettings": "" +} diff --git a/test-rig/profiles/native114/packaged-current.json b/test-rig/profiles/native114/packaged-current.json new file mode 100644 index 0000000..a219c8b --- /dev/null +++ b/test-rig/profiles/native114/packaged-current.json @@ -0,0 +1,73 @@ +{ + "name": "packaged-current", + "mode": "packaged", + "runtimeBase": "build/works-native114", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "shutdownTimeoutSeconds": 60, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "working-tree-unqualified", + "sourceVersion": "2.4.5.114041", + "forge": { + "version": "1.14.4-28.2.26", + "installerPath": ".cache/forge-1.14.4-28.2.26-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.14.4-28.2.26/forge-1.14.4-28.2.26-installer.jar", + "sha256": "E68A0E2D589B2954C5D109A04B80747AC8C061ACABE074DB1607F9C2FD7D2CE1" + }, + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.0.114041", + "file": "AdvantageWorksTestHarness-1.1.0.114041.jar", + "path": "build/libs/AdvantageWorksTestHarness-1.1.0.114041.jar", + "workspaceProduced": true, + "sha256": "09DE1F386774C4C2B36AE95A4E636D35D6A2BCCAEE938BD5A05E6EE356652C3B" + }, + { + "id": "poweradvantage", + "version": "2.4.5.114041", + "file": "PowerAdvantage-2.4.5.114041.jar", + "path": "../build/libs/PowerAdvantage-2.4.5.114041.jar", + "workspaceProduced": true, + "sha256": "33F57A1401A13597DCA53F4E49EAEDDD2D5CB3C3F406D5200902D5BA9CB41352" + }, + { + "id": "steamadvantage", + "version": "2.2.6.114041", + "file": "SteamAdvantage-2.2.6.114041.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs/SteamAdvantage-2.2.6.114041.jar", + "workspaceProduced": true, + "sha256": "444872113E13D806D4520CCBD9B3230C931353D43875BAE57EB9B5FC86614669" + }, + { + "id": "electricadvantage", + "version": "2.2.6.114041", + "file": "ElectricAdvantage-2.2.6.114041.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs/ElectricAdvantage-2.2.6.114041.jar", + "workspaceProduced": true, + "sha256": "5BE41447AF5AA3AF60C145B3A862BC42802C620C637D1E65810B350A6A9AFBA9" + }, + { + "id": "basemetals", + "version": "3.0.1.114041", + "file": "BaseMetals-3.0.1.114041.jar", + "path": "../../../../BaseMetals/BaseMetals 114/BaseMetals/build/libs/BaseMetals-3.0.1.114041.jar", + "workspaceProduced": true, + "sha256": "8C5416A59F88C41883DB2B8DCD180E2BDC29AD00EF6DF964D67B6C49AEB6EB9E" + }, + { + "id": "orespawn", + "version": "4.1.1.114041", + "file": "OreSpawn-4.1.1.114041.jar", + "path": "../../../../MinecraftMineralogy/OS114/OreSpawn/build/libs/OreSpawn-4.1.1.114041.jar", + "workspaceProduced": true, + "sha256": "219AE0FA08CCE400C3952AECEA9EB93B8C193D4337B2ECC09A382E6B1D525294" + } + ], + "minecraftVersion": "1.14.4", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/packaged-current.json", + "generatorSettings": "" +} diff --git a/test-rig/profiles/native114/packaged-decoupled.json b/test-rig/profiles/native114/packaged-decoupled.json new file mode 100644 index 0000000..2069da8 --- /dev/null +++ b/test-rig/profiles/native114/packaged-decoupled.json @@ -0,0 +1,65 @@ +{ + "name": "packaged-decoupled", + "mode": "packaged", + "runtimeBase": "build/works-native114", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "shutdownTimeoutSeconds": 60, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "working-tree-unqualified", + "sourceVersion": "2.4.5.114041", + "forge": { + "version": "1.14.4-28.2.26", + "installerPath": ".cache/forge-1.14.4-28.2.26-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.14.4-28.2.26/forge-1.14.4-28.2.26-installer.jar", + "sha256": "E68A0E2D589B2954C5D109A04B80747AC8C061ACABE074DB1607F9C2FD7D2CE1" + }, + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.0.114041", + "file": "AdvantageWorksTestHarness-1.1.0.114041.jar", + "path": "build/libs/AdvantageWorksTestHarness-1.1.0.114041.jar", + "workspaceProduced": true, + "sha256": "09DE1F386774C4C2B36AE95A4E636D35D6A2BCCAEE938BD5A05E6EE356652C3B" + }, + { + "id": "poweradvantage", + "version": "2.4.5.114041", + "file": "PowerAdvantage-2.4.5.114041.jar", + "path": "../build/libs/PowerAdvantage-2.4.5.114041.jar", + "workspaceProduced": true, + "sha256": "33F57A1401A13597DCA53F4E49EAEDDD2D5CB3C3F406D5200902D5BA9CB41352" + }, + { + "id": "steamadvantage", + "version": "2.2.6.114041", + "file": "SteamAdvantage-2.2.6.114041.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs/SteamAdvantage-2.2.6.114041.jar", + "workspaceProduced": true, + "sha256": "444872113E13D806D4520CCBD9B3230C931353D43875BAE57EB9B5FC86614669" + }, + { + "id": "electricadvantage", + "version": "2.2.6.114041", + "file": "ElectricAdvantage-2.2.6.114041.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs/ElectricAdvantage-2.2.6.114041.jar", + "workspaceProduced": true, + "sha256": "5BE41447AF5AA3AF60C145B3A862BC42802C620C637D1E65810B350A6A9AFBA9" + }, + { + "id": "orespawn", + "version": "4.1.1.114041", + "file": "OreSpawn-4.1.1.114041.jar", + "path": "../../../../MinecraftMineralogy/OS114/OreSpawn/build/libs/OreSpawn-4.1.1.114041.jar", + "workspaceProduced": true, + "sha256": "219AE0FA08CCE400C3952AECEA9EB93B8C193D4337B2ECC09A382E6B1D525294" + } + ], + "minecraftVersion": "1.14.4", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/packaged-decoupled.json", + "generatorSettings": "" +} diff --git a/test-rig/profiles/native114/packaged-mineralogy.json b/test-rig/profiles/native114/packaged-mineralogy.json new file mode 100644 index 0000000..d421a26 --- /dev/null +++ b/test-rig/profiles/native114/packaged-mineralogy.json @@ -0,0 +1,78 @@ +{ + "name": "packaged-mineralogy", + "mode": "packaged", + "runtimeBase": "build/works-native114", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "shutdownTimeoutSeconds": 60, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "working-tree-unqualified", + "sourceVersion": "2.4.5.114041", + "forge": { + "version": "1.14.4-28.2.26", + "installerPath": ".cache/forge-1.14.4-28.2.26-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.14.4-28.2.26/forge-1.14.4-28.2.26-installer.jar", + "sha256": "E68A0E2D589B2954C5D109A04B80747AC8C061ACABE074DB1607F9C2FD7D2CE1" + }, + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.0.114041", + "file": "AdvantageWorksTestHarness-1.1.0.114041.jar", + "path": "build/libs/AdvantageWorksTestHarness-1.1.0.114041.jar", + "workspaceProduced": true, + "sha256": "09DE1F386774C4C2B36AE95A4E636D35D6A2BCCAEE938BD5A05E6EE356652C3B" + }, + { + "id": "poweradvantage", + "version": "2.4.5.114041", + "file": "PowerAdvantage-2.4.5.114041.jar", + "path": "../build/libs/PowerAdvantage-2.4.5.114041.jar", + "workspaceProduced": true, + "sha256": "33F57A1401A13597DCA53F4E49EAEDDD2D5CB3C3F406D5200902D5BA9CB41352" + }, + { + "id": "steamadvantage", + "version": "2.2.6.114041", + "file": "SteamAdvantage-2.2.6.114041.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs/SteamAdvantage-2.2.6.114041.jar", + "workspaceProduced": true, + "sha256": "444872113E13D806D4520CCBD9B3230C931353D43875BAE57EB9B5FC86614669" + }, + { + "id": "electricadvantage", + "version": "2.2.6.114041", + "file": "ElectricAdvantage-2.2.6.114041.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs/ElectricAdvantage-2.2.6.114041.jar", + "workspaceProduced": true, + "sha256": "5BE41447AF5AA3AF60C145B3A862BC42802C620C637D1E65810B350A6A9AFBA9" + }, + { + "id": "orespawn", + "version": "4.1.1.114041", + "file": "OreSpawn-4.1.1.114041.jar", + "path": "../../../../MinecraftMineralogy/OS114/OreSpawn/build/libs/OreSpawn-4.1.1.114041.jar", + "workspaceProduced": true, + "sha256": "219AE0FA08CCE400C3952AECEA9EB93B8C193D4337B2ECC09A382E6B1D525294" + }, + { + "id": "mineralogy", + "version": "6.0.1.114041", + "file": "Mineralogy-6.0.1.114041.jar", + "path": "../../../../MinecraftMineralogy/MinecraftMineralogy 114-new/MinecraftMineralogy/build/libs/Mineralogy-6.0.1.114041.jar", + "sourceRef": "5bcec041775bed9de562075d177e33c507b2e2e1", + "workspaceProduced": true, + "sha256": "75CE58BCDCB845407DE55F476FDB06211E2F8DBE6FCB0AC0F7FBDB5B7841F7BA" + } + ], + "lifecycleStations": [ + "W-03", + "S-01" + ], + "minecraftVersion": "1.14.4", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/packaged-mineralogy.json", + "generatorSettings": "" +} diff --git a/test-rig/profiles/native114/packaged-optional-power.json b/test-rig/profiles/native114/packaged-optional-power.json new file mode 100644 index 0000000..dc4df3e --- /dev/null +++ b/test-rig/profiles/native114/packaged-optional-power.json @@ -0,0 +1,65 @@ +{ + "name": "packaged-optional-power", + "mode": "packaged", + "runtimeBase": "build/works-native114", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "shutdownTimeoutSeconds": 60, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "working-tree-unqualified", + "sourceVersion": "2.4.5.114041", + "forge": { + "version": "1.14.4-28.2.26", + "installerPath": ".cache/forge-1.14.4-28.2.26-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.14.4-28.2.26/forge-1.14.4-28.2.26-installer.jar", + "sha256": "E68A0E2D589B2954C5D109A04B80747AC8C061ACABE074DB1607F9C2FD7D2CE1" + }, + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.0.114041", + "file": "AdvantageWorksTestHarness-1.1.0.114041.jar", + "path": "build/libs/AdvantageWorksTestHarness-1.1.0.114041.jar", + "workspaceProduced": true, + "sha256": "09DE1F386774C4C2B36AE95A4E636D35D6A2BCCAEE938BD5A05E6EE356652C3B" + }, + { + "id": "poweradvantage", + "version": "2.4.5.114041", + "file": "PowerAdvantage-2.4.5.114041.jar", + "path": "../build/libs/PowerAdvantage-2.4.5.114041.jar", + "workspaceProduced": true, + "sha256": "33F57A1401A13597DCA53F4E49EAEDDD2D5CB3C3F406D5200902D5BA9CB41352" + }, + { + "id": "steamadvantage", + "version": "2.2.6.114041", + "file": "SteamAdvantage-2.2.6.114041.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs/SteamAdvantage-2.2.6.114041.jar", + "workspaceProduced": true, + "sha256": "444872113E13D806D4520CCBD9B3230C931353D43875BAE57EB9B5FC86614669" + }, + { + "id": "electricadvantage", + "version": "2.2.6.114041", + "file": "ElectricAdvantage-2.2.6.114041.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs/ElectricAdvantage-2.2.6.114041.jar", + "workspaceProduced": true, + "sha256": "5BE41447AF5AA3AF60C145B3A862BC42802C620C637D1E65810B350A6A9AFBA9" + }, + { + "id": "orespawn", + "version": "4.1.1.114041", + "file": "OreSpawn-4.1.1.114041.jar", + "path": "../../../../MinecraftMineralogy/OS114/OreSpawn/build/libs/OreSpawn-4.1.1.114041.jar", + "workspaceProduced": true, + "sha256": "219AE0FA08CCE400C3952AECEA9EB93B8C193D4337B2ECC09A382E6B1D525294" + } + ], + "minecraftVersion": "1.14.4", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/packaged-optional-power.json", + "generatorSettings": "" +} diff --git a/test-rig/profiles/native114/worldgen-advantage.json b/test-rig/profiles/native114/worldgen-advantage.json new file mode 100644 index 0000000..848b200 --- /dev/null +++ b/test-rig/profiles/native114/worldgen-advantage.json @@ -0,0 +1,157 @@ +{ + "mode": "packaged", + "runtimeBase": "build/works-native114", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "working-tree-unqualified", + "sourceVersion": "2.4.5.114041", + "forge": { + "version": "1.14.4-28.2.26", + "installerPath": ".cache/forge-1.14.4-28.2.26-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.14.4-28.2.26/forge-1.14.4-28.2.26-installer.jar", + "sha256": "E68A0E2D589B2954C5D109A04B80747AC8C061ACABE074DB1607F9C2FD7D2CE1" + }, + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.0.114041", + "file": "AdvantageWorksTestHarness-1.1.0.114041.jar", + "path": "build/libs/AdvantageWorksTestHarness-1.1.0.114041.jar", + "workspaceProduced": true, + "sha256": "09DE1F386774C4C2B36AE95A4E636D35D6A2BCCAEE938BD5A05E6EE356652C3B" + }, + { + "id": "poweradvantage", + "version": "2.4.5.114041", + "file": "PowerAdvantage-2.4.5.114041.jar", + "path": "../build/libs/PowerAdvantage-2.4.5.114041.jar", + "workspaceProduced": true, + "sha256": "33F57A1401A13597DCA53F4E49EAEDDD2D5CB3C3F406D5200902D5BA9CB41352" + }, + { + "id": "steamadvantage", + "version": "2.2.6.114041", + "file": "SteamAdvantage-2.2.6.114041.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs/SteamAdvantage-2.2.6.114041.jar", + "workspaceProduced": true, + "sha256": "444872113E13D806D4520CCBD9B3230C931353D43875BAE57EB9B5FC86614669" + }, + { + "id": "electricadvantage", + "version": "2.2.6.114041", + "file": "ElectricAdvantage-2.2.6.114041.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs/ElectricAdvantage-2.2.6.114041.jar", + "workspaceProduced": true, + "sha256": "5BE41447AF5AA3AF60C145B3A862BC42802C620C637D1E65810B350A6A9AFBA9" + }, + { + "id": "orespawn", + "version": "4.1.1.114041", + "file": "OreSpawn-4.1.1.114041.jar", + "path": "../../../../MinecraftMineralogy/OS114/OreSpawn/build/libs/OreSpawn-4.1.1.114041.jar", + "workspaceProduced": true, + "sha256": "219AE0FA08CCE400C3952AECEA9EB93B8C193D4337B2ECC09A382E6B1D525294" + } + ], + "name": "worldgen-advantage", + "levelName": "AdvantageWorksWorldgen", + "levelSeed": "advantage-works-worldgen-1.14.4-v1", + "levelType": "DEFAULT", + "generatorSettings": "", + "buildWorks": false, + "worldgenSampleRadius": 8, + "shutdownTimeoutSeconds": 300, + "lifecycleStations": [], + "firstProfileAssertions": [ + { + "id": "desert-oil-visible", + "section": "fluid_deposits", + "entry": "poweradvantage:fluid_deposit/desert_crude_oil", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "desert-oil-owner", + "section": "fluid_deposits", + "entry": "poweradvantage:fluid_deposit/desert_crude_oil", + "present": true, + "field": "source_provider", + "expectedString": "poweradvantage" + }, + { + "id": "electric-sulfur-default", + "section": "ores", + "entry": "electricadvantage:ore/sulfur", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "electric-lithium-default", + "section": "ores", + "entry": "electricadvantage:ore/lithium", + "present": true, + "field": "enabled", + "expectedBoolean": true + } + ], + "profileMutations": [ + { + "section": "fluid_deposits", + "entry": "poweradvantage:fluid_deposit/desert_crude_oil", + "field": "enabled", + "booleanValue": false + }, + { + "section": "ores", + "entry": "electricadvantage:ore/sulfur", + "field": "enabled", + "booleanValue": false + } + ], + "restartProfileAssertions": [ + { + "id": "desert-oil-user-disable-persists", + "section": "fluid_deposits", + "entry": "poweradvantage:fluid_deposit/desert_crude_oil", + "present": true, + "field": "enabled", + "expectedBoolean": false + }, + { + "id": "electric-sulfur-user-disable-persists", + "section": "ores", + "entry": "electricadvantage:ore/sulfur", + "present": true, + "field": "enabled", + "expectedBoolean": false + }, + { + "id": "electric-lithium-remains-enabled", + "section": "ores", + "entry": "electricadvantage:ore/lithium", + "present": true, + "field": "enabled", + "expectedBoolean": true + } + ], + "worldgenExpectations": { + "minimumCounts": { + "poweradvantage:crude_oil": 1, + "electricadvantage:li_ore": 1, + "electricadvantage:sulfur_ore": 1 + }, + "maximumCounts": { + "mineralogy:crude_oil": 0, + "mineralogy:sulfur_ore": 0 + }, + "zeroBiomeViolations": true + }, + "minecraftVersion": "1.14.4", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/worldgen-advantage.json" +} diff --git a/test-rig/profiles/native114/worldgen-legacy-import.json b/test-rig/profiles/native114/worldgen-legacy-import.json new file mode 100644 index 0000000..a4a2815 --- /dev/null +++ b/test-rig/profiles/native114/worldgen-legacy-import.json @@ -0,0 +1,224 @@ +{ + "mode": "packaged", + "runtimeBase": "build/works-native114", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "working-tree-unqualified", + "sourceVersion": "2.4.5.114041", + "forge": { + "version": "1.14.4-28.2.26", + "installerPath": ".cache/forge-1.14.4-28.2.26-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.14.4-28.2.26/forge-1.14.4-28.2.26-installer.jar", + "sha256": "E68A0E2D589B2954C5D109A04B80747AC8C061ACABE074DB1607F9C2FD7D2CE1" + }, + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.0.114041", + "file": "AdvantageWorksTestHarness-1.1.0.114041.jar", + "path": "build/libs/AdvantageWorksTestHarness-1.1.0.114041.jar", + "workspaceProduced": true, + "sha256": "09DE1F386774C4C2B36AE95A4E636D35D6A2BCCAEE938BD5A05E6EE356652C3B" + }, + { + "id": "poweradvantage", + "version": "2.4.5.114041", + "file": "PowerAdvantage-2.4.5.114041.jar", + "path": "../build/libs/PowerAdvantage-2.4.5.114041.jar", + "workspaceProduced": true, + "sha256": "33F57A1401A13597DCA53F4E49EAEDDD2D5CB3C3F406D5200902D5BA9CB41352" + }, + { + "id": "steamadvantage", + "version": "2.2.6.114041", + "file": "SteamAdvantage-2.2.6.114041.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs/SteamAdvantage-2.2.6.114041.jar", + "workspaceProduced": true, + "sha256": "444872113E13D806D4520CCBD9B3230C931353D43875BAE57EB9B5FC86614669" + }, + { + "id": "electricadvantage", + "version": "2.2.6.114041", + "file": "ElectricAdvantage-2.2.6.114041.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs/ElectricAdvantage-2.2.6.114041.jar", + "workspaceProduced": true, + "sha256": "5BE41447AF5AA3AF60C145B3A862BC42802C620C637D1E65810B350A6A9AFBA9" + }, + { + "id": "orespawn", + "version": "4.1.1.114041", + "file": "OreSpawn-4.1.1.114041.jar", + "path": "../../../../MinecraftMineralogy/OS114/OreSpawn/build/libs/OreSpawn-4.1.1.114041.jar", + "workspaceProduced": true, + "sha256": "219AE0FA08CCE400C3952AECEA9EB93B8C193D4337B2ECC09A382E6B1D525294" + } + ], + "name": "worldgen-legacy-import", + "levelName": "AdvantageWorksWorldgen", + "levelSeed": "advantage-works-worldgen-1.14.4-v1", + "levelType": "DEFAULT", + "generatorSettings": "", + "buildWorks": false, + "worldgenSampleRadius": 8, + "stableWorldgenCountExclusions": [ + "poweradvantage:crude_oil" + ], + "shutdownTimeoutSeconds": 300, + "lifecycleStations": [], + "configFixtures": [ + { + "source": "../../../PA113/PowerAdvantage/test-rig/../src/test/resources/orespawn-legacy/poweradvantage.json", + "destination": "config/orespawn/poweradvantage.json" + }, + { + "source": "../../../PA113/PowerAdvantage/test-rig/../../ElectricAdvantage/ElectricAdvantage/src/test/resources/orespawn-legacy/electricadvantage.json", + "destination": "config/orespawn/electricadvantage.json" + } + ], + "firstProfileAssertions": [ + { + "id": "legacy-oil-retained", + "section": "ores", + "entry": "poweradvantage:legacy/poweradvantage_crude_oil_0", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "legacy-lithium-retained", + "section": "ores", + "entry": "electricadvantage:legacy/electricadvantage_li_ore_0", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "legacy-sulfur-retained", + "section": "ores", + "entry": "electricadvantage:legacy/electricadvantage_sulfur_ore_1", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "modern-oil-not-duplicated", + "section": "fluid_deposits", + "entry": "poweradvantage:fluid_deposit/desert_crude_oil", + "present": false + }, + { + "id": "modern-lithium-not-duplicated", + "section": "ores", + "entry": "electricadvantage:ore/lithium", + "present": false + }, + { + "id": "modern-sulfur-not-duplicated", + "section": "ores", + "entry": "electricadvantage:ore/sulfur", + "present": false + } + ], + "restartProfileAssertions": [ + { + "id": "legacy-oil-retained", + "section": "ores", + "entry": "poweradvantage:legacy/poweradvantage_crude_oil_0", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "legacy-lithium-retained", + "section": "ores", + "entry": "electricadvantage:legacy/electricadvantage_li_ore_0", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "legacy-sulfur-retained", + "section": "ores", + "entry": "electricadvantage:legacy/electricadvantage_sulfur_ore_1", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "modern-oil-not-duplicated", + "section": "fluid_deposits", + "entry": "poweradvantage:fluid_deposit/desert_crude_oil", + "present": false + }, + { + "id": "modern-lithium-not-duplicated", + "section": "ores", + "entry": "electricadvantage:ore/lithium", + "present": false + }, + { + "id": "modern-sulfur-not-duplicated", + "section": "ores", + "entry": "electricadvantage:ore/sulfur", + "present": false + } + ], + "firstFileAssertions": [ + { + "id": "migration-report", + "path": "config/orespawn-os3-migration-report.json", + "contains": "os1_config_source", + "minimumOccurrences": 2 + }, + { + "id": "power-provider", + "path": "config/poweradvantage-orespawn.json", + "contains": "poweradvantage:legacy/poweradvantage_crude_oil_0", + "minimumOccurrences": 1 + }, + { + "id": "electric-provider", + "path": "config/electricadvantage-orespawn.json", + "contains": "electricadvantage:legacy/electricadvantage_li_ore_0", + "minimumOccurrences": 1 + } + ], + "restartFileAssertions": [ + { + "id": "migration-report", + "path": "config/orespawn-os3-migration-report.json", + "contains": "os1_config_source", + "minimumOccurrences": 2 + }, + { + "id": "power-provider", + "path": "config/poweradvantage-orespawn.json", + "contains": "poweradvantage:legacy/poweradvantage_crude_oil_0", + "minimumOccurrences": 1 + }, + { + "id": "electric-provider", + "path": "config/electricadvantage-orespawn.json", + "contains": "electricadvantage:legacy/electricadvantage_li_ore_0", + "minimumOccurrences": 1 + } + ], + "worldgenExpectations": { + "minimumCounts": { + "poweradvantage:crude_oil": 1, + "electricadvantage:li_ore": 1, + "electricadvantage:sulfur_ore": 1 + }, + "maximumCounts": { + "mineralogy:crude_oil": 0, + "mineralogy:sulfur_ore": 0 + }, + "zeroBiomeViolations": false + }, + "minecraftVersion": "1.14.4", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/worldgen-legacy-import.json" +} diff --git a/test-rig/profiles/native114/worldgen-mineralogy.json b/test-rig/profiles/native114/worldgen-mineralogy.json new file mode 100644 index 0000000..4920996 --- /dev/null +++ b/test-rig/profiles/native114/worldgen-mineralogy.json @@ -0,0 +1,168 @@ +{ + "mode": "packaged", + "runtimeBase": "build/works-native114", + "serverPort": 25565, + "startupTimeoutSeconds": 180, + "stationRunSeconds": 5, + "recipeMode": "NORMAL", + "sourceCommit": "working-tree-unqualified", + "sourceVersion": "2.4.5.114041", + "forge": { + "version": "1.14.4-28.2.26", + "installerPath": ".cache/forge-1.14.4-28.2.26-installer.jar", + "url": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.14.4-28.2.26/forge-1.14.4-28.2.26-installer.jar", + "sha256": "E68A0E2D589B2954C5D109A04B80747AC8C061ACABE074DB1607F9C2FD7D2CE1" + }, + "mods": [ + { + "id": "advantageworkstest", + "version": "1.1.0.114041", + "file": "AdvantageWorksTestHarness-1.1.0.114041.jar", + "path": "build/libs/AdvantageWorksTestHarness-1.1.0.114041.jar", + "workspaceProduced": true, + "sha256": "09DE1F386774C4C2B36AE95A4E636D35D6A2BCCAEE938BD5A05E6EE356652C3B" + }, + { + "id": "poweradvantage", + "version": "2.4.5.114041", + "file": "PowerAdvantage-2.4.5.114041.jar", + "path": "../build/libs/PowerAdvantage-2.4.5.114041.jar", + "workspaceProduced": true, + "sha256": "33F57A1401A13597DCA53F4E49EAEDDD2D5CB3C3F406D5200902D5BA9CB41352" + }, + { + "id": "steamadvantage", + "version": "2.2.6.114041", + "file": "SteamAdvantage-2.2.6.114041.jar", + "path": "../../SteamAdvantage/SteamAdvantage/build/libs/SteamAdvantage-2.2.6.114041.jar", + "workspaceProduced": true, + "sha256": "444872113E13D806D4520CCBD9B3230C931353D43875BAE57EB9B5FC86614669" + }, + { + "id": "electricadvantage", + "version": "2.2.6.114041", + "file": "ElectricAdvantage-2.2.6.114041.jar", + "path": "../../ElectricAdvantage/ElectricAdvantage/build/libs/ElectricAdvantage-2.2.6.114041.jar", + "workspaceProduced": true, + "sha256": "5BE41447AF5AA3AF60C145B3A862BC42802C620C637D1E65810B350A6A9AFBA9" + }, + { + "id": "orespawn", + "version": "4.1.1.114041", + "file": "OreSpawn-4.1.1.114041.jar", + "path": "../../../../MinecraftMineralogy/OS114/OreSpawn/build/libs/OreSpawn-4.1.1.114041.jar", + "workspaceProduced": true, + "sha256": "219AE0FA08CCE400C3952AECEA9EB93B8C193D4337B2ECC09A382E6B1D525294" + }, + { + "id": "mineralogy", + "version": "6.0.1.114041", + "file": "Mineralogy-6.0.1.114041.jar", + "path": "../../../../MinecraftMineralogy/MinecraftMineralogy 114-new/MinecraftMineralogy/build/libs/Mineralogy-6.0.1.114041.jar", + "sourceRef": "5bcec041775bed9de562075d177e33c507b2e2e1", + "workspaceProduced": true, + "sha256": "75CE58BCDCB845407DE55F476FDB06211E2F8DBE6FCB0AC0F7FBDB5B7841F7BA" + } + ], + "name": "worldgen-mineralogy", + "levelName": "AdvantageWorksWorldgen", + "levelSeed": "advantage-works-worldgen-1.14.4-v1", + "levelType": "DEFAULT", + "generatorSettings": "", + "buildWorks": false, + "worldgenSampleRadius": 16, + "shutdownTimeoutSeconds": 300, + "lifecycleStations": [], + "firstProfileAssertions": [ + { + "id": "desert-oil-enabled", + "section": "fluid_deposits", + "entry": "poweradvantage:fluid_deposit/desert_crude_oil", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "ocean-oil-enabled", + "section": "fluid_deposits", + "entry": "mineralogy:fluid_deposit/crude_oil", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "mineralogy-sulfur-enabled", + "section": "ores", + "entry": "mineralogy:ore/mineralogy/sulfur_ore", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "electric-sulfur-disabled", + "section": "ores", + "entry": "electricadvantage:ore/sulfur", + "present": true, + "field": "enabled", + "expectedBoolean": false + }, + { + "id": "electric-lithium-fallback-enabled", + "section": "ores", + "entry": "electricadvantage:ore/lithium", + "present": true, + "field": "enabled", + "expectedBoolean": true + } + ], + "profileMutations": [ + { + "section": "ores", + "entry": "electricadvantage:ore/sulfur", + "field": "enabled", + "booleanValue": true + } + ], + "restartProfileAssertions": [ + { + "id": "electric-sulfur-user-enable-persists", + "section": "ores", + "entry": "electricadvantage:ore/sulfur", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "mineralogy-sulfur-remains-enabled", + "section": "ores", + "entry": "mineralogy:ore/mineralogy/sulfur_ore", + "present": true, + "field": "enabled", + "expectedBoolean": true + }, + { + "id": "ocean-oil-remains-enabled", + "section": "fluid_deposits", + "entry": "mineralogy:fluid_deposit/crude_oil", + "present": true, + "field": "enabled", + "expectedBoolean": true + } + ], + "worldgenExpectations": { + "minimumCounts": { + "poweradvantage:crude_oil": 1, + "mineralogy:crude_oil": 1, + "mineralogy:sulfur_ore": 1, + "electricadvantage:li_ore": 1 + }, + "maximumCounts": { + "electricadvantage:sulfur_ore": 0 + }, + "zeroBiomeViolations": true + }, + "minecraftVersion": "1.14.4", + "portStatus": "PORTED_UNQUALIFIED", + "qualificationStatus": "UNQUALIFIED", + "legacySourceProfile": "../../../PA113/PowerAdvantage/test-rig/profiles/worldgen-mineralogy.json" +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ActorProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ActorProbe.java index 599d86d..9456f0a 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ActorProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ActorProbe.java @@ -10,19 +10,18 @@ import java.io.File; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.util.ArrayList; import java.util.Base64; import java.util.UUID; import net.minecraft.command.Commands; import net.minecraft.entity.Entity; -import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.entity.player.ServerPlayerEntity; import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.CompoundNBT; import net.minecraft.server.MinecraftServer; import net.minecraft.server.management.PlayerInteractionManager; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.event.TickEvent; import net.minecraftforge.fml.event.server.FMLServerStartingEvent; /** Real server player-file adapter and saved-entity witnesses; not a client login. */ @@ -30,7 +29,7 @@ final class ActorProbe { private static final Gson JSON = new GsonBuilder().setPrettyPrinting().create(); private static final File MARKER = new File("advantage-migration.allow"); private final java.util.Set liveWorlds = java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>()); - private final java.util.Map> heldActors = new java.util.IdentityHashMap<>(); + private final java.util.Map heldActors = new java.util.IdentityHashMap<>(); static void register() { if (!MARKER.isFile()) return; @@ -44,17 +43,15 @@ private boolean frozen(net.minecraft.world.World world) { } private void tick(TickEvent.WorldTickEvent event) { - if (event.world.isRemote) return; + if (!(event.world instanceof ServerWorld)) return; if (event.phase == TickEvent.Phase.START && frozen(event.world)) { - java.util.List held = new ArrayList<>(); - for (Entity entity : event.world.loadedEntityList) { - if (entity.getEntityData().contains("AdvWorksActor", 8)) held.add(entity); - } - event.world.loadedEntityList.removeAll(held); + NativeEntityHold held = new NativeEntityHold(); + ((ServerWorld) event.world).getEntities().filter(entity -> entity.getPersistentData().contains("AdvWorksActor", 8)) + .forEach(held::hold); heldActors.put(event.world, held); } else if (event.phase == TickEvent.Phase.END) { - java.util.List held = heldActors.remove(event.world); - if (held != null) event.world.loadedEntityList.addAll(held); + NativeEntityHold held = heldActors.remove(event.world); + if (held != null) held.restore(); } } @@ -71,7 +68,7 @@ private void starting(FMLServerStartingEvent event) { } } - private void snapshot(MinecraftServer server, WorldServer world, String operation, String label) throws Exception { + private void snapshot(MinecraftServer server, ServerWorld world, String operation, String label) throws Exception { if (!MARKER.isFile() || !world.getWorldInfo().getWorldName().equals("AdvantageWorksMigration") || !label.matches("[a-zA-Z0-9_-]+")) throw new IllegalStateException("Unmarked world or unsafe label"); JsonObject definition = JSON.fromJson(new String(Files.readAllBytes(new File("actor-fixture.json").toPath()), @@ -80,7 +77,7 @@ private void snapshot(MinecraftServer server, WorldServer world, String operatio // sentinel is never logged in and never reads/writes the witness player. net.minecraftforge.common.util.FakePlayer sentinel = net.minecraftforge.common.util.FakePlayerFactory.getMinecraft(world); sentinel.setPosition(128.5, 80, -16.5); - if (!world.playerEntities.contains(sentinel)) world.playerEntities.add(sentinel); + if (!world.getPlayers().contains(sentinel)) world.getPlayers().add(sentinel); for (int x = 6; x <= 13; x++) { for (int z = -2; z <= 2; z++) world.getChunk(x, z); } @@ -89,7 +86,7 @@ private void snapshot(MinecraftServer server, WorldServer world, String operatio result.addProperty("frozen", frozen(world)); result.addProperty("gameTime", world.getGameTime()); try (java.io.FileInputStream input = new java.io.FileInputStream(new File("AdvantageWorksMigration/level.dat"))) { - NBTTagCompound data = CompressedStreamTools.readCompressed(input).getCompound("Data"); + CompoundNBT data = CompressedStreamTools.readCompressed(input).getCompound("Data"); if (data.contains("Player", 10)) result.add("embedded", encode(data.getCompound("Player"))); } boolean read = operation.equals("read"); @@ -97,19 +94,19 @@ private void snapshot(MinecraftServer server, WorldServer world, String operatio if (read) { GameProfile profile = new GameProfile(UUID.fromString(definition.get("playerUuid").getAsString()), definition.get("playerName").getAsString()); - EntityPlayerMP player = new EntityPlayerMP(server, world, profile, new PlayerInteractionManager(world)); + ServerPlayerEntity player = new ServerPlayerEntity(server, world, profile, new PlayerInteractionManager(world)); if (server.getPlayerList().readPlayerDataFromFile(player) == null) throw new IllegalStateException("No player file loaded"); - result.add("player", encode(player.writeWithoutTypeId(new NBTTagCompound()))); - world.getSaveHandler().getPlayerNBTManager().writePlayerData(player); + result.add("player", encode(player.writeWithoutTypeId(new CompoundNBT()))); + world.getSaveHandler().writePlayerData(player); } JsonArray entities = new JsonArray(); - for (Entity entity : new ArrayList<>(world.loadedEntityList)) { - if (!entity.getEntityData().contains("AdvWorksActor", 8)) continue; + for (Entity entity : world.getEntities().collect(java.util.stream.Collectors.toList())) { + if (!entity.getPersistentData().contains("AdvWorksActor", 8)) continue; if (operation.equals("operate")) entity.canUpdate(true); - NBTTagCompound nbt = new NBTTagCompound(); + CompoundNBT nbt = new CompoundNBT(); if (!entity.writeUnlessRemoved(nbt)) throw new IllegalStateException("Could not serialize actor"); JsonObject entry = encode(nbt); - entry.addProperty("witness", entity.getEntityData().getString("AdvWorksActor")); + entry.addProperty("witness", entity.getPersistentData().getString("AdvWorksActor")); entry.addProperty("ticksExisted", entity.ticksExisted); entities.add(entry); } @@ -118,7 +115,7 @@ private void snapshot(MinecraftServer server, WorldServer world, String operatio System.out.println("ADVACTORS COMPLETE " + label); } - private static JsonObject encode(NBTTagCompound nbt) throws Exception { + private static JsonObject encode(CompoundNBT nbt) throws Exception { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); CompressedStreamTools.writeCompressed(nbt, bytes); JsonObject result = new JsonObject(); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksCommand.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksCommand.java index 4eb9485..ae242f2 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksCommand.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksCommand.java @@ -6,7 +6,7 @@ import net.minecraft.command.CommandSource; import net.minecraft.command.Commands; -import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.StringTextComponent; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -61,7 +61,7 @@ private static int execute(CommandSource source, String operation, String target case "checkpoint": message = controller.checkpoint(source.getServer(), target, name); break; default: throw new IllegalArgumentException("Unknown Advantage Works operation " + operation); } - source.sendFeedback(new TextComponentString(message), false); + source.sendFeedback(new StringTextComponent(message), false); return 1; } catch (RuntimeException ex) { LOGGER.error("Advantage Works command failed: {} {}", operation, target, ex); @@ -72,7 +72,7 @@ private static int execute(CommandSource source, String operation, String target private static int sampleWorldgen(CommandSource source, int radius) { WorksController controller = WorksController.getInstance(); controller.requireAuthorizedWorld(source.getServer()); - source.sendFeedback(new TextComponentString(controller.sampleWorldgen(source.getServer(), radius)), false); + source.sendFeedback(new StringTextComponent(controller.sampleWorldgen(source.getServer(), radius)), false); return 1; } } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java index 8d45088..7d17d78 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java @@ -7,7 +7,7 @@ @Mod(AdvantageWorksMod.MODID) public final class AdvantageWorksMod { public static final String MODID = "advantageworkstest"; - public static final String VERSION = "1.1.0.113021"; + public static final String VERSION = "1.1.0.114041"; public AdvantageWorksMod() { MigrationProbe.register(); @@ -17,6 +17,7 @@ public AdvantageWorksMod() { LifecycleProbe.register(); RecipeUnlockProbe.register(); ProgressionProbe.register(); + NativeInteropProbe.register(); WorksController.getInstance().loadDefinition(); MinecraftForge.EVENT_BUS.addListener(this::serverStarting); } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/BayDrillAssertions.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/BayDrillAssertions.java new file mode 100644 index 0000000..0b82ebe --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/BayDrillAssertions.java @@ -0,0 +1,40 @@ +package zone.moddev.mc.advantageworks; + +import java.util.Arrays; +import java.util.function.Predicate; + +/** The walkable bays permit one track step, not an arbitrary search for a drill. */ +final class BayDrillAssertions { + private static final int[][] REAR = {{0, 1, 0}, {0, -1, 0}, {0, 0, 1}, + {0, 0, -1}, {1, 0, 0}, {-1, 0, 0}}; + + private BayDrillAssertions() { } + + static boolean applies(WorksDefinition.Station station, WorksDefinition.Assertion assertion) { + return ("S-05".equals(station.id) && "steamadvantage:steam_drill".equals(assertion.block)) + || ("E-06".equals(station.id) && "electricadvantage:electric_drill".equals(assertion.block)); + } + + static int[] rearTrack(WorksDefinition.Station station, WorksDefinition.Assertion assertion) { + int direction = assertion.meta == null + ? Arrays.asList("down", "up", "north", "south", "west", "east").indexOf(assertion.expectedText) + : assertion.meta; + if (direction < 0 || direction >= REAR.length) throw new IllegalArgumentException("Invalid drill facing"); + int[] rear = assertion.pos.clone(); + for (int i = 0; i < 3; i++) rear[i] += REAR[direction][i]; + String track = assertion.block.startsWith("steamadvantage:") + ? "steamadvantage:steam_track" : "electricadvantage:electric_track"; + if (station.placements.stream().noneMatch(p -> track.equals(p.block) && Arrays.equals(p.pos, rear))) { + throw new IllegalArgumentException("Drill bay has no declared rear track"); + } + return rear; + } + + static boolean matches(WorksDefinition.Station station, WorksDefinition.Assertion assertion, + Predicate isDrill, Predicate hasFacing) { + int[] rear = rearTrack(station, assertion); + boolean initial = isDrill.test(assertion.pos); + boolean moved = isDrill.test(rear); + return initial != moved && hasFacing.test(initial ? assertion.pos : rear); + } +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ConnectedProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ConnectedProbe.java index 3073ca1..bfe54b9 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ConnectedProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ConnectedProbe.java @@ -8,21 +8,21 @@ import java.io.File; import java.util.ArrayList; import java.util.List; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.command.Commands; -import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.CompoundNBT; import net.minecraft.state.IProperty; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.math.BlockPos; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.event.TickEvent; import net.minecraftforge.fml.event.server.FMLServerStartingEvent; import zone.moddev.mc.advantageworks.connected.ConnectedSession; /** The target adapter has no construction, inventory, energy or network-reset operation. */ final class ConnectedProbe implements ConnectedSession.Port { - private WorldServer world; + private ServerWorld world; private ConnectedSession session; private final List held = new ArrayList<>(); @@ -33,7 +33,7 @@ static void register() { MinecraftForge.EVENT_BUS.addListener(probe::starting); } - private ConnectedSession get(WorldServer current) throws Exception { + private ConnectedSession get(ServerWorld current) throws Exception { if (!current.getWorldInfo().getWorldName().equals("AdvantageWorksMigration")) throw new IllegalStateException("Unexpected connected world"); if (world != current) { world = current; @@ -45,7 +45,7 @@ private ConnectedSession get(WorldServer current) throws Exception { private void tick(TickEvent.WorldTickEvent event) { if (event.world.isRemote || event.world.dimension.getType().getId() != 0) return; try { - ConnectedSession current = get((WorldServer) event.world); + ConnectedSession current = get((ServerWorld) event.world); if (event.phase == TickEvent.Phase.START) current.start(); else current.end(); } catch (Exception ex) { throw new IllegalStateException("Connected probe failed", ex); } } @@ -61,7 +61,7 @@ private void starting(FMLServerStartingEvent event) { }))))); } - private void execute(WorldServer current, String operation, String label, int ticks) { + private void execute(ServerWorld current, String operation, String label, int ticks) { try { get(current).execute(operation, label, ticks, false); } catch (Exception ex) { throw new IllegalStateException("Connected probe failed", ex); } } @@ -70,13 +70,13 @@ private void execute(WorldServer current, String operation, String label, int ti JsonArray b = definition.getAsJsonArray("bounds"); for (int x = b.get(0).getAsInt() >> 4; x <= b.get(3).getAsInt() >> 4; x++) { for (int z = b.get(2).getAsInt() >> 4; z <= b.get(5).getAsInt() >> 4; z++) { - world.func_212414_b(x, z, true); + world.forceChunk(x, z, true); world.getChunk(x, z); } } net.minecraftforge.common.util.FakePlayer sentinel = net.minecraftforge.common.util.FakePlayerFactory.getMinecraft(world); sentinel.setPosition(0.5, 80, 0.5); - if (!world.playerEntities.contains(sentinel)) world.playerEntities.add(sentinel); + if (!world.getPlayers().contains(sentinel)) world.getPlayers().add(sentinel); } @Override public void build(JsonObject definition) { throw new UnsupportedOperationException("Target construction is forbidden"); } @@ -91,14 +91,14 @@ private void execute(WorldServer current, String operation, String label, int ti JsonObject entry = new com.google.gson.Gson().fromJson(e.toString(), JsonObject.class); entry.remove("seed"); BlockPos pos = new BlockPos(e.get("x").getAsInt(), e.get("y").getAsInt(), e.get("z").getAsInt()); - IBlockState state = world.getBlockState(pos); + BlockState state = world.getBlockState(pos); entry.addProperty("actualName", state.getBlock().getRegistryName().toString()); JsonObject properties = new JsonObject(); for (IProperty p : state.getProperties()) properties.addProperty(p.getName(), state.get(p).toString().toLowerCase(java.util.Locale.ROOT)); entry.add("properties", properties); TileEntity tile = world.getTileEntity(pos); if (tile != null) { - NBTTagCompound tag = tile.write(new NBTTagCompound()); + CompoundNBT tag = tile.write(new CompoundNBT()); java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream(); net.minecraft.nbt.CompressedStreamTools.writeCompressed(tag, bytes); entry.addProperty("nbt", tag.toString()); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/DimensionEntityProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/DimensionEntityProbe.java index 34df592..bb9081e 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/DimensionEntityProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/DimensionEntityProbe.java @@ -21,37 +21,37 @@ import java.util.Map; import java.util.Set; import java.util.UUID; -import net.minecraft.block.BlockBed; +import net.minecraft.block.BedBlock; import net.minecraft.command.Commands; import net.minecraft.entity.Entity; -import net.minecraft.entity.EntityLiving; -import net.minecraft.entity.EntityLeashKnot; -import net.minecraft.entity.EntityHanging; -import net.minecraft.entity.item.EntityItem; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.init.Blocks; +import net.minecraft.entity.MobEntity; +import net.minecraft.entity.item.LeashKnotEntity; +import net.minecraft.entity.item.HangingEntity; +import net.minecraft.entity.item.ItemEntity; +import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.block.Blocks; import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.network.EnumPacketDirection; -import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.network.PacketDirection; +import net.minecraft.network.play.ServerPlayNetHandler; import net.minecraft.network.NetworkManager; -import net.minecraft.network.Packet; -import net.minecraft.network.play.client.CPacketClientStatus; +import net.minecraft.network.IPacket; +import net.minecraft.network.play.client.CClientStatusPacket; import net.minecraft.server.MinecraftServer; import net.minecraft.server.management.PlayerInteractionManager; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.DamageSource; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; import net.minecraft.world.dimension.DimensionType; import net.minecraftforge.common.DimensionManager; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.FakePlayer; import net.minecraftforge.common.util.FakePlayerFactory; import net.minecraftforge.event.world.WorldEvent; -import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.event.TickEvent; import net.minecraftforge.fml.event.server.FMLServerStartingEvent; /** Native additional-dimension player and entity observations, without client acceptance claims. */ @@ -63,7 +63,7 @@ final class DimensionEntityProbe { private static final BlockPos SPAWN = new BlockPos(160, 100, 24); private final Set live = Collections.newSetFromMap(new IdentityHashMap()); private static final Set PINNED = Collections.newSetFromMap(new IdentityHashMap()); - private final Map> held = new IdentityHashMap<>(); + private final Map held = new IdentityHashMap<>(); static void register() { if (!MARKER.isFile()) return; @@ -86,11 +86,11 @@ private boolean frozen(World world, JsonObject definition) { return marked(world) && DEFINITION.isFile() && !nativeOnLoad(definition) && !live.contains(world); } - private static boolean witnessed(Entity entity) { return entity.getEntityData().contains(WITNESS, 8); } + private static boolean witnessed(Entity entity) { return entity.getPersistentData().contains(WITNESS, 8); } private void load(WorldEvent.Load event) { - if (!(event.getWorld() instanceof WorldServer)) return; - WorldServer world = (WorldServer) event.getWorld(); + if (!(event.getWorld() instanceof ServerWorld)) return; + ServerWorld world = (ServerWorld) event.getWorld(); if (!marked(world) || !DEFINITION.isFile()) return; try { JsonObject definition = definition(); @@ -105,24 +105,21 @@ private void tick(TickEvent.WorldTickEvent event) { JsonObject definition = definition(); int selected = dimension(definition); if (event.world.getDimension().getType() == DimensionType.OVERWORLD && nativeOnLoad(definition)) { - WorldServer selectedWorld = DimensionManager.getWorld(event.world.getServer(), DimensionType.getById(selected), true, true); + ServerWorld selectedWorld = DimensionManager.getWorld(event.world.getServer(), DimensionType.getById(selected), true, true); if (selectedWorld == null || !marked(selectedWorld)) throw new IllegalStateException("Dimension unavailable"); prepare(selectedWorld); } if (event.world.getDimension().getType().getId() != selected) return; // Pin/load before either frozen observation or normal native initialization. - prepare((WorldServer) event.world); + prepare((ServerWorld) event.world); if (!frozen(event.world, definition)) return; - List entities = witnesses((WorldServer) event.world); - entities.retainAll(event.world.loadedEntityList); - event.world.loadedEntityList.removeAll(entities); - held.put(event.world, entities); + NativeEntityHold hold = new NativeEntityHold(); + for (Entity entity : witnesses((ServerWorld) event.world)) hold.hold(entity); + held.put(event.world, hold); } catch (Exception ex) { throw new IllegalStateException("Dimension tick preparation failed", ex); } } else if (event.phase == TickEvent.Phase.END) { - List entities = held.remove(event.world); - if (entities != null) for (Entity entity : entities) { - if (!event.world.loadedEntityList.contains(entity)) event.world.loadedEntityList.add(entity); - } + NativeEntityHold hold = held.remove(event.world); + if (hold != null) hold.restore(); } } @@ -141,10 +138,10 @@ private void starting(FMLServerStartingEvent event) { private void execute(MinecraftServer server, String operation, String label) throws Exception { if (!MARKER.isFile() || !label.matches("[a-zA-Z0-9_-]+")) throw new IllegalArgumentException("Unmarked runtime or unsafe label"); JsonObject definition = definition(); - WorldServer overworld = server.getWorld(DimensionType.OVERWORLD); + ServerWorld overworld = server.getWorld(DimensionType.OVERWORLD); if (overworld == null || !marked(overworld)) throw new IllegalStateException("Unexpected overworld"); DimensionType type = DimensionType.getById(dimension(definition)); - WorldServer world = DimensionManager.getWorld(server, type, true, true); + ServerWorld world = DimensionManager.getWorld(server, type, true, true); if (world == null || !marked(world) || world.getDimension().getType().getId() != dimension(definition)) { throw new IllegalStateException("Dimension unavailable"); } @@ -174,28 +171,27 @@ private static int dimension(JsonObject definition) { return result; } - private static void prepare(WorldServer world) { + private static void prepare(ServerWorld world) { if (!PINNED.contains(world)) { DimensionManager.keepLoaded(world.getDimension().getType(), true); for (int x = 6; x <= 13; x++) for (int z = -2; z <= 2; z++) { - // Snapshot mappings retain SRG names for vanilla forceChunk/unqueueUnload. - world.func_212414_b(x, z, true); - world.getChunkProvider().func_212469_a(x, z); + world.forceChunk(x, z, true); + world.getChunk(x, z); } PINNED.add(world); } FakePlayer sentinel = FakePlayerFactory.getMinecraft(world); if (sentinel.connection == null) new OfflineConnection(world.getServer(), sentinel); sentinel.setPosition(144.5, 100, 8.5); - if (!world.playerEntities.contains(sentinel)) world.playerEntities.add(sentinel); + if (!world.getPlayers().contains(sentinel)) world.getPlayers().add(sentinel); for (int x = 6; x <= 13; x++) for (int z = -2; z <= 2; z++) world.getChunk(x, z); } - private static List witnesses(WorldServer world) { + private static List witnesses(ServerWorld world) { Set seen = Collections.newSetFromMap(new IdentityHashMap()); List result = new ArrayList<>(); - for (Entity entity : new ArrayList<>(world.loadedEntityList)) collect(entity, seen, result); - result.sort((a, b) -> a.getEntityData().getString(WITNESS).compareTo(b.getEntityData().getString(WITNESS))); + world.getEntities().forEach(entity -> collect(entity, seen, result)); + result.sort((a, b) -> a.getPersistentData().getString(WITNESS).compareTo(b.getPersistentData().getString(WITNESS))); return result; } @@ -205,8 +201,8 @@ private static void collect(Entity entity, Set seen, List result for (Entity passenger : entity.getPassengers()) collect(passenger, seen, result); } - private static EntityPlayerMP readPlayer(MinecraftServer server, WorldServer world, JsonObject definition) { - EntityPlayerMP player = new EntityPlayerMP(server, world, + private static ServerPlayerEntity readPlayer(MinecraftServer server, ServerWorld world, JsonObject definition) { + ServerPlayerEntity player = new ServerPlayerEntity(server, world, new GameProfile(UUID.fromString(definition.get("playerUuid").getAsString()), definition.get("playerName").getAsString()), new PlayerInteractionManager(world)); if (server.getPlayerList().readPlayerDataFromFile(player) == null) throw new IllegalStateException("No player file loaded"); @@ -216,7 +212,7 @@ private static EntityPlayerMP readPlayer(MinecraftServer server, WorldServer wor return player; } - private JsonObject snapshot(MinecraftServer server, WorldServer world, JsonObject definition, String operation) throws Exception { + private JsonObject snapshot(MinecraftServer server, ServerWorld world, JsonObject definition, String operation) throws Exception { JsonObject result = new JsonObject(); result.add("definition", definition); result.addProperty("operation", operation); result.addProperty("dimension", world.getDimension().getType().getId()); result.addProperty("gameTime", world.getGameTime()); result.addProperty("frozen", frozen(world, definition)); @@ -224,27 +220,27 @@ private JsonObject snapshot(MinecraftServer server, WorldServer world, JsonObjec boolean read = operation.equals("read") || operation.equals("respawn"); result.addProperty("playerAdapter", read); result.add("playerWorldDimension", JsonNull.INSTANCE); if (read) { - EntityPlayerMP player = readPlayer(server, world, definition); + ServerPlayerEntity player = readPlayer(server, world, definition); result.addProperty("playerWorldDimension", player.getServerWorld().getDimension().getType().getId()); - result.add("player", encode(player.writeWithoutTypeId(new NBTTagCompound()))); + result.add("player", encode(player.writeWithoutTypeId(new CompoundNBT()))); if (operation.equals("respawn")) result.add("respawn", respawn(server, world, definition, player)); - else world.getSaveHandler().getPlayerNBTManager().writePlayerData(player); + else world.getSaveHandler().writePlayerData(player); } JsonArray entities = new JsonArray(); for (Entity entity : witnesses(world)) { // This native writer includes type ID and works for both roots and riding passengers. - NBTTagCompound nbt = entityNbt(entity); - JsonObject entry = encode(nbt); entry.addProperty("witness", entity.getEntityData().getString(WITNESS)); + CompoundNBT nbt = entityNbt(entity); + JsonObject entry = encode(nbt); entry.addProperty("witness", entity.getPersistentData().getString(WITNESS)); entry.addProperty("uuid", entity.getUniqueID().toString()); entry.addProperty("ticksExisted", entity.ticksExisted); entry.addProperty("entityId", entity.getEntityId()); entry.addProperty("instanceId", System.identityHashCode(entity)); entry.add("vehicleUuid", entity.getRidingEntity() == null ? JsonNull.INSTANCE : JSON.toJsonTree(entity.getRidingEntity().getUniqueID().toString())); JsonArray passengers = new JsonArray(); for (Entity passenger : entity.getPassengers()) passengers.add(passenger.getUniqueID().toString()); entry.add("passengerUuids", passengers); - Entity leash = entity instanceof EntityLiving ? ((EntityLiving) entity).getLeashHolder() : null; + Entity leash = entity instanceof MobEntity ? ((MobEntity) entity).getLeashHolder() : null; entry.add("leashHolderUuid", leash == null ? JsonNull.INSTANCE : JSON.toJsonTree(leash.getUniqueID().toString())); entry.add("leashHolderType", leash == null ? JsonNull.INSTANCE : JSON.toJsonTree(leash.getType().getRegistryName().toString())); - BlockPos leashPos = leash instanceof EntityHanging ? ((EntityHanging) leash).getHangingPosition() : null; + BlockPos leashPos = leash instanceof HangingEntity ? ((HangingEntity) leash).getHangingPosition() : null; entry.add("leashHolderPosition", leashPos == null ? JsonNull.INSTANCE : JSON.toJsonTree(new int[] {leashPos.getX(), leashPos.getY(), leashPos.getZ()})); entities.add(entry); @@ -252,12 +248,12 @@ private JsonObject snapshot(MinecraftServer server, WorldServer world, JsonObjec result.add("entities", entities); BlockPos pos = new BlockPos(176, 101, 8); JsonObject generator = new JsonObject(); generator.addProperty("actualName", world.getBlockState(pos).getBlock().getRegistryName().toString()); - TileEntity tile = world.getTileEntity(pos); if (tile != null) generator.add("tile", encode(tile.write(new NBTTagCompound()))); + TileEntity tile = world.getTileEntity(pos); if (tile != null) generator.add("tile", encode(tile.write(new CompoundNBT()))); result.add("generator", generator); return result; } - private static void platform(WorldServer world) { + private static void platform(ServerWorld world) { for (int x = 156; x <= 166; x++) for (int z = 20; z <= 29; z++) { world.setBlockState(new BlockPos(x, 99, z), Blocks.BEDROCK.getDefaultState(), 2); world.setBlockState(new BlockPos(x, 106, z), Blocks.BEDROCK.getDefaultState(), 2); @@ -266,52 +262,53 @@ private static void platform(WorldServer world) { } } - private static JsonObject respawn(MinecraftServer server, WorldServer world, JsonObject definition, EntityPlayerMP player) throws Exception { + private static JsonObject respawn(MinecraftServer server, ServerWorld world, JsonObject definition, ServerPlayerEntity player) throws Exception { JsonObject options = definition.has("respawn") ? definition.getAsJsonObject("respawn") : new JsonObject(); boolean keep = !options.has("keepInventory") || options.get("keepInventory").getAsBoolean(); String bedMode = options.has("bedMode") ? options.get("bedMode").getAsString() : "forced"; if (!bedMode.matches("forced|bed|missing")) throw new IllegalArgumentException("Invalid bed mode"); - WorldServer destination = server.getWorld(DimensionType.OVERWORLD); + ServerWorld destination = server.getWorld(DimensionType.OVERWORLD); platform(destination); if (bedMode.equals("bed")) { - destination.setBlockState(SPAWN, Blocks.RED_BED.getDefaultState().with(BlockBed.HORIZONTAL_FACING, EnumFacing.EAST) - .with(BlockBed.PART, net.minecraft.state.properties.BedPart.FOOT), 2); - destination.setBlockState(SPAWN.east(), Blocks.RED_BED.getDefaultState().with(BlockBed.HORIZONTAL_FACING, EnumFacing.EAST) - .with(BlockBed.PART, net.minecraft.state.properties.BedPart.HEAD), 2); + destination.setBlockState(SPAWN, Blocks.RED_BED.getDefaultState().with(BedBlock.HORIZONTAL_FACING, Direction.EAST) + .with(BedBlock.PART, net.minecraft.state.properties.BedPart.FOOT), 2); + destination.setBlockState(SPAWN.east(), Blocks.RED_BED.getDefaultState().with(BedBlock.HORIZONTAL_FACING, Direction.EAST) + .with(BedBlock.PART, net.minecraft.state.properties.BedPart.HEAD), 2); } player.setSpawnPoint(SPAWN, bedMode.equals("forced"), DimensionType.OVERWORLD); JsonObject result = new JsonObject(); result.addProperty("scope", "server-only-dummy-network"); - result.addProperty("handler", "NetHandlerPlayServer.PERFORM_RESPAWN"); + result.addProperty("handler", "ServerPlayNetHandler.PERFORM_RESPAWN"); result.addProperty("keepInventory", keep); result.addProperty("bedMode", bedMode); - NBTTagCompound before = player.writeWithoutTypeId(new NBTTagCompound()); + CompoundNBT before = player.writeWithoutTypeId(new CompoundNBT()); result.addProperty("fromDimension", player.dimension.getId()); result.add("before", encode(before)); JsonObject diagnostics = new JsonObject(); result.add("spawnDiagnostics", diagnostics); diagnostics.add("before", spawnDiagnostics(player, before)); - Set existing = Collections.newSetFromMap(new IdentityHashMap()); existing.addAll(world.loadedEntityList); - boolean oldSource = world.getGameRules().getBoolean("keepInventory"), oldTarget = destination.getGameRules().getBoolean("keepInventory"); - EntityPlayerMP replacement = null; + Set existing = Collections.newSetFromMap(new IdentityHashMap()); world.getEntities().forEach(existing::add); + boolean oldSource = world.getGameRules().getBoolean(net.minecraft.world.GameRules.KEEP_INVENTORY), + oldTarget = destination.getGameRules().getBoolean(net.minecraft.world.GameRules.KEEP_INVENTORY); + ServerPlayerEntity replacement = null; try { - world.getGameRules().setOrCreateGameRule("keepInventory", Boolean.toString(keep), server); - destination.getGameRules().setOrCreateGameRule("keepInventory", Boolean.toString(keep), server); + world.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).set(keep, server); + destination.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).set(keep, server); OfflineConnection connection = new OfflineConnection(server, player); player.setHealth(0); player.onDeath(DamageSource.GENERIC); - NBTTagCompound afterDeath = player.writeWithoutTypeId(new NBTTagCompound()); + CompoundNBT afterDeath = player.writeWithoutTypeId(new CompoundNBT()); result.add("afterDeath", encode(afterDeath)); diagnostics.add("afterDeath", spawnDiagnostics(player, afterDeath)); if (player.queuedEndExit) throw new IllegalStateException("Expected ordinary death respawn, not queued End exit"); - connection.processClientStatus(new CPacketClientStatus(CPacketClientStatus.State.PERFORM_RESPAWN)); + connection.processClientStatus(new CClientStatusPacket(CClientStatusPacket.State.PERFORM_RESPAWN)); replacement = server.getPlayerList().getPlayerByUUID(player.getUniqueID()); if (replacement == null || replacement == player) throw new IllegalStateException("Native respawn handler did not replace player"); new OfflineConnection(server, replacement); - NBTTagCompound after = replacement.writeWithoutTypeId(new NBTTagCompound()); + CompoundNBT after = replacement.writeWithoutTypeId(new CompoundNBT()); result.add("after", encode(after)); diagnostics.add("after", spawnDiagnostics(replacement, after)); result.addProperty("toDimension", replacement.getServerWorld().getDimension().getType().getId()); BlockPos bed = replacement.getBedLocation(replacement.dimension); result.add("bedLocation", bed == null ? JsonNull.INSTANCE : JSON.toJsonTree(new int[] {bed.getX(), bed.getY(), bed.getZ()})); JsonArray drops = new JsonArray(); - for (Entity entity : world.loadedEntityList) if (entity instanceof EntityItem && !existing.contains(entity)) { - NBTTagCompound nbt = new NBTTagCompound(); if (!entity.writeUnlessRemoved(nbt)) throw new IllegalStateException("Could not serialize death drop"); + for (Entity entity : world.getEntities().collect(java.util.stream.Collectors.toList())) if (entity instanceof ItemEntity && !existing.contains(entity)) { + CompoundNBT nbt = new CompoundNBT(); if (!entity.writeUnlessRemoved(nbt)) throw new IllegalStateException("Could not serialize death drop"); drops.add(encode(nbt)); } result.add("drops", drops); @@ -320,13 +317,13 @@ private static JsonObject respawn(MinecraftServer server, WorldServer world, Jso try { if (replacement != null) server.getPlayerList().playerLoggedOut(replacement); } finally { - world.getGameRules().setOrCreateGameRule("keepInventory", Boolean.toString(oldSource), server); - destination.getGameRules().setOrCreateGameRule("keepInventory", Boolean.toString(oldTarget), server); + world.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).set(oldSource, server); + destination.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).set(oldTarget, server); } } } - private static JsonObject spawnDiagnostics(EntityPlayerMP player, NBTTagCompound nbt) { + private static JsonObject spawnDiagnostics(ServerPlayerEntity player, CompoundNBT nbt) { JsonObject result = new JsonObject(); result.addProperty("queryDimension", 0); result.addProperty("playerWorldDimension", player.getServerWorld().getDimension().getType().getId()); result.addProperty("spawnDimension", player.getSpawnDimension().getId()); @@ -344,26 +341,26 @@ private static JsonObject spawnDiagnostics(EntityPlayerMP player, NBTTagCompound return result; } - private static final class OfflineConnection extends NetHandlerPlayServer { - OfflineConnection(MinecraftServer server, EntityPlayerMP player) { super(server, new NetworkManager(EnumPacketDirection.SERVERBOUND), player); } - @Override public void sendPacket(Packet packet) { /* Deliberately no socket or client login. */ } - @Override public void sendPacket(Packet packet, GenericFutureListener> listener) { + private static final class OfflineConnection extends ServerPlayNetHandler { + OfflineConnection(MinecraftServer server, ServerPlayerEntity player) { super(server, new NetworkManager(PacketDirection.SERVERBOUND), player); } + @Override public void sendPacket(IPacket packet) { /* Deliberately no socket or client login. */ } + @Override public void sendPacket(IPacket packet, GenericFutureListener> listener) { // Death-message packets use this overload; no native network work is queued. } } - private static NBTTagCompound entityNbt(Entity entity) { - if (entity instanceof EntityLeashKnot) { - NBTTagCompound nbt = entity.writeWithoutTypeId(new NBTTagCompound()); - nbt.setString("id", entity.getType().getRegistryName().toString()); + private static CompoundNBT entityNbt(Entity entity) { + if (entity instanceof LeashKnotEntity) { + CompoundNBT nbt = entity.writeWithoutTypeId(new CompoundNBT()); + nbt.putString("id", entity.getType().getRegistryName().toString()); return nbt; } - NBTTagCompound nbt = new NBTTagCompound(); + CompoundNBT nbt = new CompoundNBT(); if (!entity.writeUnlessRemoved(nbt)) throw new IllegalStateException("Could not serialize dimension witness"); return nbt; } - private static JsonObject encode(NBTTagCompound nbt) throws Exception { + private static JsonObject encode(CompoundNBT nbt) throws Exception { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); CompressedStreamTools.writeCompressed(nbt, bytes); JsonObject result = new JsonObject(); result.addProperty("nbtBase64", Base64.getEncoder().encodeToString(bytes.toByteArray())); return result; } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/JsonNbtConverter.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/JsonNbtConverter.java index d45dfa4..31a9cd9 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/JsonNbtConverter.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/JsonNbtConverter.java @@ -4,14 +4,14 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; -import net.minecraft.nbt.INBTBase; -import net.minecraft.nbt.NBTTagByte; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagDouble; -import net.minecraft.nbt.NBTTagInt; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.nbt.NBTTagLong; -import net.minecraft.nbt.NBTTagString; +import net.minecraft.nbt.INBT; +import net.minecraft.nbt.ByteNBT; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.DoubleNBT; +import net.minecraft.nbt.IntNBT; +import net.minecraft.nbt.ListNBT; +import net.minecraft.nbt.LongNBT; +import net.minecraft.nbt.StringNBT; import java.util.Map; @@ -19,34 +19,34 @@ final class JsonNbtConverter { private JsonNbtConverter() { } - static NBTTagCompound toCompound(JsonObject object) { - NBTTagCompound result = new NBTTagCompound(); + static CompoundNBT toCompound(JsonObject object) { + CompoundNBT result = new CompoundNBT(); for (Map.Entry entry : object.entrySet()) { - result.setTag(entry.getKey(), toTag(entry.getValue())); + result.put(entry.getKey(), toTag(entry.getValue())); } return result; } - private static INBTBase toTag(JsonElement element) { + private static INBT toTag(JsonElement element) { if (element == null || element.isJsonNull()) { throw new IllegalArgumentException("NBT values may not be null"); } if (element.isJsonObject()) return toCompound(element.getAsJsonObject()); if (element.isJsonArray()) { - NBTTagList result = new NBTTagList(); + ListNBT result = new ListNBT(); JsonArray array = element.getAsJsonArray(); for (JsonElement child : array) result.add(toTag(child)); return result; } JsonPrimitive primitive = element.getAsJsonPrimitive(); - if (primitive.isBoolean()) return new NBTTagByte((byte) (primitive.getAsBoolean() ? 1 : 0)); - if (primitive.isString()) return new NBTTagString(primitive.getAsString()); + if (primitive.isBoolean()) return new ByteNBT((byte) (primitive.getAsBoolean() ? 1 : 0)); + if (primitive.isString()) return new StringNBT(primitive.getAsString()); String number = primitive.getAsString(); if (number.contains(".") || number.contains("e") || number.contains("E")) { - return new NBTTagDouble(primitive.getAsDouble()); + return new DoubleNBT(primitive.getAsDouble()); } long value = primitive.getAsLong(); return value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE - ? new NBTTagInt((int) value) : new NBTTagLong(value); + ? new IntNBT((int) value) : new LongNBT(value); } } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleProbe.java index 8aa0167..4eb9ef8 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleProbe.java @@ -17,17 +17,19 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BooleanSupplier; import net.minecraft.command.Commands; -import net.minecraft.init.Blocks; -import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.block.Blocks; +import net.minecraft.nbt.CompoundNBT; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.ChunkPos; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; import net.minecraft.world.chunk.Chunk; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.world.ChunkEvent; -import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.fml.ModList; import net.minecraftforge.fml.event.server.FMLServerStartingEvent; import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry; @@ -50,6 +52,7 @@ static final class Fixture { int[] min, max, from, to, hold; String block, energyType; String bucket; + boolean nativeTicketNetwork; int[] source; float initialEnergy; List witnesses; @@ -58,21 +61,39 @@ static final class Fixture { } static void validate(Fixture fixture) { + validate(fixture, () -> ModList.get().isLoaded("mineralogy")); + } + + static void validate(Fixture fixture, BooleanSupplier mineralogyLoaded) { if (fixture.id == null || !fixture.id.matches("[A-Za-z0-9_-]+")) throw new IllegalArgumentException("Invalid fixture ID"); if (fixture.min == null || fixture.max == null || fixture.min.length != 3 || fixture.max.length != 3) throw new IllegalArgumentException("Invalid bounds"); if (fixture.min[0] < 1024 || fixture.min[2] < 1024 || fixture.min[1] < 8 || fixture.max[1] > 240) throw new IllegalArgumentException("Fixtures must be remote and bounded above terrain"); + boolean nativeNetwork = fixture.nativeTicketNetwork; + if (nativeNetwork && (!Arrays.asList("C-STEAM", "C-ELECTRIC", "C-FLUID").contains(fixture.id) + || fixture.from != null || fixture.bucket != null || fixture.groups == null + || !(fixture.groups.containsKey("supply") && fixture.groups.containsKey("machine") + || fixture.groups.containsKey("bridge") && fixture.groups.containsKey("endpoints")))) + throw new IllegalArgumentException("Native ticket bounds require a declared network fixture"); + int width = fixture.max[0] - fixture.min[0], depth = fixture.max[2] - fixture.min[2]; + if (nativeNetwork && !((width == 159 || width == 543) && depth == 31 + || width == 31 && (depth == 159 || depth == 543))) + throw new IllegalArgumentException("Native ticket networks require a 160 or 544 by 32 block footprint"); for (int axis = 0; axis < 3; axis++) - if (fixture.max[axis] < fixture.min[axis] || fixture.max[axis] - fixture.min[axis] > 31) + if (fixture.max[axis] < fixture.min[axis] + || fixture.max[axis] - fixture.min[axis] > (nativeNetwork && axis != 1 ? 543 : 31)) throw new IllegalArgumentException("Oversized fixture"); if (fixture.witnesses == null || fixture.witnesses.isEmpty()) throw new IllegalArgumentException("Missing witnesses"); for (int[] pos : fixture.witnesses) requireInside(fixture, pos); if (fixture.bucket != null) { - if (!fixture.bucket.matches("poweradvantage:(crude|refined)_oil_bucket")) + boolean mineralogy = "mineralogy:crude_oil_bucket".equals(fixture.bucket); + if (!fixture.bucket.matches("poweradvantage:(crude|refined)_oil_bucket") && !mineralogy) throw new IllegalArgumentException("Unsupported oil bucket"); + if (mineralogy && !mineralogyLoaded.getAsBoolean()) + throw new IllegalArgumentException("Mineralogy oil bucket requires mineralogy"); requireInside(fixture, fixture.source); if (fixture.source[1] >= fixture.max[1]) throw new IllegalArgumentException("Bucket needs headroom inside fixture bounds"); @@ -165,7 +186,7 @@ private void starting(FMLServerStartingEvent event) { })))); } - private void execute(WorldServer world, String operation, String label) { + private void execute(ServerWorld world, String operation, String label) { if (operation.startsWith("release-") || operation.startsWith("load-")) { boolean acquire = operation.startsWith("load-"); String group = operation.substring(acquire ? 5 : 8); @@ -177,12 +198,10 @@ private void execute(WorldServer world, String operation, String label) { if (selected.isEmpty()) throw new IllegalArgumentException("Unknown chunk group " + group); for (long key : selected) { ChunkPos pos = new ChunkPos(key); - world.func_212414_b(pos.x, pos.z, acquire); - if (acquire) world.getChunkProvider().provideChunk(pos.x, pos.z, true, true); - else { - Chunk chunk = world.getChunkProvider().loadedChunks.get(key); - if (chunk != null) world.getChunkProvider().queueUnload(chunk); - } + world.forceChunk(pos.x, pos.z, acquire); + if (acquire) world.getChunk(pos.x, pos.z); + // Removing the forced ticket lets the native ticket manager unload naturally. + // The runner still requires actual ChunkEvent.Unload/Load witnesses. } snapshot(world, label); return; @@ -195,13 +214,13 @@ private void execute(WorldServer world, String operation, String label) { if (!world.isAirBlock(source)) throw new IllegalStateException("Oil source is not empty: " + oil.id); net.minecraft.item.Item item = net.minecraftforge.registries.ForgeRegistries.ITEMS.getValue( new net.minecraft.util.ResourceLocation(oil.bucket)); - if (!(item instanceof net.minecraft.item.ItemBucket) - || !((net.minecraft.item.ItemBucket) item).tryPlaceContainedLiquid(null, world, source, null)) + if (!(item instanceof net.minecraft.item.BucketItem) + || !((net.minecraft.item.BucketItem) item).tryPlaceContainedLiquid(null, world, source, null)) throw new IllegalStateException("Bucket placement failed: " + oil.id); String expected = oil.bucket.substring(0, oil.bucket.length() - "_bucket".length()); net.minecraft.fluid.IFluidState state = world.getFluidState(source); if (!state.isSource() || !expected.equals( - net.minecraft.util.registry.IRegistry.field_212619_h.getKey(state.getFluid()).toString())) + net.minecraft.util.registry.Registry.FLUID.getKey(state.getFluid()).toString())) throw new IllegalStateException("Bucket placed the wrong fluid: " + oil.id); } break; @@ -223,16 +242,14 @@ private void execute(WorldServer world, String operation, String label) { case "release": for (long key : chunks()) { ChunkPos pos = new ChunkPos(key); - world.func_212414_b(pos.x, pos.z, false); - Chunk chunk = world.getChunkProvider().loadedChunks.get(key); - if (chunk != null) world.getChunkProvider().queueUnload(chunk); + world.forceChunk(pos.x, pos.z, false); } break; case "load": for (long key : chunks()) { ChunkPos pos = new ChunkPos(key); - world.func_212414_b(pos.x, pos.z, true); - world.getChunkProvider().provideChunk(pos.x, pos.z, true, true); + world.forceChunk(pos.x, pos.z, true); + world.getChunk(pos.x, pos.z); } break; case "offer": @@ -240,7 +257,7 @@ private void execute(WorldServer world, String operation, String label) { Fixture fixture = fixtures.get("C-FLUID"); if (fixture == null) throw new IllegalStateException("Missing fluid fixture"); int accepted = FluidNetworkApi.offerFluid(world, position(fixture.witnesses.get(0)), - new FluidStack(net.minecraft.init.Fluids.WATER, 1000), 1000); + new FluidStack(net.minecraft.fluid.Fluids.WATER, 1000), 1000); int expected = "offer-blocked".equals(operation) ? 0 : 1000; if (accepted != expected) throw new IllegalStateException("Fluid network accepted " + accepted + " instead of " + expected); break; @@ -264,12 +281,12 @@ private void execute(WorldServer world, String operation, String label) { if (!(tile instanceof PoweredEntity) || !drill.block.equals(world.getBlockState(position(drill.to)).getBlock().getRegistryName().toString())) throw new IllegalStateException("Missing restored drill " + drill.id); - NBTTagCompound saved = tile.write(new NBTTagCompound()); + CompoundNBT saved = tile.write(new CompoundNBT()); if (!saved.contains("targetX", 99) || saved.getShort("progress") <= 0) throw new IllegalStateException("Drill must be mid-block " + drill.id); if ("legacy-read".equals(operation)) { - saved.removeTag("TargetState"); - saved.removeTag("TargetStates"); + saved.remove("TargetState"); + saved.remove("TargetStates"); } else { int[] target = {saved.getInt("targetX"), saved.getInt("targetY"), saved.getInt("targetZ")}; requireInside(drill, target); @@ -294,14 +311,14 @@ private Set chunks() { } private void unload(ChunkEvent.Unload event) { - if (event.getWorld() instanceof WorldServer) { + if (event.getWorld() instanceof ServerWorld) { increment(unloads, event.getChunk().getPos().asLong()); recordEvent(event, "unload"); } } private void load(ChunkEvent.Load event) { - if (event.getWorld() instanceof WorldServer) { + if (event.getWorld() instanceof ServerWorld) { increment(loads, event.getChunk().getPos().asLong()); recordEvent(event, "load"); } @@ -312,7 +329,7 @@ private void recordEvent(ChunkEvent event, String operation) { Map record = new LinkedHashMap<>(); record.put("operation", operation); record.put("pos", Arrays.asList(event.getChunk().getPos().x, event.getChunk().getPos().z)); - record.put("worldTime", ((WorldServer) event.getWorld()).getGameTime()); + record.put("worldTime", ((ServerWorld) event.getWorld()).getGameTime()); if (tracingLoads && "load".equals(operation)) { List stack = new ArrayList<>(); for (StackTraceElement frame : Thread.currentThread().getStackTrace()) { @@ -328,19 +345,19 @@ private void increment(Map counters, long key) { if (chunks().contains(key)) counters.put(key, counters.getOrDefault(key, 0) + 1); } - private Map witness(WorldServer world, int[] coordinates) { + private Map witness(ServerWorld world, int[] coordinates) { Map result = new LinkedHashMap<>(); result.put("pos", coordinates); BlockPos pos = position(coordinates); // No world.getChunk/getTileEntity call is permitted before this non-loading lookup. - Chunk chunk = world.getChunkProvider().loadedChunks.get(ChunkPos.asLong(pos.getX() >> 4, pos.getZ() >> 4)); + Chunk chunk = world.getChunkProvider().func_225313_a(pos.getX() >> 4, pos.getZ() >> 4); result.put("loaded", chunk != null); if (chunk == null) return result; result.put("block", chunk.getBlockState(pos).getBlock().getRegistryName().toString()); result.put("state", chunk.getBlockState(pos).toString()); TileEntity tile = chunk.getTileEntity(pos); if (tile != null) { - result.put("nbt", tile.write(new NBTTagCompound()).toString()); + result.put("nbt", tile.write(new CompoundNBT()).toString()); result.put("tileIdentity", System.identityHashCode(tile)); if (tile instanceof PoweredEntity) { PoweredEntity powered = (PoweredEntity) tile; @@ -353,8 +370,8 @@ private Map witness(WorldServer world, int[] coordinates) { } private void tick(TickEvent.WorldTickEvent event) { - if (!watching || !(event.world instanceof WorldServer)) return; - WorldServer world = (WorldServer) event.world; + if (!watching || !(event.world instanceof ServerWorld)) return; + ServerWorld world = (ServerWorld) event.world; for (Fixture fixture : fixtures.values()) { if (fixture.from == null || completed.contains(fixture.id)) continue; int step = routeSteps.getOrDefault(fixture.id, 0); @@ -385,7 +402,7 @@ private void tick(TickEvent.WorldTickEvent event) { } } - private void snapshot(WorldServer world, String label) { + private void snapshot(ServerWorld world, String label) { Map result = new LinkedHashMap<>(); Map cases = new LinkedHashMap<>(); for (Fixture fixture : fixtures.values()) { @@ -398,7 +415,7 @@ private void snapshot(WorldServer world, String label) { ChunkPos pos = new ChunkPos(key); Map entry = new LinkedHashMap<>(); entry.put("pos", Arrays.asList(pos.x, pos.z)); - entry.put("loaded", world.getChunkProvider().loadedChunks.containsKey(key)); + entry.put("loaded", world.getChunkProvider().func_225313_a(pos.x, pos.z) != null); entry.put("unloads", unloads.getOrDefault(key, 0)); entry.put("loads", loads.getOrDefault(key, 0)); chunkStates.add(entry); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/MigrationProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/MigrationProbe.java index cd17a15..e45f9b8 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/MigrationProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/MigrationProbe.java @@ -12,14 +12,14 @@ import java.util.Locale; import java.util.Map; import net.minecraft.state.IProperty; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.BlockState; import net.minecraft.command.Commands; -import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.CompoundNBT; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.event.TickEvent; import net.minecraftforge.event.world.WorldEvent; import net.minecraftforge.fml.event.server.FMLServerStartingEvent; @@ -70,7 +70,7 @@ private void snapshot(World world, String label) throws Exception { for (JsonElement element : definition) { JsonObject entry = JSON.fromJson(JSON.toJson(element), JsonObject.class); BlockPos pos = new BlockPos(entry.get("x").getAsInt(), entry.get("y").getAsInt(), entry.get("z").getAsInt()); - IBlockState state = world.getBlockState(pos); + BlockState state = world.getBlockState(pos); entry.addProperty("actualName", state.getBlock().getRegistryName().toString()); JsonObject properties = new JsonObject(); for (IProperty property : state.getProperties()) { @@ -80,7 +80,7 @@ private void snapshot(World world, String label) throws Exception { TileEntity tile = world.getTileEntity(pos); if (tile != null) { tile.markDirty(); - NBTTagCompound nbt = tile.write(new NBTTagCompound()); + CompoundNBT nbt = tile.write(new CompoundNBT()); entry.addProperty("nbt", nbt.toString()); java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream(); net.minecraft.nbt.CompressedStreamTools.writeCompressed(nbt, bytes); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeEntityHold.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeEntityHold.java new file mode 100644 index 0000000..d886104 --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeEntityHold.java @@ -0,0 +1,36 @@ +package zone.moddev.mc.advantageworks; + +import java.util.IdentityHashMap; +import java.util.Map; +import net.minecraft.entity.Entity; + +/** Holds native entity ticks without removing actors from the world's tracking indexes. */ +final class NativeEntityHold { + private final Map held = new IdentityHashMap<>(); + + void hold(Entity entity) { + if (!held.containsKey(entity)) held.put(entity, new State(entity)); + entity.canUpdate(false); + } + + void restore() { + for (Map.Entry entry : held.entrySet()) { + Entity entity = entry.getKey(); + State state = entry.getValue(); + // ServerWorld increments this even when canUpdate() is false. + entity.ticksExisted = state.ticks; + entity.canUpdate(state.updating); + } + held.clear(); + } + + private static final class State { + final boolean updating; + final int ticks; + + State(Entity entity) { + updating = entity.canUpdate(); + ticks = entity.ticksExisted; + } + } +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeInteropProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeInteropProbe.java new file mode 100644 index 0000000..32f067a --- /dev/null +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/NativeInteropProbe.java @@ -0,0 +1,255 @@ +package zone.moddev.mc.advantageworks; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.mojang.brigadier.arguments.StringArgumentType; +import cyano.poweradvantage.api.ConduitType; +import cyano.poweradvantage.api.modsupport.rf.TileEntityRFConverter; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.command.Commands; +import net.minecraft.entity.item.ItemEntity; +import net.minecraft.inventory.IInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.Direction; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.AxisAlignedBB; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.server.ServerWorld; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.energy.CapabilityEnergy; +import net.minecraftforge.energy.IEnergyStorage; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.CapabilityFluidHandler; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fml.event.server.FMLServerStartingEvent; +import net.minecraftforge.registries.ForgeRegistries; +import zone.moddev.mc.poweradvantage.machines.conveyors.BlockConveyor; +import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityConveyor; +import zone.moddev.mc.poweradvantage.machines.fluidmachines.StorageTankTileEntity; +import zone.moddev.mc.poweradvantage.util.InventoryLocks; + +/** Capability and native loot witnesses, restricted to a marked disposable world. */ +final class NativeInteropProbe { + private static final File MARKER = new File("advantage-native-interop.allow"); + private static final BlockPos TANK = new BlockPos(1024, 8, 1024); + private static final BlockPos BELT = new BlockPos(1040, 8, 1024); + + static void register() { + if (MARKER.isFile()) MinecraftForge.EVENT_BUS.addListener(new NativeInteropProbe()::starting); + } + + private void starting(FMLServerStartingEvent event) { + event.getCommandDispatcher().register(Commands.literal("advinterop") + .requires(source -> source.hasPermissionLevel(4)) + .then(Commands.argument("phase", StringArgumentType.word()).executes(context -> { + String phase = StringArgumentType.getString(context, "phase"); + try { run(context.getSource().getWorld(), phase); } + catch (Throwable error) { throw new IllegalStateException("Native interop probe failed", error); } + System.out.println("ADVINTEROP COMPLETE " + phase); + return 1; + }))); + } + + private static void require(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + + private static void check(JsonArray checks, String id, Runnable body) { + JsonObject row = new JsonObject(); + row.addProperty("id", id); + try { body.run(); row.addProperty("status", "PASS"); } + catch (AssertionError | RuntimeException error) { + row.addProperty("status", "FAIL"); + row.addProperty("failure", error.toString()); + java.io.StringWriter trace = new java.io.StringWriter(); + error.printStackTrace(new java.io.PrintWriter(trace)); + row.addProperty("traceback", trace.toString()); + } + checks.add(row); + } + + private static Block block(String id) { + ResourceLocation key = new ResourceLocation(id); + require(ForgeRegistries.BLOCKS.containsKey(key), "Missing registry entry: " + id); + return ForgeRegistries.BLOCKS.getValue(key); + } + + private static void place(ServerWorld world, BlockPos pos, BlockState state) { + require(pos.getX() >= 1023 && pos.getX() <= 1100 && pos.getY() == 8 + && pos.getZ() >= 1023 && pos.getZ() <= 1030, "Outside interop fixture bounds"); + world.setBlockState(pos, net.minecraft.block.Blocks.AIR.getDefaultState(), 3); + world.setBlockState(pos, state, 3); + } + + private static IFluidHandler fluid(TileEntity tile) { + return tile.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, Direction.UP) + .orElseThrow(() -> new AssertionError("Missing native fluid capability")); + } + + private static void energy(ServerWorld world, JsonArray checks) { + String[] ids = {"converter_rf_steam", "converter_rf_electricity", "converter_rf_quantum", "converter_tr_electricity"}; + String[] types = {"steam", "electricity", "quantum", "electricity"}; + for (int index = 0; index < ids.length; index++) { + final String id = ids[index]; + final ConduitType type = new ConduitType(types[index]); + final BlockPos pos = new BlockPos(1060 + index * 8, 8, 1024); + place(world, pos, block("poweradvantage:" + id).getDefaultState()); + TileEntityRFConverter tile = (TileEntityRFConverter) world.getTileEntity(pos); + check(checks, id + "/capability", () -> { + IEnergyStorage storage = tile.getCapability(CapabilityEnergy.ENERGY, Direction.NORTH) + .orElseThrow(() -> new AssertionError("Missing Forge Energy capability")); + require(storage == tile, "Capability does not expose the live converter"); + require(storage.receiveEnergy(-1, false) == 0 && storage.extractEnergy(-1, false) == 0, + "Negative transfers accepted"); + require(storage.receiveEnergy(123, true) == 123 && storage.getEnergyStored() == 0, + "Simulation changed converter storage"); + require(storage.receiveEnergy(123, false) == 123 && storage.getEnergyStored() == 123, + "Native receive lost energy"); + require(storage.extractEnergy(23, true) == 23 && storage.getEnergyStored() == 123, + "Extraction simulation changed storage"); + require(storage.extractEnergy(23, false) == 23 && storage.getEnergyStored() == 100, + "Native extraction lost energy"); + require(storage.receiveEnergy(Integer.MAX_VALUE, false) == storage.getMaxEnergyStored() - 100, + "Receive exceeded capacity"); + require(storage.extractEnergy(Integer.MAX_VALUE, false) == storage.getMaxEnergyStored() + && storage.getEnergyStored() == 0, "Extraction exceeded stored energy"); + }); + check(checks, id + "/forge-to-advantage", () -> { + tile.setEnergy(0, type); + tile.setOtherEnergy(100); + double before = tile.getOtherEnergy(); + tile.powerUpdate(); + double after = tile.getOtherEnergy() + tile.convertEnergyToOther(tile.getEnergy(type), type); + require(tile.getEnergy(type) > 0 && Math.abs(before - after) < 0.001, + "Conversion lost energy: before=" + before + ", after=" + after + + ", advantage=" + tile.getEnergy(type)); + }); + check(checks, id + "/advantage-to-forge", () -> { + tile.setEnergy(tile.getEnergyCapacity(type) * 0.75f, type); + tile.setOtherEnergy(0); + double before = tile.convertEnergyToOther(tile.getEnergy(type), type); + tile.powerUpdate(); + double after = tile.getOtherEnergy() + tile.convertEnergyToOther(tile.getEnergy(type), type); + require(tile.getOtherEnergy() > 0 && Math.abs(before - after) < 0.001, + "Reverse conversion lost energy: before=" + before + ", after=" + after); + }); + check(checks, id + "/invalidation", () -> { + LazyOptional old = tile.getCapability(CapabilityEnergy.ENERGY, Direction.NORTH); + require(old.isPresent(), "Missing original capability"); + world.setBlockState(pos, net.minecraft.block.Blocks.AIR.getDefaultState(), 3); + require(!old.isPresent(), "Removed converter still exposes its stale capability"); + world.setBlockState(pos, block("poweradvantage:" + id).getDefaultState(), 3); + LazyOptional replacement = world.getTileEntity(pos).getCapability(CapabilityEnergy.ENERGY, Direction.NORTH); + require(replacement.isPresent() && replacement != old, "Replacement did not expose a fresh capability"); + }); + } + } + + private static void tank(ServerWorld world, JsonArray checks) { + Block block = block("poweradvantage:fluid_storage_tank"); + place(world, TANK, block.getDefaultState()); + TileEntity tile = world.getTileEntity(TANK); + check(checks, "fluid/simulation-conservation-and-identity", () -> { + IFluidHandler tank = fluid(tile); + FluidStack offered = new FluidStack(net.minecraft.fluid.Fluids.WATER, 1000); + require(tank.fill(offered, IFluidHandler.FluidAction.SIMULATE) == 1000 + && tank.getFluidInTank(0).isEmpty(), "Simulated fill changed contents"); + require(tank.fill(offered, IFluidHandler.FluidAction.EXECUTE) == 1000 && offered.getAmount() == 1000, + "Native fill amount or input immutability failed"); + require(tank.fill(new FluidStack(net.minecraft.fluid.Fluids.LAVA, 1000), IFluidHandler.FluidAction.EXECUTE) == 0, + "Different fluids mixed"); + require(tank.drain(500, IFluidHandler.FluidAction.SIMULATE).getAmount() == 500 + && tank.getFluidInTank(0).getAmount() == 1000, "Simulated drain changed contents"); + FluidStack drained = tank.drain(500, IFluidHandler.FluidAction.EXECUTE); + require(drained.getAmount() == 500 && drained.getFluid() == net.minecraft.fluid.Fluids.WATER + && tank.getFluidInTank(0).getAmount() == 500, "Drain lost fluid or identity"); + }); + check(checks, "portable-tank/native-break-drop-and-replace", () -> { + LazyOptional old = tile.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, Direction.UP); + require(world.destroyBlock(TANK, true), "Native block destruction failed"); + require(!old.isPresent(), "Removed tank still exposes a stale capability"); + List drops = world.getEntitiesWithinAABB(ItemEntity.class, new AxisAlignedBB(TANK).grow(2)); + require(drops.size() == 1, "Expected exactly one portable tank drop; got " + drops.size()); + ItemStack item = drops.get(0).getItem().copy(); + require(item.getItem() == block.asItem() && item.getCount() == 1 && item.hasTag() + && item.getTag().getString("FluidID").equals("minecraft:water") + && item.getTag().getInt("Volume") == 500, "Native loot lost portable fluid NBT: " + item); + drops.get(0).remove(); + world.setBlockState(TANK, block.getDefaultState(), 3); + block.onBlockPlacedBy(world, TANK, block.getDefaultState(), + net.minecraftforge.common.util.FakePlayerFactory.getMinecraft(world), item); + require(fluid(world.getTileEntity(TANK)).getFluidInTank(0).getAmount() == 500, + "Replacing native drop did not restore exactly 500 mB"); + }); + } + + private static void locks(ServerWorld world, JsonArray checks) { + place(world, BELT.west(), net.minecraft.block.Blocks.CHEST.getDefaultState()); + place(world, BELT, block("poweradvantage:item_conveyor").getDefaultState().with(BlockConveyor.FACING, Direction.EAST)); + place(world, BELT.east(), net.minecraft.block.Blocks.CHEST.getDefaultState()); + IInventory input = (IInventory) world.getTileEntity(BELT.west()); + IInventory output = (IInventory) world.getTileEntity(BELT.east()); + input.setInventorySlotContents(0, new ItemStack(net.minecraft.item.Items.DIRT, 16)); + TileEntityConveyor belt = (TileEntityConveyor) world.getTileEntity(BELT); + TileEntity chest = world.getTileEntity(BELT.west()); + CompoundNBT state = chest.write(new CompoundNBT()); + state.putString("Lock", "AdvantageInteropKey"); + chest.read(state); + check(checks, "locked-chest/conveyor-refuses-extraction", () -> { + require(InventoryLocks.isLocked(input), "Native nonempty chest lock was not detected"); + for (int i = 0; i < 160; i++) belt.tick(); + require(input.getStackInSlot(0).getCount() == 16 && output.isEmpty() && belt.isEmpty(), + "Conveyor bypassed locked source"); + }); + state.remove("Lock"); + chest.read(state); + check(checks, "unlocked-chest/conveyor-transfers-exactly", () -> { + require(!InventoryLocks.isLocked(input), "Cleared lock still blocks extraction"); + for (int i = 0; i < 160; i++) belt.tick(); + require(input.isEmpty() && belt.isEmpty() && output.getStackInSlot(0).getCount() == 16 + && output.getStackInSlot(0).getItem() == net.minecraft.item.Items.DIRT, + "Unlocked native chest did not deliver exactly sixteen dirt"); + }); + } + + private static void run(ServerWorld world, String phase) throws Exception { + require(MARKER.isFile() && world.getWorldInfo().getWorldName().equals("AdvantageWorksMigration"), "Unmarked world"); + require(phase.equals("exercise") || phase.equals("restored"), "Unknown interop phase"); + JsonArray checks = new JsonArray(); + if (phase.equals("exercise")) { + energy(world, checks); + tank(world, checks); + locks(world, checks); + } else { + check(checks, "portable-tank/full-process-restart", () -> { + FluidStack stack = fluid(world.getTileEntity(TANK)).getFluidInTank(0); + require(stack.getFluid() == net.minecraft.fluid.Fluids.WATER && stack.getAmount() == 500, + "Restart lost portable tank contents"); + }); + check(checks, "conveyor/full-process-restart", () -> { + IInventory output = (IInventory) world.getTileEntity(BELT.east()); + require(output.getStackInSlot(0).getItem() == net.minecraft.item.Items.DIRT + && output.getStackInSlot(0).getCount() == 16, "Restart changed exact conveyor output"); + }); + } + JsonObject report = new JsonObject(); + report.addProperty("phase", phase); + report.addProperty("worldTime", world.getGameTime()); + report.addProperty("status", java.util.stream.StreamSupport.stream(checks.spliterator(), false) + .allMatch(row -> row.getAsJsonObject().get("status").getAsString().equals("PASS")) ? "PASS" : "FAIL"); + report.add("checks", checks); + File destination = new File("advantage-works-results", "native-interop-" + phase + ".json"); + require(!destination.exists(), "Refusing to overwrite interop evidence"); + destination.getParentFile().mkdirs(); + Files.write(destination.toPath(), new GsonBuilder().setPrettyPrinting().create().toJson(report).getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProcessProgression.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProcessProgression.java index 9dd8a70..4c30fa9 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProcessProgression.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProcessProgression.java @@ -10,22 +10,21 @@ import java.util.Arrays; import java.util.Collections; import java.util.Map; -import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.entity.player.ServerPlayerEntity; import net.minecraft.inventory.IInventory; import net.minecraft.item.ItemStack; -import net.minecraft.item.ItemBlock; +import net.minecraft.item.BlockItem; import net.minecraft.item.ItemUseContext; -import net.minecraft.entity.item.EntityItem; -import net.minecraft.util.EnumFacing; -import net.minecraft.util.EnumActionResult; +import net.minecraft.entity.item.ItemEntity; +import net.minecraft.util.Direction; +import net.minecraft.util.ActionResultType; import net.minecraft.util.math.AxisAlignedBB; import net.minecraft.tileentity.TileEntity; -import net.minecraft.tileentity.TileEntityFurnace; -import net.minecraft.util.EnumHand; -import net.minecraft.util.NonNullList; +import net.minecraft.tileentity.FurnaceTileEntity; +import net.minecraft.util.Hand; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.BlockPos; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; import net.minecraftforge.registries.ForgeRegistries; import zone.moddev.mc.poweradvantage.init.Fluids; import zone.moddev.mc.poweradvantage.machines.fluidmachines.StillTileEntity; @@ -39,8 +38,8 @@ final class ProcessProgression { static void reset() { evidence = new JsonArray(); crusherWitnesses.clear(); } static JsonArray witnesses() { return evidence; } - static void smelt(WorldServer world, JsonObject fixture, String recipe, ItemStack input, ItemStack expected) { - TileEntityFurnace furnace = (TileEntityFurnace) fresh(world, fixture, + static void smelt(ServerWorld world, JsonObject fixture, String recipe, ItemStack input, ItemStack expected) { + FurnaceTileEntity furnace = (FurnaceTileEntity) fresh(world, fixture, new BlockPos(16394, 80, 16384), "minecraft:furnace"); furnace.setInventorySlotContents(0, input.copy()); furnace.setInventorySlotContents(1, stack("minecraft:coal")); @@ -50,32 +49,34 @@ static void smelt(WorldServer world, JsonObject fixture, String recipe, ItemStac && !output.isEmpty() && id(output).equals(id(expected)) && output.getCount() == expected.getCount(), "Live furnace failed " + recipe + ": input=" + furnace.getStackInSlot(0) + ", fuel=" + furnace.getStackInSlot(1) + ", output=" + output - + ", expected=" + expected + ", cook=" + furnace.getField(2) + "/" + furnace.getField(3)); + + ", expected=" + expected + ", saved=" + furnace.write(new net.minecraft.nbt.CompoundNBT())); record("live_furnace:" + recipe, input.getCount(), output.getCount()); } - static void hammer(WorldServer world, EntityPlayerMP player, Map known, + static void hammer(ServerWorld world, ServerPlayerEntity player, Map known, JsonObject fixture, String recipe, ItemStack input, ItemStack expected) { BlockPos pos = new BlockPos(16396, 80, 16384); fresh(world, fixture, pos, "minecraft:stone"); AxisAlignedBB area = new AxisAlignedBB(pos.up()); - for (EntityItem entity : world.getEntitiesWithinAABB(EntityItem.class, area)) { + for (ItemEntity entity : world.getEntitiesWithinAABB(ItemEntity.class, area)) { entity.setItem(ItemStack.EMPTY); entity.remove(); } String hammer = known.keySet().stream().filter(key -> key.startsWith("basemetals:") && key.endsWith("_crackhammer")).findFirst().orElseThrow(() -> new IllegalStateException("No craftable hammer")); player.inventory.clear(); - player.setHeldItem(EnumHand.MAIN_HAND, stack(hammer)); + player.setHeldItem(Hand.MAIN_HAND, stack(hammer)); player.setSneaking(false); - EntityItem source = new EntityItem(world, pos.getX() + 0.5D, pos.getY() + 1.1D, pos.getZ() + 0.5D, input.copy()); - world.spawnEntity(source); - EnumActionResult action = player.getHeldItemMainhand().getItem().onItemUse( - new ItemUseContext(player, player.getHeldItemMainhand(), pos, EnumFacing.UP, 0.5F, 1F, 0.5F)); - require(action == EnumActionResult.SUCCESS && source.getItem().isEmpty() + ItemEntity source = new ItemEntity(world, pos.getX() + 0.5D, pos.getY() + 1.1D, pos.getZ() + 0.5D, input.copy()); + world.addEntity(source); + ActionResultType action = player.getHeldItemMainhand().getItem().onItemUse( + new ItemUseContext(player, Hand.MAIN_HAND, new net.minecraft.util.math.BlockRayTraceResult( + new net.minecraft.util.math.Vec3d(pos.getX() + 0.5D, pos.getY() + 1D, pos.getZ() + 0.5D), + Direction.UP, pos, false))); + require(action == ActionResultType.SUCCESS && source.getItem().isEmpty() && player.getHeldItemMainhand().getDamage() == 1, "Live hammer failed " + recipe); int produced = 0; - for (EntityItem entity : world.getEntitiesWithinAABB(EntityItem.class, area)) { + for (ItemEntity entity : world.getEntitiesWithinAABB(ItemEntity.class, area)) { if (entity == source || entity.getItem().isEmpty()) continue; require(id(entity.getItem()).equals(id(expected)), "Unexpected hammer output for " + recipe + ": " + entity.getItem() + ", expected " + expected); @@ -88,10 +89,10 @@ static void hammer(WorldServer world, EntityPlayerMP player, Map known, ItemStack input) { - if (input.getItem() instanceof ItemBlock) { + if (input.getItem() instanceof BlockItem) { for (String id : known.keySet()) { if (id.startsWith("basemetals:") && id.endsWith("_crackhammer") - && stack(id).getItem().canHarvestBlock(((ItemBlock) input.getItem()).getBlock().getDefaultState())) return true; + && stack(id).getItem().canHarvestBlock(((BlockItem) input.getItem()).getBlock().getDefaultState())) return true; } return (has(known, "steamadvantage:steam_crusher", "steamadvantage:steam_boiler_coal", "minecraft:water_bucket")) || has(known, "electricadvantage:electric_crusher", "electricadvantage:hydroelectric_generator", "minecraft:water_bucket"); @@ -99,7 +100,7 @@ static boolean canCrush(Map known, ItemStack input) { return true; } - static boolean advance(WorldServer world, EntityPlayerMP player, Map known, JsonObject fixture) { + static boolean advance(ServerWorld world, ServerPlayerEntity player, Map known, JsonObject fixture) { boolean changed = false; for (String machine : new String[] {"steamadvantage:steam_crusher", "electricadvantage:electric_crusher"}) { if (known.containsKey(machine) && crusherWitnesses.add(machine)) { @@ -110,8 +111,8 @@ static boolean advance(WorldServer world, EntityPlayerMP player, Map drops = NonNullList.create(); - world.getBlockState(pos).getBlock().getDrops(world.getBlockState(pos), drops, world, pos, 0); + java.util.List drops = net.minecraft.block.Block.getDrops( + world.getBlockState(pos), world, pos, world.getTileEntity(pos)); require(drops.size() == 1 && id(drops.get(0)).equals("electricadvantage:sulfur_powder") && drops.get(0).getCount() >= 1 && drops.get(0).getCount() <= 4, "Sulfur ore drops changed"); changed |= acquire(known, player, drops.get(0), "mine_generated_sulfur_ore", Collections.singletonList("electricadvantage:sulfur_ore")); @@ -124,8 +125,8 @@ static boolean advance(WorldServer world, EntityPlayerMP player, Map known, String... ids) { return true; } - private static TileEntity fresh(WorldServer world, JsonObject fixture, BlockPos pos, String block) { + private static TileEntity fresh(ServerWorld world, JsonObject fixture, BlockPos pos, String block) { JsonObject bounds = fixture.getAsJsonObject("machineBounds"); int[] p = {pos.getX(), pos.getY(), pos.getZ()}; for (int i = 0; i < 3; i++) require(p[i] >= bounds.getAsJsonArray("min").get(i).getAsInt() && p[i] <= bounds.getAsJsonArray("max").get(i).getAsInt(), "Machine fixture escaped its bounds"); - world.setBlockState(pos, net.minecraft.init.Blocks.AIR.getDefaultState()); + world.setBlockState(pos, net.minecraft.block.Blocks.AIR.getDefaultState()); world.removeTileEntity(pos); net.minecraft.block.Block material = ForgeRegistries.BLOCKS.getValue(new ResourceLocation(block)); require(material != null, "Missing machine/fluid " + block); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProgressionProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProgressionProbe.java index c723ce4..1cae391 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProgressionProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/ProgressionProbe.java @@ -23,28 +23,27 @@ import net.minecraft.advancements.Advancement; import net.minecraft.advancements.CriteriaTriggers; import net.minecraft.command.Commands; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.inventory.Container; -import net.minecraft.inventory.InventoryBasic; -import net.minecraft.inventory.InventoryCrafting; +import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.inventory.container.Container; +import net.minecraft.inventory.Inventory; +import net.minecraft.inventory.CraftingInventory; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.item.crafting.IRecipe; import net.minecraft.item.crafting.Ingredient; -import net.minecraft.network.EnumPacketDirection; -import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.PacketDirection; +import net.minecraft.network.play.ServerPlayNetHandler; import net.minecraft.network.NetworkManager; -import net.minecraft.network.Packet; +import net.minecraft.network.IPacket; import net.minecraft.server.MinecraftServer; import net.minecraft.server.management.PlayerInteractionManager; import net.minecraft.tags.ItemTags; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.BlockPos; -import net.minecraft.util.text.TextComponentString; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; import net.minecraft.world.storage.loot.LootContext; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.common.crafting.VanillaRecipeTypes; +import net.minecraft.item.crafting.IRecipeType; import net.minecraftforge.fml.event.server.FMLServerStartingEvent; import net.minecraftforge.registries.ForgeRegistries; import zone.moddev.mc.poweradvantage.PowerAdvantage; @@ -74,22 +73,22 @@ private void starting(FMLServerStartingEvent event) { }))); } - private void audit(MinecraftServer server, WorldServer world, String label) throws Exception { + private void audit(MinecraftServer server, ServerWorld world, String label) throws Exception { require(MARKER.isFile() && world.getWorldInfo().getWorldName().equals("AdvantageWorksMigration") && label.matches("[A-Za-z0-9_-]+"), "Unmarked world or unsafe label"); JsonObject fixture = JSON.fromJson(new String(Files.readAllBytes(new File("progression-fixture.json").toPath()), StandardCharsets.UTF_8), JsonObject.class); - EntityPlayerMP player = new EntityPlayerMP(server, world, new GameProfile( + ServerPlayerEntity player = new ServerPlayerEntity(server, world, new GameProfile( UUID.fromString("3ea512a5-4c41-42ac-901f-7fabd2b1cbde"), "ProgressionProbe"), new PlayerInteractionManager(world)); - player.connection = new NetHandlerPlayServer(server, new NetworkManager(EnumPacketDirection.SERVERBOUND), player) { - @Override public void sendPacket(Packet packet) {} + player.connection = new ServerPlayNetHandler(server, new NetworkManager(PacketDirection.SERVERBOUND), player) { + @Override public void sendPacket(IPacket packet) {} }; for (Advancement advancement : server.getAdvancementManager().getAllAdvancements()) { if (owned(advancement.getId())) for (String criterion : advancement.getCriteria().keySet()) player.getAdvancements().revokeCriterion(advancement, criterion); } - List oldBook = new ArrayList<>(); - for (IRecipe recipe : server.getRecipeManager().getRecipes()) if (owned(recipe.getId())) oldBook.add(recipe); + List> oldBook = new ArrayList<>(); + for (IRecipe recipe : server.getRecipeManager().getRecipes()) if (owned(recipe.getId())) oldBook.add(recipe); player.resetRecipes(oldBook); ProcessProgression.reset(); Map known = new LinkedHashMap<>(); @@ -100,35 +99,34 @@ private void audit(MinecraftServer server, WorldServer world, String label) thro if (item != null) acquire(known, player, new ItemStack(item), "natural_resource", Collections.emptyList()); } JsonArray loot = new JsonArray(); - LootContext lootContext = new LootContext.Builder(world).withPosition(new BlockPos(0, 80, 0)).build(); for (int seed = 0; seed < 8; seed++) { List overridden = server.getLootTableManager().getLootTableFromLocation( - new ResourceLocation("minecraft:chests/simple_dungeon")).generateLootForPools(new Random(seed), lootContext); + new ResourceLocation("minecraft:chests/simple_dungeon")).generate(chestContext(world, seed)); require(overridden.size() == 1 && id(overridden.get(0)).equals("minecraft:dirt") && overridden.get(0).getCount() == 1, "Player loot data-pack override was changed"); } if (PowerAdvantage.recipeMode == RecipeMode.APOCALYPTIC) { for (int i = 0; i < 4096; i++) { for (ItemStack stack : server.getLootTableManager().getLootTableFromLocation( - new ResourceLocation("minecraft:chests/abandoned_mineshaft")).generateLootForPools(new Random(i), lootContext)) { + new ResourceLocation("minecraft:chests/abandoned_mineshaft")).generate(chestContext(world, i))) { if (advantage(stack)) { if (acquire(known, player, stack, "vanilla_chest_loot", Collections.emptyList())) loot.add(id(stack)); } } } } - List recipes = new ArrayList<>(server.getRecipeManager().getRecipes()); + List> recipes = new ArrayList<>(server.getRecipeManager().getRecipes()); recipes.sort(Comparator.comparing(recipe -> recipe.getId().toString())); boolean changed = true; int rounds = 0; while (changed) { require(++rounds <= 100, "Progression closure did not converge"); changed = false; - for (IRecipe recipe : recipes) { + for (IRecipe recipe : recipes) { if (executed.contains(recipe.getId().toString()) || recipe.getIngredients().isEmpty()) continue; - String type = recipe.getType().getId().toString(); - boolean crafting = recipe.getType() == VanillaRecipeTypes.CRAFTING; - boolean smelting = recipe.getType() == VanillaRecipeTypes.SMELTING; + String type = net.minecraft.util.registry.Registry.RECIPE_TYPE.getKey(recipe.getType()).toString(); + boolean crafting = recipe.getType() == IRecipeType.CRAFTING; + boolean smelting = recipe.getType() == IRecipeType.SMELTING; boolean crushing = type.equals("basemetals:crushing"); if (!crafting && !smelting && !crushing) continue; if (smelting && (!known.containsKey("minecraft:furnace") || !known.containsKey("minecraft:coal"))) continue; @@ -148,19 +146,19 @@ private void audit(MinecraftServer server, WorldServer world, String label) thro if (crushing && !ProcessProgression.canCrush(known, ingredients.get(0))) continue; ItemStack result = ItemStack.EMPTY; if (crafting) { - Container container = new Container() { - @Override public boolean canInteractWith(net.minecraft.entity.player.EntityPlayer p) { return true; } + Container container = new Container(null, 0) { + @Override public boolean canInteractWith(net.minecraft.entity.player.PlayerEntity p) { return true; } }; for (int width = 1; width <= 3 && result.isEmpty(); width++) { if (ingredients.size() > width * 3) continue; - InventoryCrafting grid = new InventoryCrafting(container, 3, 3); + CraftingInventory grid = new CraftingInventory(container, 3, 3); for (int i = 0; i < ingredients.size(); i++) grid.setInventorySlotContents((i / width) * 3 + i % width, ingredients.get(i)); - if (recipe.matches(grid, world)) result = recipe.getCraftingResult(grid); + result = matchedResult(recipe, grid, world); } } else { - InventoryBasic grid = new InventoryBasic(new TextComponentString("manufacturing"), 1); + Inventory grid = new Inventory(1); grid.setInventorySlotContents(0, ingredients.get(0)); - if (recipe.matches(grid, world)) result = recipe.getCraftingResult(grid); + result = matchedResult(recipe, grid, world); } if (result.isEmpty()) continue; if (owned(recipe.getId()) && !crushing) { @@ -171,7 +169,7 @@ private void audit(MinecraftServer server, WorldServer world, String label) thro } if (owned(recipe.getId()) && smelting) ProcessProgression.smelt(world, fixture, recipe.getId().toString(), ingredients.get(0), result); - if (owned(recipe.getId()) && crushing && !(ingredients.get(0).getItem() instanceof net.minecraft.item.ItemBlock)) + if (owned(recipe.getId()) && crushing && !(ingredients.get(0).getItem() instanceof net.minecraft.item.BlockItem)) ProcessProgression.hammer(world, player, known, fixture, recipe.getId().toString(), ingredients.get(0), result); executed.add(recipe.getId().toString()); List inputs = new ArrayList<>(); @@ -203,8 +201,8 @@ private void audit(MinecraftServer server, WorldServer world, String label) thro JsonObject report = new JsonObject(); JsonArray missingRecipes = new JsonArray(); for (IRecipe recipe : recipes) { - if (!owned(recipe.getId()) || (recipe.getType() != VanillaRecipeTypes.CRAFTING - && recipe.getType() != VanillaRecipeTypes.SMELTING)) continue; + if (!owned(recipe.getId()) || (recipe.getType() != IRecipeType.CRAFTING + && recipe.getType() != IRecipeType.SMELTING)) continue; if (!executed.contains(recipe.getId().toString())) missingRecipes.add(recipe.getId().toString()); } if (missingRecipes.size() > 0) blocked += missingRecipes.size(); @@ -232,7 +230,7 @@ private void audit(MinecraftServer server, WorldServer world, String label) thro Files.write(new File("progression-" + label + ".json").toPath(), JSON.toJson(report).getBytes(StandardCharsets.UTF_8)); } - static boolean acquire(Map known, EntityPlayerMP player, ItemStack output, String route, List inputs) { + static boolean acquire(Map known, ServerPlayerEntity player, ItemStack output, String route, List inputs) { if (output == null || output.isEmpty() || known.containsKey(id(output))) return false; JsonObject node = new JsonObject(); node.addProperty("via", route); @@ -250,6 +248,18 @@ static ItemStack stack(String id) { return item == null ? ItemStack.EMPTY : new ItemStack(item); } + private static LootContext chestContext(ServerWorld world, int seed) { + return new LootContext.Builder(world).withRandom(new Random(seed)) + .withParameter(net.minecraft.world.storage.loot.LootParameters.POSITION, new BlockPos(0, 80, 0)) + .build(net.minecraft.world.storage.loot.LootParameterSets.CHEST); + } + + @SuppressWarnings("unchecked") + private static ItemStack matchedResult(IRecipe recipe, net.minecraft.inventory.IInventory inventory, ServerWorld world) { + IRecipe typed = (IRecipe) recipe; + return typed.matches(inventory, world) ? typed.getCraftingResult(inventory) : ItemStack.EMPTY; + } + private static boolean advantage(ItemStack stack) { return !stack.isEmpty() && owned(stack.getItem().getRegistryName()); } private static boolean owned(ResourceLocation id) { return id != null && (id.getNamespace().equals("poweradvantage") || id.getNamespace().equals("steamadvantage") diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeBookProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeBookProbe.java index d01bd9d..3a16a0f 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeBookProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeBookProbe.java @@ -7,33 +7,33 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; -import net.minecraft.inventory.Container; -import net.minecraft.inventory.InventoryCrafting; +import net.minecraft.inventory.container.Container; +import net.minecraft.inventory.CraftingInventory; import net.minecraft.item.ItemStack; import net.minecraft.item.crafting.IRecipe; import net.minecraft.network.PacketBuffer; -import net.minecraft.network.play.server.SPacketUpdateRecipes; +import net.minecraft.network.play.server.SUpdateRecipesPacket; import net.minecraft.server.MinecraftServer; import net.minecraft.util.ResourceLocation; -import net.minecraft.world.WorldServer; -import net.minecraftforge.common.crafting.VanillaRecipeTypes; +import net.minecraft.world.server.ServerWorld; +import net.minecraft.item.crafting.IRecipeType; import static zone.moddev.mc.advantageworks.ProgressionProbe.*; /** Native crafting and server-to-client recipe payload, not a source-JSON simulation. */ final class RecipeBookProbe { - static void verify(MinecraftServer server, WorldServer world, String label, JsonObject report) throws Exception { + static void verify(MinecraftServer server, ServerWorld world, String label, JsonObject report) throws Exception { JsonArray entries = new JsonArray(); - List recipes = new ArrayList<>(); - for (IRecipe recipe : server.getRecipeManager().getRecipes()) { + List> recipes = new ArrayList<>(); + for (IRecipe recipe : server.getRecipeManager().getRecipes()) { String namespace = recipe.getId().getNamespace(); if (!(namespace.equals("poweradvantage") || namespace.equals("steamadvantage") || namespace.equals("electricadvantage"))) continue; - if (recipe.getType() != VanillaRecipeTypes.CRAFTING && recipe.getType() != VanillaRecipeTypes.SMELTING) continue; + if (recipe.getType() != IRecipeType.CRAFTING && recipe.getType() != IRecipeType.SMELTING) continue; String output = id(recipe.getRecipeOutput()); require(output.equals(recipe.getGroup()), "Mixed-output recipe group: " + recipe.getId() + " -> " + recipe.getGroup()); JsonObject entry = new JsonObject(); entry.addProperty("id", recipe.getId().toString()); entry.addProperty("group", recipe.getGroup()); - entry.addProperty("type", recipe.getType().getId().toString()); + entry.addProperty("type", net.minecraft.util.registry.Registry.RECIPE_TYPE.getKey(recipe.getType()).toString()); entry.addProperty("output", output); entry.addProperty("count", recipe.getRecipeOutput().getCount()); entries.add(entry); @@ -42,22 +42,22 @@ static void verify(MinecraftServer server, WorldServer world, String label, Json report.add("bookRecipes", entries); PacketBuffer buffer = new PacketBuffer(Unpooled.buffer()); try { - new SPacketUpdateRecipes(recipes).writePacketData(buffer); + new SUpdateRecipesPacket(recipes).writePacketData(buffer); byte[] bytes = new byte[buffer.readableBytes()]; buffer.readBytes(bytes); Files.write(Paths.get("recipes-" + label + ".bin"), bytes); } finally { buffer.release(); } - IRecipe starch = server.getRecipeManager().getRecipe(new ResourceLocation("poweradvantage:starch_from_potato")); - require(starch != null && starch.getType() == VanillaRecipeTypes.CRAFTING, "Missing native potato crafting recipe"); + IRecipe starch = server.getRecipeManager().getRecipe(new ResourceLocation("poweradvantage:starch_from_potato")).orElse(null); + require(starch != null && starch.getType() == IRecipeType.CRAFTING, "Missing native potato crafting recipe"); int cases = 0; for (int width : new int[] {2, 3}) for (String potato : new String[] {"minecraft:potato", "minecraft:poisonous_potato"}) { for (int slot = 0; slot < width * width; slot++) for (int count : new int[] {1, 64}) { - InventoryCrafting grid = grid(width); + CraftingInventory grid = grid(width); ItemStack input = stack(potato); input.setCount(count); grid.setInventorySlotContents(slot, input); require(starch.matches(grid, world), "Potato recipe rejected slot/stack " + slot + "/" + count); - IRecipe selected = world.getRecipeManager().getRecipe(grid, world, VanillaRecipeTypes.CRAFTING); + IRecipe selected = world.getRecipeManager().getRecipe(IRecipeType.CRAFTING, grid, world).orElse(null); require(selected != null && selected.getId().equals(starch.getId()), "Conflicting native potato crafting recipe"); ItemStack result = starch.getCraftingResult(grid); require(id(result).equals("poweradvantage:starch") && result.getCount() == 1, "Starch output changed"); @@ -69,13 +69,13 @@ static void verify(MinecraftServer server, WorldServer world, String label, Json } } for (String item : new String[] {null, "minecraft:baked_potato", "minecraft:carrot", "minecraft:dirt"}) { - InventoryCrafting grid = grid(3); + CraftingInventory grid = grid(3); if (item != null) grid.setInventorySlotContents(0, stack(item)); require(!starch.matches(grid, world), "Invalid starch input accepted: " + item); cases++; } for (String second : new String[] {"minecraft:potato", "minecraft:poisonous_potato", "minecraft:dirt"}) { - InventoryCrafting grid = grid(3); + CraftingInventory grid = grid(3); grid.setInventorySlotContents(0, stack("minecraft:potato")); grid.setInventorySlotContents(1, stack(second)); require(!starch.matches(grid, world), "Extra occupied ingredient slot accepted"); @@ -84,9 +84,9 @@ static void verify(MinecraftServer server, WorldServer world, String label, Json report.addProperty("potatoCraftingCases", cases); } - private static InventoryCrafting grid(int width) { - return new InventoryCrafting(new Container() { - @Override public boolean canInteractWith(net.minecraft.entity.player.EntityPlayer player) { return true; } + private static CraftingInventory grid(int width) { + return new CraftingInventory(new Container(null, 0) { + @Override public boolean canInteractWith(net.minecraft.entity.player.PlayerEntity player) { return true; } }, width, width); } } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeUnlockProbe.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeUnlockProbe.java index cab7eda..bec81f4 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeUnlockProbe.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeUnlockProbe.java @@ -20,21 +20,21 @@ import net.minecraft.advancements.CriteriaTriggers; import net.minecraft.advancements.criterion.InventoryChangeTrigger; import net.minecraft.command.Commands; -import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.entity.player.ServerPlayerEntity; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.item.crafting.IRecipe; -import net.minecraft.network.EnumPacketDirection; -import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.PacketDirection; +import net.minecraft.network.play.ServerPlayNetHandler; import net.minecraft.network.NetworkManager; -import net.minecraft.network.Packet; +import net.minecraft.network.IPacket; import net.minecraft.server.MinecraftServer; import net.minecraft.server.management.PlayerInteractionManager; import net.minecraft.tags.ItemTags; import net.minecraft.util.ResourceLocation; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.fml.common.gameevent.PlayerEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; import net.minecraftforge.fml.event.server.FMLServerStartingEvent; import net.minecraftforge.registries.ForgeRegistries; @@ -42,7 +42,7 @@ final class RecipeUnlockProbe { private static final Gson JSON = new GsonBuilder().setPrettyPrinting().create(); private static final File MARKER = new File("advantage-recipes.allow"); - private EntityPlayerMP player; + private ServerPlayerEntity player; static void register() { if (MARKER.isFile()) MinecraftForge.EVENT_BUS.addListener(new RecipeUnlockProbe()::starting); @@ -67,18 +67,18 @@ private void starting(FMLServerStartingEvent event) { })))); } - private void run(MinecraftServer server, WorldServer world, String operation, String label) throws Exception { + private void run(MinecraftServer server, ServerWorld world, String operation, String label) throws Exception { if (!MARKER.isFile() || !world.getWorldInfo().getWorldName().equals("AdvantageWorksMigration") || !label.matches("[a-zA-Z0-9_-]+")) throw new IllegalStateException("Unmarked world or unsafe label"); JsonObject fixture = JSON.fromJson(new String(Files.readAllBytes(new File("recipe-fixture.json").toPath()), StandardCharsets.UTF_8), JsonObject.class); if (player == null) { - player = new EntityPlayerMP(server, world, new GameProfile( + player = new ServerPlayerEntity(server, world, new GameProfile( UUID.fromString("b9bcb1ba-e8e3-44ec-a729-ef70587dc453"), "RecipeProbe"), new PlayerInteractionManager(world)); // Real server player and recipe book, with a sink instead of a remote client. - player.connection = new NetHandlerPlayServer(server, new NetworkManager(EnumPacketDirection.SERVERBOUND), player) { - @Override public void sendPacket(Packet packet) {} + player.connection = new ServerPlayNetHandler(server, new NetworkManager(PacketDirection.SERVERBOUND), player) { + @Override public void sendPacket(IPacket packet) {} }; } JsonArray cases = fixture.getAsJsonArray("recipes"); @@ -96,7 +96,7 @@ private void run(MinecraftServer server, WorldServer world, String operation, St JsonObject definition = element.getAsJsonObject(); String id = definition.get("id").getAsString(); Advancement advancement = advancement(server, id); - IRecipe recipe = server.getRecipeManager().getRecipe(new ResourceLocation(id)); + IRecipe recipe = server.getRecipeManager().getRecipe(new ResourceLocation(id)).orElse(null); JsonObject row = new JsonObject(); row.addProperty("id", id); row.addProperty("recipeActive", recipe != null); @@ -141,7 +141,7 @@ private void run(MinecraftServer server, WorldServer world, String operation, St } else if (operation.equals("persist")) { for (JsonElement element : cases) { String id = element.getAsJsonObject().get("id").getAsString(); - reset(advancement(server, id), server.getRecipeManager().getRecipe(new ResourceLocation(id))); + reset(advancement(server, id), server.getRecipeManager().getRecipe(new ResourceLocation(id)).orElse(null)); } JsonArray kit = new JsonArray(); for (JsonElement id : fixture.getAsJsonArray("materials")) { @@ -168,7 +168,7 @@ private void run(MinecraftServer server, WorldServer world, String operation, St } player.inventory.clear(); player.getAdvancements().save(); - world.getSaveHandler().getPlayerNBTManager().writePlayerData(player); + world.getSaveHandler().writePlayerData(player); result.add("materials", kit); result.add("additionalMaterials", additions); verifyDiscovered(server, cases, rows); @@ -192,7 +192,7 @@ private void verifyDiscovered(MinecraftServer server, JsonArray definitions, Jso available |= !choices(criterion.getValue().getAsJsonObject()).isEmpty(); boolean done = player.getAdvancements().getProgress(advancement(server, id)).isDone(); require(!available || done, "Lost discovery: " + id); - IRecipe recipe = server.getRecipeManager().getRecipe(new ResourceLocation(id)); + IRecipe recipe = server.getRecipeManager().getRecipe(new ResourceLocation(id)).orElse(null); require(recipe == null || !available || player.getRecipeBook().isUnlocked(recipe), "Lost recipe reward: " + id); JsonObject row = new JsonObject(); row.addProperty("id", id); @@ -209,7 +209,7 @@ private void discover(Item item) { CriteriaTriggers.INVENTORY_CHANGED.trigger(player, player.inventory); } - private void reset(Advancement advancement, IRecipe recipe) { + private void reset(Advancement advancement, IRecipe recipe) { player.inventory.clear(); if (recipe != null) player.resetRecipes(Collections.singletonList(recipe)); for (String criterion : advancement.getCriteria().keySet()) diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksController.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksController.java index aa78b33..19f8646 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksController.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksController.java @@ -5,33 +5,33 @@ import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler; import cyano.poweradvantage.api.migration.LegacyMigrationApi; import net.minecraft.block.Block; -import net.minecraft.block.BlockFenceGate; -import net.minecraft.block.BlockFlowingFluid; -import net.minecraft.block.BlockStandingSign; -import net.minecraft.block.state.IBlockState; +import net.minecraft.block.FenceGateBlock; +import net.minecraft.block.FlowingFluidBlock; +import net.minecraft.block.StandingSignBlock; +import net.minecraft.block.BlockState; import net.minecraft.command.CommandException; import net.minecraft.enchantment.Enchantment; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityType; -import net.minecraft.entity.item.EntityItem; -import net.minecraft.init.Blocks; +import net.minecraft.entity.item.ItemEntity; +import net.minecraft.block.Blocks; import net.minecraft.inventory.IInventory; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.CompoundNBT; import net.minecraft.server.MinecraftServer; import net.minecraft.state.IProperty; import net.minecraft.tileentity.TileEntity; -import net.minecraft.tileentity.TileEntitySign; -import net.minecraft.util.EnumFacing; +import net.minecraft.tileentity.SignTileEntity; +import net.minecraft.util.Direction; import net.minecraft.util.ResourceLocation; -import net.minecraft.util.registry.IRegistry; +import net.minecraft.util.registry.Registry; import net.minecraft.util.math.AxisAlignedBB; import net.minecraft.util.math.BlockPos; import net.minecraft.util.text.Style; -import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.StringTextComponent; import net.minecraft.util.text.event.ClickEvent; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; import net.minecraft.world.biome.Biome; import net.minecraft.world.chunk.Chunk; import net.minecraft.world.dimension.DimensionType; @@ -87,7 +87,7 @@ void requireAuthorizedWorld(MinecraftServer server) throws CommandException { synchronized String build(MinecraftServer server, String target) throws CommandException { loadDefinition(); - WorldServer world = overworld(server); + ServerWorld world = overworld(server); if ("all".equalsIgnoreCase(target)) buildConcourse(world); List selected = select(target, true); int built = 0; @@ -109,7 +109,7 @@ synchronized String build(MinecraftServer server, String target) throws CommandE synchronized String reset(MinecraftServer server, String target) throws CommandException { List selected = select(target, true); - WorldServer world = overworld(server); + ServerWorld world = overworld(server); int reset = 0; for (WorksDefinition.Station station : selected) { if (!requirementsPresent(station)) continue; @@ -132,7 +132,7 @@ synchronized String start(MinecraftServer server, String target) throws CommandE && !station.id.equals(activeExclusiveStation)) { throw commandError("Stop exclusive station " + activeExclusiveStation + " first."); } - WorldServer world = overworld(server); + ServerWorld world = overworld(server); setStationChunksForced(world, station, true); applyActions(world, station, station.startActions); if (station.exclusive) activeExclusiveStation = station.id; @@ -143,7 +143,7 @@ synchronized String start(MinecraftServer server, String target) throws CommandE synchronized String stop(MinecraftServer server, String target) throws CommandException { WorksDefinition.Station station = oneStation(target); - WorldServer world = overworld(server); + ServerWorld world = overworld(server); if (requirementsPresent(station)) { applyActions(world, station, station.stopActions); removeTaggedEntities(world, station); @@ -154,7 +154,7 @@ synchronized String stop(MinecraftServer server, String target) throws CommandEx return "Stopped " + station.id + " - " + station.name; } - private static void setStationChunksForced(WorldServer world, + private static void setStationChunksForced(ServerWorld world, WorksDefinition.Station station, boolean forced) { int minChunkX = (station.origin[0] + station.boundsMin[0]) >> 4; @@ -163,14 +163,14 @@ private static void setStationChunksForced(WorldServer world, int maxChunkZ = (station.origin[2] + station.boundsMax[2]) >> 4; for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { - world.func_212414_b(chunkX, chunkZ, forced); + world.forceChunk(chunkX, chunkZ, forced); } } } synchronized String check(MinecraftServer server, String target) throws CommandException { List selected = select(target, true); - WorldServer world = overworld(server); + ServerWorld world = overworld(server); Map summary = new LinkedHashMap<>(); for (WorksDefinition.Station station : selected) { String status = checkStation(world, station); @@ -203,7 +203,7 @@ synchronized String sampleWorldgen(MinecraftServer server, int radius) throws Co if (radius < 1 || radius > 16) { throw commandError("Worldgen sample radius must be between 1 and 16 chunks"); } - WorldServer world = overworld(server); + ServerWorld world = overworld(server); Map expectedBiomes = new LinkedHashMap<>(); expectedBiomes.put("poweradvantage:crude_oil", "DESERT"); expectedBiomes.put("mineralogy:crude_oil", "OCEAN"); @@ -287,7 +287,7 @@ synchronized String sampleWorldgen(MinecraftServer server, int radius) throws Co + ": " + counts; } - private static void addBiomeSample(WorldServer world, Set chunks, + private static void addBiomeSample(ServerWorld world, Set chunks, Map centers, BiomeDictionary.Type type, String label, int radius, BiomeDictionary.Type... excludedTypes) { @@ -355,7 +355,7 @@ List stationIdsWithAll() { return ids; } - private String checkStation(WorldServer world, WorksDefinition.Station station) { + private String checkStation(ServerWorld world, WorksDefinition.Station station) { if (!requirementsPresent(station)) { statuses.put(station.id, "SKIP"); setStatusBlock(world, station, STATUS_ATTENTION); @@ -421,7 +421,7 @@ private String checkStation(WorldServer world, WorksDefinition.Station station) return status; } - private void writeStationResult(WorldServer world, WorksDefinition.Station station, String status, + private void writeStationResult(ServerWorld world, WorksDefinition.Station station, String status, List> assertions) { Map payload = new LinkedHashMap<>(); payload.put("name", station.name); @@ -434,7 +434,7 @@ private void writeStationResult(WorldServer world, WorksDefinition.Station stati resultWriter.write(station.id, "latest", payload); } - private List> snapshot(WorldServer world, WorksDefinition.Station station) { + private List> snapshot(ServerWorld world, WorksDefinition.Station station) { List> snapshot = new ArrayList<>(); Set positions = new LinkedHashSet<>(); for (WorksDefinition.Placement placement : station.placements) positions.add(absolute(station, placement.pos)); @@ -443,20 +443,33 @@ private List> snapshot(WorldServer world, WorksDefinition.St } for (BlockPos pos : positions) { Map block = new LinkedHashMap<>(); - IBlockState state = world.getBlockState(pos); + BlockState state = world.getBlockState(pos); ResourceLocation name = ForgeRegistries.BLOCKS.getKey(state.getBlock()); block.put("pos", Arrays.asList(pos.getX(), pos.getY(), pos.getZ())); block.put("block", name == null ? "unknown" : name.toString()); block.put("state", state.toString()); TileEntity tile = world.getTileEntity(pos); - if (tile != null) block.put("nbt", tile.write(new NBTTagCompound()).toString()); + if (tile != null) block.put("nbt", tile.write(new CompoundNBT()).toString()); snapshot.add(block); } return snapshot; } - private boolean evaluateAssertion(WorldServer world, WorksDefinition.Station station, + private boolean evaluateAssertion(ServerWorld world, WorksDefinition.Station station, WorksDefinition.Assertion assertion) { + if (BayDrillAssertions.applies(station, assertion)) { + return BayDrillAssertions.matches(station, assertion, + relative -> world.getBlockState(absolute(station, relative)).getBlock() == block(assertion.block), + relative -> { + BlockState state = world.getBlockState(absolute(station, relative)); + if (assertion.meta != null) { + return state.equals(legacyState(assertion.block, block(assertion.block), assertion.meta)); + } + return state.getValues().entrySet().stream().anyMatch(entry -> + assertion.property.equals(entry.getKey().getName()) + && assertion.expectedText.equals(String.valueOf(entry.getValue()))); + }); + } if ("registryBlock".equals(assertion.type)) { return block(assertion.block) != Blocks.AIR; } @@ -471,7 +484,7 @@ private boolean evaluateAssertion(WorldServer world, WorksDefinition.Station sta for (int[] position : assertion.positions) { TileEntity source = world.getTileEntity(absolute(station, position)); if (source == null) return false; - NBTTagCompound tank = source.write(new NBTTagCompound()).getCompound(assertion.nbtPath); + CompoundNBT tank = source.write(new CompoundNBT()).getCompound(assertion.nbtPath); if (tank.contains("Empty", 1)) continue; int amount = tank.getInt("Amount"); if (amount < 0) return false; @@ -481,12 +494,12 @@ private boolean evaluateAssertion(WorldServer world, WorksDefinition.Station sta } BlockPos pos = absolute(station, assertion.pos); if ("block".equals(assertion.type)) { - IBlockState state = world.getBlockState(pos); + BlockState state = world.getBlockState(pos); if (state.getBlock() != block(assertion.block)) return false; return assertion.meta == null || state.equals(legacyState(assertion.block, state.getBlock(), assertion.meta)); } if ("blockProperty".equals(assertion.type)) { - IBlockState state = world.getBlockState(pos); + BlockState state = world.getBlockState(pos); if (state.getBlock() != block(assertion.block)) return false; for (Map.Entry, Comparable> entry : state.getValues().entrySet()) { if (entry.getKey().getName().equals(assertion.property)) { @@ -496,7 +509,7 @@ private boolean evaluateAssertion(WorldServer world, WorksDefinition.Station sta return false; } if ("comparatorOutput".equals(assertion.type)) { - IBlockState state = world.getBlockState(pos); + BlockState state = world.getBlockState(pos); Block source = state.getBlock(); return source.hasComparatorInputOverride(state) && compare(source.getComparatorInputOverride(state, world, pos), @@ -523,18 +536,18 @@ && compare(source.getComparatorInputOverride(state, world, pos), } if ("nbtNumber".equals(assertion.type)) { if (tile == null) return false; - NBTTagCompound nbt = tile.write(new NBTTagCompound()); + CompoundNBT nbt = tile.write(new CompoundNBT()); Number number = numberAt(nbt, assertion.nbtPath); return number != null && compare(number.doubleValue(), assertion.operator, assertion.value); } if ("nbtString".equals(assertion.type)) { if (tile == null) return false; - String text = stringAt(tile.write(new NBTTagCompound()), assertion.nbtPath); + String text = stringAt(tile.write(new CompoundNBT()), assertion.nbtPath); return assertion.expectedText != null && assertion.expectedText.equals(text); } if ("energy".equals(assertion.type)) { if (tile == null) return false; - int[] energy = tile.write(new NBTTagCompound()).getIntArray("Energy"); + int[] energy = tile.write(new CompoundNBT()).getIntArray("Energy"); int index = assertion.energyIndex == null ? 0 : assertion.energyIndex; return index >= 0 && index < energy.length && compare(Float.intBitsToFloat(energy[index]), assertion.operator, assertion.value); @@ -552,10 +565,10 @@ private static int inventoryCount(IInventory inventory, String itemName) { return count; } - private static Number numberAt(NBTTagCompound root, String path) { + private static Number numberAt(CompoundNBT root, String path) { if (path == null || path.isEmpty()) return null; String[] parts = path.split("\\."); - NBTTagCompound current = root; + CompoundNBT current = root; for (int i = 0; i < parts.length - 1; i++) { if (!current.contains(parts[i], 10)) return null; current = current.getCompound(parts[i]); @@ -565,10 +578,10 @@ private static Number numberAt(NBTTagCompound root, String path) { return current.getDouble(leaf); } - private static String stringAt(NBTTagCompound root, String path) { + private static String stringAt(CompoundNBT root, String path) { if (path == null || path.isEmpty()) return null; String[] parts = path.split("\\."); - NBTTagCompound current = root; + CompoundNBT current = root; for (int i = 0; i < parts.length - 1; i++) { if (!current.contains(parts[i], 10)) return null; current = current.getCompound(parts[i]); @@ -590,7 +603,7 @@ private static boolean compare(double actual, String operator, Number expected) } } - private void resetStation(WorldServer world, WorksDefinition.Station station) { + private void resetStation(ServerWorld world, WorksDefinition.Station station) { clearStation(world, station); decorateStation(world, station); for (WorksDefinition.Placement placement : station.placements) applyPlacement(world, station, placement); @@ -598,27 +611,27 @@ private void resetStation(WorldServer world, WorksDefinition.Station station) { setStatusBlock(world, station, STATUS_NEUTRAL); } - private void clearStation(WorldServer world, WorksDefinition.Station station) { + private void clearStation(ServerWorld world, WorksDefinition.Station station) { BlockPos min = absolute(station, station.boundsMin); BlockPos max = absolute(station, station.boundsMax); removeTaggedEntities(world, station); - for (BlockPos.MutableBlockPos pos : BlockPos.getAllInBoxMutable(min, max)) { + for (BlockPos pos : BlockPos.getAllInBoxMutable(min, max)) { world.setBlockState(pos, Blocks.AIR.getDefaultState(), 2); } } - private void removeTaggedEntities(WorldServer world, WorksDefinition.Station station) { + private void removeTaggedEntities(ServerWorld world, WorksDefinition.Station station) { BlockPos min = absolute(station, station.boundsMin); BlockPos max = absolute(station, station.boundsMax); for (Entity entity : world.getEntitiesWithinAABB(Entity.class, new AxisAlignedBB(min, max.add(1, 1, 1)))) { - if (station.id.equals(entity.getEntityData().getString("AdvantageWorksStation"))) { + if (station.id.equals(entity.getPersistentData().getString("AdvantageWorksStation"))) { entity.remove(); } } } - private void decorateStation(WorldServer world, WorksDefinition.Station station) { + private void decorateStation(ServerWorld world, WorksDefinition.Station station) { int color = districtColor(station.district); int maxX = station.boundsMax[0]; int maxZ = station.boundsMax[2]; @@ -626,7 +639,7 @@ private void decorateStation(WorldServer world, WorksDefinition.Station station) for (int z = station.boundsMin[2]; z <= maxZ; z++) { boolean edge = x == station.boundsMin[0] || x == maxX || z == station.boundsMin[2] || z == maxZ; - IBlockState state = edge ? wool(color).getDefaultState() : Blocks.STONE_BRICKS.getDefaultState(); + BlockState state = edge ? wool(color).getDefaultState() : Blocks.STONE_BRICKS.getDefaultState(); world.setBlockState(absolute(station, new int[]{x, 0, z}), state, 2); } } @@ -643,7 +656,7 @@ private void decorateStation(WorldServer world, WorksDefinition.Station station) "/advworks stop " + station.id); } - private void encloseStation(WorldServer world, WorksDefinition.Station station) { + private void encloseStation(ServerWorld world, WorksDefinition.Station station) { int minX = station.boundsMin[0]; int maxX = station.boundsMax[0]; int minZ = station.boundsMin[2]; @@ -666,10 +679,10 @@ private void encloseStation(WorldServer world, WorksDefinition.Station station) } } world.setBlockState(absolute(station, new int[]{gateX, 1, minZ}), - Blocks.OAK_FENCE_GATE.getDefaultState().with(BlockFenceGate.HORIZONTAL_FACING, EnumFacing.NORTH), 2); + Blocks.OAK_FENCE_GATE.getDefaultState().with(FenceGateBlock.HORIZONTAL_FACING, Direction.NORTH), 2); } - private void buildConcourse(WorldServer world) { + private void buildConcourse(ServerWorld world) { for (int x = -15; x <= 15; x++) { for (int z = -15; z <= 15; z++) { world.setBlockState(new BlockPos(x, 3, z), Blocks.STONE_BRICKS.getDefaultState(), 2); @@ -687,7 +700,7 @@ private void buildConcourse(WorldServer world) { placeSign(world, new BlockPos(0, 4, 3), "PROVING", "GROUNDS SOUTH", "", ""); } - private void applyActions(WorldServer world, WorksDefinition.Station station, + private void applyActions(ServerWorld world, WorksDefinition.Station station, List actions) { for (WorksDefinition.Action action : actions) { if (!requirementsPresent(action.requiredMods)) continue; @@ -781,17 +794,18 @@ private void applyActions(WorldServer world, WorksDefinition.Station station, case "spawnItem": Item dropped = item(action.item); if (dropped == null) throw new IllegalStateException("Unknown item " + action.item); - EntityItem entityItem = new EntityItem(world, pos.getX() + 0.5, pos.getY() + 0.1, + ItemEntity entityItem = new ItemEntity(world, pos.getX() + 0.5, pos.getY() + 0.1, pos.getZ() + 0.5, itemStack(dropped, action.count, action.damage)); tagEntity(entityItem, station.id); - world.spawnEntity(entityItem); + world.addEntity(entityItem); break; case "spawnEntity": - Entity entity = EntityType.create(world, new ResourceLocation(action.entity)); + EntityType entityType = ForgeRegistries.ENTITIES.getValue(new ResourceLocation(action.entity)); + Entity entity = entityType == null ? null : entityType.create(world); if (entity == null) throw new IllegalStateException("Unknown entity " + action.entity); entity.setPosition(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); tagEntity(entity, station.id); - world.spawnEntity(entity); + world.addEntity(entity); break; case "time": world.setDayTime(action.value); @@ -814,7 +828,7 @@ private void applyActions(WorldServer world, WorksDefinition.Station station, } } - private void applyPlacement(WorldServer world, WorksDefinition.Station station, + private void applyPlacement(ServerWorld world, WorksDefinition.Station station, WorksDefinition.Placement placement) { if (!requirementsPresent(placement.requiredMods)) return; Block target = block(placement.block); @@ -827,16 +841,16 @@ private void applyPlacement(WorldServer world, WorksDefinition.Station station, if (placement.nbt != null) mergeTileNbt(world, pos, placement.nbt); } - private void mergeTileNbt(WorldServer world, BlockPos pos, JsonObject supplied) { + private void mergeTileNbt(ServerWorld world, BlockPos pos, JsonObject supplied) { TileEntity tile = world.getTileEntity(pos); if (tile == null) throw new IllegalStateException("No tile entity at " + pos); try { - NBTTagCompound base = tile.write(new NBTTagCompound()); - NBTTagCompound additions = JsonNbtConverter.toCompound(supplied); + CompoundNBT base = tile.write(new CompoundNBT()); + CompoundNBT additions = JsonNbtConverter.toCompound(supplied); base.merge(additions); - base.setInt("x", pos.getX()); - base.setInt("y", pos.getY()); - base.setInt("z", pos.getZ()); + base.putInt("x", pos.getX()); + base.putInt("y", pos.getY()); + base.putInt("z", pos.getZ()); tile.read(base); tile.markDirty(); } catch (Exception ex) { @@ -896,29 +910,29 @@ private static int statusColor(String status) { return STATUS_ATTENTION; } - private static void setStatusBlock(WorldServer world, WorksDefinition.Station station, int meta) { + private static void setStatusBlock(ServerWorld world, WorksDefinition.Station station, int meta) { world.setBlockState(absolute(station, new int[]{11, 1, 1}), wool(meta).getDefaultState(), 2); } - private static void placeCommandSign(WorldServer world, BlockPos pos, String label, String command) { - world.setBlockState(pos, Blocks.SIGN.getDefaultState().with(BlockStandingSign.ROTATION, 8), 2); + private static void placeCommandSign(ServerWorld world, BlockPos pos, String label, String command) { + world.setBlockState(pos, Blocks.OAK_SIGN.getDefaultState().with(StandingSignBlock.ROTATION, 8), 2); TileEntity tile = world.getTileEntity(pos); - if (!(tile instanceof TileEntitySign)) return; - TileEntitySign sign = (TileEntitySign) tile; - TextComponentString clickable = new TextComponentString(label); + if (!(tile instanceof SignTileEntity)) return; + SignTileEntity sign = (SignTileEntity) tile; + StringTextComponent clickable = new StringTextComponent(label); clickable.setStyle(new Style().setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, command))); sign.signText[0] = clickable; - sign.signText[1] = new TextComponentString("right-click"); + sign.signText[1] = new StringTextComponent("right-click"); sign.markDirty(); } - private static void placeSign(WorldServer world, BlockPos pos, String... lines) { - world.setBlockState(pos, Blocks.SIGN.getDefaultState().with(BlockStandingSign.ROTATION, 8), 2); + private static void placeSign(ServerWorld world, BlockPos pos, String... lines) { + world.setBlockState(pos, Blocks.OAK_SIGN.getDefaultState().with(StandingSignBlock.ROTATION, 8), 2); TileEntity tile = world.getTileEntity(pos); - if (!(tile instanceof TileEntitySign)) return; - TileEntitySign sign = (TileEntitySign) tile; + if (!(tile instanceof SignTileEntity)) return; + SignTileEntity sign = (SignTileEntity) tile; for (int i = 0; i < sign.signText.length; i++) { - sign.signText[i] = new TextComponentString(i < lines.length ? trimSign(lines[i]) : ""); + sign.signText[i] = new StringTextComponent(i < lines.length ? trimSign(lines[i]) : ""); } sign.markDirty(); } @@ -934,11 +948,11 @@ private static BlockPos absolute(WorksDefinition.Station station, int[] relative } private static void tagEntity(Entity entity, String stationId) { - entity.getEntityData().setString("AdvantageWorksStation", stationId); + entity.getPersistentData().putString("AdvantageWorksStation", stationId); } - private static WorldServer overworld(MinecraftServer server) throws CommandException { - WorldServer world = server.getWorld(DimensionType.OVERWORLD); + private static ServerWorld overworld(MinecraftServer server) throws CommandException { + ServerWorld world = server.getWorld(DimensionType.OVERWORLD); if (world == null) throw commandError("Overworld is not loaded"); return world; } @@ -959,16 +973,16 @@ private static Item item(String name) { } private static CommandException commandError(String message) { - return new CommandException(new TextComponentString(message)); + return new CommandException(new StringTextComponent(message)); } - private static IBlockState legacyState(String name, Block block, int metadata) { + private static BlockState legacyState(String name, Block block, int metadata) { if (name != null && (name.startsWith("poweradvantage:") || name.startsWith("steamadvantage:") || name.startsWith("electricadvantage:"))) { return LegacyMigrationApi.decodeBlockState(new ResourceLocation(name), block, metadata); } - if (block instanceof BlockFlowingFluid) { - return block.getDefaultState().with(BlockFlowingFluid.LEVEL, Integer.valueOf(metadata & 15)); + if (block instanceof FlowingFluidBlock) { + return block.getDefaultState().with(FlowingFluidBlock.LEVEL, Integer.valueOf(metadata & 15)); } return block.getDefaultState(); } @@ -983,15 +997,15 @@ private static Fluid fluid(String name) { if (name == null || name.trim().isEmpty()) return null; ResourceLocation id = name.indexOf(':') >= 0 ? new ResourceLocation(name) : new ResourceLocation("poweradvantage", name); - Fluid value = IRegistry.field_212619_h.func_212608_b(id); + Fluid value = Registry.FLUID.getValue(id).orElse(null); if (value == null) { String migratedName = LegacyMigrationApi.fluidTarget(name); if (!name.equals(migratedName)) { - value = IRegistry.field_212619_h.func_212608_b(new ResourceLocation(migratedName)); + value = Registry.FLUID.getValue(new ResourceLocation(migratedName)).orElse(null); } } if (value == null && name.indexOf(':') < 0) { - value = IRegistry.field_212619_h.func_212608_b(new ResourceLocation("minecraft", name)); + value = Registry.FLUID.getValue(new ResourceLocation("minecraft", name)).orElse(null); } return value; } diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksDefinitionLoader.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksDefinitionLoader.java index afb1225..e011ba0 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksDefinitionLoader.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksDefinitionLoader.java @@ -2,6 +2,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; import net.minecraft.util.ResourceLocation; import java.io.InputStream; @@ -14,7 +15,8 @@ import java.util.Set; final class WorksDefinitionLoader { - private static final String RESOURCE = "/assets/advantageworkstest/works/works-1.13.2.json"; + private static final String RESOURCE = "/assets/advantageworkstest/works/works-1.14.4.json"; + private static final String STATION_SOURCE = "/assets/advantageworkstest/works/works-1.13.2.json"; private static final Gson GSON = new GsonBuilder().create(); private static final Set ACTION_TYPES = new HashSet<>(Arrays.asList( "setBlock", "setAir", "clearInventory", "setInventory", "fillInventory", @@ -29,22 +31,32 @@ private WorksDefinitionLoader() { } static WorksDefinition load() { - InputStream stream = WorksDefinitionLoader.class.getResourceAsStream(RESOURCE); - if (stream == null) { - throw new IllegalStateException("Missing works definition " + RESOURCE); - } - WorksDefinition definition; + JsonObject target = readResource(RESOURCE); + if (target.get("format").getAsInt() != 1 || !"1.14.4".equals(target.get("minecraft").getAsString()) + || !STATION_SOURCE.equals(target.get("stationSource").getAsString())) + throw new IllegalArgumentException("Unsupported native works descriptor"); + JsonObject stations = readResource(STATION_SOURCE); + // Only the target header changes. All station cards remain the immutable source specification. + stations.add("format", target.get("format")); + stations.add("minecraft", target.get("minecraft")); + stations.add("title", target.get("title")); + WorksDefinition definition = GSON.fromJson(stations, WorksDefinition.class); + validate(definition); + return definition; + } + + private static JsonObject readResource(String resource) { + InputStream stream = WorksDefinitionLoader.class.getResourceAsStream(resource); + if (stream == null) throw new IllegalStateException("Missing works definition " + resource); try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { - definition = GSON.fromJson(reader, WorksDefinition.class); + return GSON.fromJson(reader, JsonObject.class); } catch (Exception ex) { - throw new IllegalStateException("Cannot read works definition " + RESOURCE, ex); + throw new IllegalStateException("Cannot read works definition " + resource, ex); } - validate(definition); - return definition; } private static void validate(WorksDefinition definition) { - if (definition == null || definition.format != 1 || !"1.13.2".equals(definition.minecraft)) { + if (definition == null || definition.format != 1 || !"1.14.4".equals(definition.minecraft)) { throw new IllegalArgumentException("Unsupported or missing works definition header"); } Set districtIds = new HashSet<>(); diff --git a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksResultWriter.java b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksResultWriter.java index a758b5d..f300fd9 100644 --- a/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksResultWriter.java +++ b/test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksResultWriter.java @@ -31,7 +31,7 @@ File write(String stationId, String kind, Map payload) { Map document = new LinkedHashMap<>(); document.put("format", 1); document.put("recordedAtUtc", utcNow()); - document.put("minecraft", "1.13.2"); + document.put("minecraft", "1.14.4"); document.put("forge", net.minecraftforge.versions.forge.ForgeVersion.getVersion()); document.put("mods", loadedMods()); document.put("station", stationId); diff --git a/test-rig/src/main/resources/META-INF/mods.toml b/test-rig/src/main/resources/META-INF/mods.toml index 5ea8f12..2a3648a 100644 --- a/test-rig/src/main/resources/META-INF/mods.toml +++ b/test-rig/src/main/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[25,)" +loaderVersion="[28,)" license="LGPL-3.0-only" [[mods]] @@ -12,20 +12,20 @@ description='''Server-side commissioning harness for the Advantage family. Not f [[dependencies.advantageworkstest]] modId="poweradvantage" mandatory=true -versionRange="[2.4.4.113021,)" +versionRange="[2.4.5.114041,)" ordering="AFTER" side="SERVER" [[dependencies.advantageworkstest]] modId="forge" mandatory=true -versionRange="[25,)" +versionRange="[28.2.26,)" ordering="NONE" side="SERVER" [[dependencies.advantageworkstest]] modId="minecraft" mandatory=true -versionRange="[1.13.2]" +versionRange="[1.14.4]" ordering="NONE" side="SERVER" diff --git a/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.14.4.json b/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.14.4.json new file mode 100644 index 0000000..9da483e --- /dev/null +++ b/test-rig/src/main/resources/assets/advantageworkstest/works/works-1.14.4.json @@ -0,0 +1,6 @@ +{ + "format": 1, + "minecraft": "1.14.4", + "title": "Advantage Works 1.14.4 Commissioning Ground", + "stationSource": "/assets/advantageworkstest/works/works-1.13.2.json" +} diff --git a/test-rig/src/main/resources/pack.mcmeta b/test-rig/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..7f03646 --- /dev/null +++ b/test-rig/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "Advantage Works test resources", + "pack_format": 4 + } +} diff --git a/test-rig/src/runner/java/zone/moddev/mc/advantageworks/runner/WorksRunner.java b/test-rig/src/runner/java/zone/moddev/mc/advantageworks/runner/WorksRunner.java index 4fbd577..7b3b0b0 100644 --- a/test-rig/src/runner/java/zone/moddev/mc/advantageworks/runner/WorksRunner.java +++ b/test-rig/src/runner/java/zone/moddev/mc/advantageworks/runner/WorksRunner.java @@ -57,8 +57,13 @@ public static void main(String[] args) throws Exception { Arguments arguments = Arguments.parse(args); Profile profile = readProfile(arguments.profile); if (arguments.serverPort != null) profile.serverPort = arguments.serverPort; - File testRig = arguments.profile.getParentFile().getParentFile().getCanonicalFile(); + File testRig = findTestRig(arguments.profile); File powerRoot = testRig.getParentFile().getCanonicalFile(); + if (arguments.validateOnly) { + List> inputs = stageMods(testRig, null, profile.mods); + System.out.println("Native 1.14.4 profile preflight only (no world opened): " + GSON.toJson(inputs)); + return; + } String stamp = timestamp(); File runtimeBase = resolve(testRig, profile.runtimeBase); File runtime = new File(runtimeBase, profile.name + "-" + stamp).getCanonicalFile(); @@ -307,8 +312,8 @@ private static List> stageMods(File testRig, File mods, List throw new IllegalStateException("SHA-256 mismatch for " + input.id + ": " + hash); } if (input.verifyIdentity) verifyModIdentity(source, input.id, input.version); - File destination = new File(mods, source.getName()); - Files.copy(source.toPath(), destination.toPath(), StandardCopyOption.COPY_ATTRIBUTES); + File destination = mods == null ? source : new File(mods, source.getName()); + if (mods != null) Files.copy(source.toPath(), destination.toPath(), StandardCopyOption.COPY_ATTRIBUTES); Map record = new LinkedHashMap<>(); record.put("id", input.id); record.put("version", input.version); @@ -420,7 +425,7 @@ private static File java8Home(Profile profile) throws Exception { } File executable = javaExecutable(home); if (!executable.isFile()) { - throw new IllegalStateException("Java 8 is required for packaged Forge 1.13.2. Set " + throw new IllegalStateException("Java 8 is required for packaged Forge 1.14.4. Set " + "ADVANTAGE_WORKS_JAVA8_HOME; checked " + home); } return home.getCanonicalFile(); @@ -484,8 +489,9 @@ private static void writeRuntimeFiles(File runtime, Profile profile) throws Exce + "level-seed=" + profile.levelSeed + "\n" + "level-type=" + profile.levelType + "\n" + "max-players=8\n" - + "motd=Advantage Works 1.13.2 Commissioning Ground\n" + + "motd=Advantage Works 1.14.4 Commissioning Ground\n" + "online-mode=false\n" + + "server-ip=127.0.0.1\n" + "pvp=false\n" + "server-port=" + profile.serverPort + "\n" + "spawn-animals=false\n" @@ -506,6 +512,9 @@ private static void writeRunManifest(File runtime, Profile profile, manifest.put("recipeMode", profile.recipeMode); manifest.put("sourceCommit", profile.sourceCommit); manifest.put("sourceVersion", profile.sourceVersion); + manifest.put("minecraftVersion", profile.minecraftVersion); + manifest.put("qualificationStatus", profile.qualificationStatus); + manifest.put("legacySourceProfile", profile.legacySourceProfile); manifest.put("levelName", profile.levelName); manifest.put("levelType", profile.levelType); manifest.put("buildWorks", profile.buildWorks); @@ -522,13 +531,22 @@ private static void writeRunManifest(File runtime, Profile profile, } } - private static Profile readProfile(File file) throws Exception { + static Profile readProfile(File file) throws Exception { try (InputStreamReader reader = new InputStreamReader( new FileInputStream(file), StandardCharsets.UTF_8)) { Profile profile = GSON.fromJson(reader, Profile.class); if (profile == null || profile.name == null || profile.mode == null) { throw new IllegalArgumentException("Incomplete profile " + file); } + if (!"1.14.4".equals(profile.minecraftVersion)) { + throw new IllegalArgumentException("Historical source profile, not a native114 destination: " + file); + } + if (!"PORTED_UNQUALIFIED".equals(profile.portStatus)) { + throw new IllegalStateException("Native114 profile is not ported: " + profile.portBlocker); + } + if (profile.forge == null || !"1.14.4-28.2.26".equals(profile.forge.version)) { + throw new IllegalArgumentException("Native114 profile must select Forge 1.14.4-28.2.26"); + } if (profile.levelName == null || !profile.levelName.startsWith("AdvantageWorks")) { throw new IllegalArgumentException("Profile levelName must begin with AdvantageWorks: " + file); } @@ -536,6 +554,16 @@ private static Profile readProfile(File file) throws Exception { } } + static File findTestRig(File profile) throws Exception { + File current = profile.getCanonicalFile().getParentFile(); + while (current != null) { + if (new File(current, "build.gradle").isFile() && new File(current, "profiles").isDirectory() + && new File(current, "src/main/resources/assets/advantageworkstest").isDirectory()) return current; + current = current.getParentFile(); + } + throw new IllegalArgumentException("Profile must be inside the Advantage Works test-rig: " + profile); + } + private static void stageConfigFixtures(File testRig, File runtime, List fixtures) throws Exception { for (ConfigFixture fixture : fixtures) { @@ -861,6 +889,7 @@ private static String timestamp() { private static final class Arguments { File profile; boolean automated; + boolean validateOnly; List lifecycleStations; Integer stationRunSeconds; Integer serverPort; @@ -870,6 +899,7 @@ static Arguments parse(String[] args) { for (int i = 0; i < args.length; i++) { if ("--profile".equals(args[i]) && i + 1 < args.length) result.profile = new File(args[++i]); else if ("--automated".equals(args[i])) result.automated = true; + else if ("--validate-only".equals(args[i])) result.validateOnly = true; else if ("--stations".equals(args[i]) && i + 1 < args.length) { result.lifecycleStations = new ArrayList<>(); for (String station : args[++i].split(",")) { @@ -898,18 +928,18 @@ else if ("--server-port".equals(args[i]) && i + 1 < args.length) { } } - private static final class Profile { + static final class Profile { String name; String mode; - String runtimeBase = "../run-advantage-works"; + String runtimeBase = "build/works-native114"; int serverPort = 25565; int startupTimeoutSeconds = 120; int shutdownTimeoutSeconds = 60; int stationRunSeconds = 5; String levelName = "AdvantageWorks"; - String levelSeed = "advantage-works-1.13.2-v1"; + String levelSeed = "advantage-works-1.14.4-v1"; String levelType = "FLAT"; - String generatorSettings = "3;minecraft:bedrock,2*minecraft:dirt,minecraft:grass;1;"; + String generatorSettings = ""; boolean buildWorks = true; int worldgenSampleRadius; boolean requireStableWorldgenCounts = true; @@ -919,6 +949,11 @@ private static final class Profile { String recipeMode = "NORMAL"; String sourceCommit; String sourceVersion; + String minecraftVersion; + String portStatus; + String portBlocker; + String qualificationStatus; + String legacySourceProfile; ForgeInput forge; List launchCommand = new ArrayList<>(); List mods = new ArrayList<>(); diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/BayDrillAssertionsTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/BayDrillAssertionsTest.java new file mode 100644 index 0000000..5bb49be --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/BayDrillAssertionsTest.java @@ -0,0 +1,58 @@ +package zone.moddev.mc.advantageworks; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.junit.Test; +import static org.junit.Assert.*; + +public class BayDrillAssertionsTest { + @Test public void initialAndSingleMovedWitnessesKeepAllTwelveFacingChecks() { + int count = 0; + for (WorksDefinition.Station station : WorksDefinitionLoader.load().stations) { + for (WorksDefinition.Assertion assertion : station.assertions) { + if (!BayDrillAssertions.applies(station, assertion)) continue; + assertTrue(matches(station, assertion, assertion.pos)); + assertTrue(station.id + "/" + assertion.id + " on its declared rear track", + matches(station, assertion, BayDrillAssertions.rearTrack(station, assertion))); + count++; + } + } + assertEquals(12, count); + } + + @Test public void missingDuplicateWrongFacingAndOffTrackWitnessesFail() { + for (WorksDefinition.Station station : WorksDefinitionLoader.load().stations) { + for (WorksDefinition.Assertion assertion : station.assertions) { + if (!BayDrillAssertions.applies(station, assertion)) continue; + int[] rear = BayDrillAssertions.rearTrack(station, assertion); + assertFalse(matches(station, assertion)); + assertFalse(matches(station, assertion, assertion.pos, rear)); + assertFalse(BayDrillAssertions.matches(station, assertion, p -> true, p -> false)); + int[] stray = rear.clone(); + stray[0] += 2; + assertFalse(matches(station, assertion, stray)); + } + } + } + + @Test public void noUndeclaredTrackFallbackIsAllowed() { + WorksDefinition.Station station = WorksDefinitionLoader.load().stations.stream() + .filter(s -> "S-05".equals(s.id)).findFirst().get(); + station.placements.clear(); + try { + BayDrillAssertions.rearTrack(station, station.assertions.get(0)); + fail("Undeclared movement accepted"); + } catch (IllegalArgumentException expected) { + assertEquals("Drill bay has no declared rear track", expected.getMessage()); + } + } + + private static boolean matches(WorksDefinition.Station station, WorksDefinition.Assertion assertion, + int[]... present) { + Set positions = new HashSet<>(); + for (int[] p : present) positions.add(Arrays.toString(p)); + return BayDrillAssertions.matches(station, assertion, + p -> positions.contains(Arrays.toString(p)), p -> positions.contains(Arrays.toString(p))); + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleProbeTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleProbeTest.java index 6896ce6..660451e 100644 --- a/test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleProbeTest.java +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleProbeTest.java @@ -4,6 +4,9 @@ import java.util.Collections; import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + public class LifecycleProbeTest { private LifecycleProbe.Fixture fixture() { LifecycleProbe.Fixture fixture = new LifecycleProbe.Fixture(); @@ -25,6 +28,66 @@ private LifecycleProbe.Fixture fixture() { LifecycleProbe.Fixture f = fixture(); f.bucket = "poweradvantage:crude_oil_bucket"; f.source = new int[] {2060, 80, 2060}; LifecycleProbe.validate(f); } + @Test public void fourPowerOilLanesRemainAcceptedWithoutMineralogy() { + for (String oil : Arrays.asList("crude_oil", "refined_oil")) { + for (String tank : Arrays.asList("fluid_storage_tank", "fluid_metal_tank")) { + LifecycleProbe.Fixture f = fixture(); + f.id = "OIL-" + oil + "-" + tank; + f.bucket = "poweradvantage:" + oil + "_bucket"; + f.source = new int[] {2060, 80, 2060}; + LifecycleProbe.validate(f, () -> { + throw new AssertionError("Power oil must not require or query Mineralogy"); + }); + LifecycleProbe.validate(f); + } + } + } + @Test public void nativeMineralogyOilBucketAcceptedWhenLoaded() { + LifecycleProbe.Fixture f = fixture(); f.bucket = "mineralogy:crude_oil_bucket"; + f.source = new int[] {2060, 80, 2060}; + LifecycleProbe.validate(f, () -> true); + } + @Test public void nativeMineralogyOilRequiresItsMod() { + LifecycleProbe.Fixture f = fixture(); f.bucket = "mineralogy:crude_oil_bucket"; + f.source = new int[] {2060, 80, 2060}; + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> LifecycleProbe.validate(f, () -> false)); + assertEquals("Mineralogy oil bucket requires mineralogy", error.getMessage()); + } + @Test public void waterAndLookalikeOilBucketsRemainRejectedWithMineralogyLoaded() { + for (String bucket : Arrays.asList("minecraft:water_bucket", "mineralogy:refined_oil_bucket", + "other:crude_oil_bucket", "mineralogy:crude_oil_bucket_extra", + "poweradvantage:crude_oil_bucket_extra", "poweradvantage:other_oil_bucket")) { + LifecycleProbe.Fixture f = fixture(); f.bucket = bucket; + f.source = new int[] {2060, 80, 2060}; + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> LifecycleProbe.validate(f, () -> true)); + assertEquals("Unsupported oil bucket", error.getMessage()); + } + } + @Test(expected = IllegalArgumentException.class) public void mineralogyOilSourceOutsideBoundsRejected() { + LifecycleProbe.Fixture f = fixture(); f.bucket = "mineralogy:crude_oil_bucket"; + f.source = new int[] {2080, 80, 2060}; LifecycleProbe.validate(f, () -> true); + } + @Test(expected = IllegalArgumentException.class) public void mineralogyOilBucketNeedsHeadroom() { + LifecycleProbe.Fixture f = fixture(); f.bucket = "mineralogy:crude_oil_bucket"; + f.source = new int[] {2060, 90, 2060}; LifecycleProbe.validate(f, () -> true); + } + @Test(expected = IllegalArgumentException.class) public void mineralogyDoesNotAllowUnsafeFixtureId() { + LifecycleProbe.Fixture f = fixture(); f.bucket = "mineralogy:crude_oil_bucket"; + f.source = new int[] {2060, 80, 2060}; f.id = "../OIL-mineralogy"; + LifecycleProbe.validate(f, () -> true); + } + @Test(expected = IllegalArgumentException.class) public void mineralogyDoesNotAllowSpawnArea() { + LifecycleProbe.Fixture f = fixture(); f.bucket = "mineralogy:crude_oil_bucket"; + f.source = new int[] {2060, 80, 2060}; f.min[0] = 0; + LifecycleProbe.validate(f, () -> true); + } + @Test(expected = IllegalArgumentException.class) public void mineralogyDoesNotAllowOversizedBounds() { + LifecycleProbe.Fixture f = fixture(); f.bucket = "mineralogy:crude_oil_bucket"; + f.source = new int[] {2060, 80, 2060}; f.max[0] = f.min[0] + 32; + LifecycleProbe.validate(f, () -> true); + } @Test(expected = IllegalArgumentException.class) public void oilBucketOutsideBoundsRejected() { LifecycleProbe.Fixture f = fixture(); f.bucket = "poweradvantage:crude_oil_bucket"; f.source = new int[] {2080, 80, 2060}; LifecycleProbe.validate(f); diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native114DefinitionTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native114DefinitionTest.java new file mode 100644 index 0000000..d06f08f --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/Native114DefinitionTest.java @@ -0,0 +1,24 @@ +package zone.moddev.mc.advantageworks; + +import com.google.gson.Gson; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class Native114DefinitionTest { + @Test + public void nativeTargetPreservesEverySourceStationAndDistrict() throws Exception { + Gson gson = new Gson(); + try (InputStreamReader reader = new InputStreamReader(getClass().getResourceAsStream( + "/assets/advantageworkstest/works/works-1.13.2.json"), StandardCharsets.UTF_8)) { + WorksDefinition source = gson.fromJson(reader, WorksDefinition.class); + WorksDefinition target = WorksDefinitionLoader.load(); + assertEquals("1.13.2", source.minecraft); + assertEquals("1.14.4", target.minecraft); + assertEquals(21, target.stations.size()); + assertEquals(gson.toJsonTree(source.districts), gson.toJsonTree(target.districts)); + assertEquals(gson.toJsonTree(source.stations), gson.toJsonTree(target.stations)); + } + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeTicketFixtureTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeTicketFixtureTest.java new file mode 100644 index 0000000..d1320b7 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/NativeTicketFixtureTest.java @@ -0,0 +1,58 @@ +package zone.moddev.mc.advantageworks; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import org.junit.Test; + +public class NativeTicketFixtureTest { + private LifecycleProbe.Fixture network() { + LifecycleProbe.Fixture f = new LifecycleProbe.Fixture(); + f.id = "C-FLUID"; + f.nativeTicketNetwork = true; + f.min = new int[] {20480, 72, 2048}; + f.max = new int[] {20639, 90, 2079}; + f.witnesses = Arrays.asList(new int[] {20488, 80, 2056}, new int[] {20616, 80, 2056}); + f.groups = new LinkedHashMap<>(); + f.groups.put("supply", Arrays.asList(new int[] {1280, 128})); + f.groups.put("machine", Arrays.asList(new int[] {1288, 128})); + return f; + } + + @Test public void boundedNativeNetworkAccepted() { LifecycleProbe.validate(network(), () -> false); } + @Test public void boundedFullHolderNetworkAccepted() { + LifecycleProbe.Fixture f = network(); f.max[0] = f.min[0] + 543; + LifecycleProbe.validate(f, () -> false); + } + @Test(expected = IllegalArgumentException.class) public void excessiveHolderFootprintRejected() { + LifecycleProbe.Fixture f = network(); f.max[0] = f.min[0] + 544; + LifecycleProbe.validate(f, () -> false); + } + @Test(expected = IllegalArgumentException.class) public void oldFixtureBoundsRemainStrict() { + LifecycleProbe.Fixture f = network(); f.nativeTicketNetwork = false; + LifecycleProbe.validate(f, () -> false); + } + @Test(expected = IllegalArgumentException.class) public void arbitraryMachineCannotOptIn() { + LifecycleProbe.Fixture f = network(); f.id = "S-MOVE-east"; + LifecycleProbe.validate(f, () -> false); + } + @Test(expected = IllegalArgumentException.class) public void bothAxesCannotExpand() { + LifecycleProbe.Fixture f = network(); f.max[2] = f.min[2] + 159; + LifecycleProbe.validate(f, () -> false); + } + @Test(expected = IllegalArgumentException.class) public void unboundedNativeLengthRejected() { + LifecycleProbe.Fixture f = network(); f.max[0]++; + LifecycleProbe.validate(f, () -> false); + } + @Test(expected = IllegalArgumentException.class) public void tallNativeNetworkRejected() { + LifecycleProbe.Fixture f = network(); f.max[1] = f.min[1] + 32; + LifecycleProbe.validate(f, () -> false); + } + @Test(expected = IllegalArgumentException.class) public void undeclaredGroupsRejected() { + LifecycleProbe.Fixture f = network(); f.groups.remove("supply"); + LifecycleProbe.validate(f, () -> false); + } + @Test(expected = IllegalArgumentException.class) public void outOfBoundsNativeGroupRejected() { + LifecycleProbe.Fixture f = network(); f.groups.put("bridge", Arrays.asList(new int[] {0, 0})); + LifecycleProbe.validate(f, () -> false); + } +} diff --git a/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native114ProfileTest.java b/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native114ProfileTest.java new file mode 100644 index 0000000..1bd3548 --- /dev/null +++ b/test-rig/src/test/java/zone/moddev/mc/advantageworks/runner/Native114ProfileTest.java @@ -0,0 +1,78 @@ +package zone.moddev.mc.advantageworks.runner; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.File; +import java.io.FileReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.junit.Test; +import static org.junit.Assert.*; + +public class Native114ProfileTest { + @Test + public void profilesSeparateUnqualifiedDestinationsFromHistoricalSources() throws Exception { + File[] files = new File("profiles/native114").listFiles((dir, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(10, files.length); + int ported = 0, blocked = 0; + for (File file : files) { + JsonObject raw; + try (FileReader reader = new FileReader(file)) { + raw = new JsonParser().parse(reader).getAsJsonObject(); + } + assertEquals("1.14.4", raw.get("minecraftVersion").getAsString()); + assertEquals("UNQUALIFIED", raw.get("qualificationStatus").getAsString()); + assertEquals("build/works-native114", raw.get("runtimeBase").getAsString()); + assertEquals("1.14.4-28.2.26", raw.getAsJsonObject("forge").get("version").getAsString()); + assertEquals(64, raw.getAsJsonObject("forge").get("sha256").getAsString().length()); + String source = raw.get("legacySourceProfile").getAsString(); + assertTrue(source, source.startsWith("../../../PA113/PowerAdvantage/test-rig/profiles/")); + assertEquals(new File(".").getCanonicalFile(), WorksRunner.findTestRig(file)); + if (raw.get("portStatus").getAsString().equals("UNPORTED")) { + blocked++; + try { WorksRunner.readProfile(file); fail("Unported profile was accepted: " + file); } + catch (IllegalStateException expected) { assertTrue(expected.getMessage().contains("not ported")); } + } else { + ported++; + WorksRunner.readProfile(file); + int harness = 0; + for (JsonElement element : raw.getAsJsonArray("mods")) { + JsonObject mod = element.getAsJsonObject(); + assertTrue(file.toString(), mod.get("version").getAsString().endsWith(".114041")); + assertFalse(file.toString(), mod.get("path").getAsString().contains("113")); + assertTrue(mod.get("workspaceProduced").getAsBoolean()); + if (mod.get("id").getAsString().equals("advantageworkstest")) harness++; + } + assertEquals(1, harness); + } + } + assertEquals(10, ported); + assertEquals(0, blocked); + } + + @Test + public void explicitlyUnportedProfilesStillFailClosed() throws Exception { + File file = File.createTempFile("unported-native114-", ".json"); + try { + Files.write(file.toPath(), ("{\"name\":\"blocked\",\"mode\":\"packaged\"," + + "\"minecraftVersion\":\"1.14.4\",\"portStatus\":\"UNPORTED\"," + + "\"portBlocker\":\"missing genuine native dependency\"}").getBytes(StandardCharsets.UTF_8)); + try { WorksRunner.readProfile(file); fail("Unported profile was accepted"); } + catch (IllegalStateException expected) { assertTrue(expected.getMessage().contains("not ported")); } + } finally { + Files.delete(file.toPath()); + } + } + + @Test + public void historicalQualificationCannotBeLaunchedAsNative114() throws Exception { + try { + WorksRunner.readProfile(new File("profiles/packaged-current.json")); + fail("Historical 1.13.2 profile must not launch native114"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("Historical source profile")); + } + } +}